Merge pull request #5182 from MetaMask/tx-activity

Add Transaction Details to the Transaction List view
feature/default_network_editable
Alexander Tseung 6 years ago committed by GitHub
commit 16d6cd5eb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      app/_locales/en/messages.json
  2. 3
      app/images/arrow-popout.svg
  3. 2
      test/e2e/beta/from-import-beta-ui.spec.js
  4. 10
      test/e2e/beta/metamask-beta-ui.spec.js
  5. 2
      test/integration/lib/confirm-sig-requests.js
  6. 8
      test/integration/lib/tx-list-items.js
  7. 5
      ui/app/components/button/button.component.js
  8. 25
      ui/app/components/card/card.component.js
  9. 1
      ui/app/components/card/index.js
  10. 11
      ui/app/components/card/index.scss
  11. 25
      ui/app/components/card/tests/card.component.test.js
  12. 7
      ui/app/components/currency-display/currency-display.component.js
  13. 8
      ui/app/components/currency-display/currency-display.container.js
  14. 44
      ui/app/components/currency-display/tests/currency-display.container.test.js
  15. 15
      ui/app/components/customize-gas-modal/index.js
  16. 161
      ui/app/components/hex-as-decimal-input.js
  17. 21
      ui/app/components/hex-to-decimal/hex-to-decimal.component.js
  18. 1
      ui/app/components/hex-to-decimal/index.js
  19. 26
      ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js
  20. 1
      ui/app/components/identicon.js
  21. 8
      ui/app/components/index.scss
  22. 11
      ui/app/components/modals/account-details-modal.js
  23. 15
      ui/app/components/modals/customize-gas/customize-gas.component.js
  24. 7
      ui/app/components/modals/deposit-ether-modal.js
  25. 36
      ui/app/components/modals/export-private-key-modal.js
  26. 31
      ui/app/components/pages/create-account/connect-hardware/account-list.js
  27. 15
      ui/app/components/pages/create-account/connect-hardware/connect-screen.js
  28. 19
      ui/app/components/pages/create-account/import-account/json.js
  29. 19
      ui/app/components/pages/create-account/import-account/private-key.js
  30. 19
      ui/app/components/pages/create-account/new-account.js
  31. 17
      ui/app/components/pages/keychains/reveal-seed.js
  32. 22
      ui/app/components/pages/settings/settings.js
  33. 3
      ui/app/components/sender-to-recipient/index.scss
  34. 2
      ui/app/components/sender-to-recipient/sender-to-recipient.component.js
  35. 8
      ui/app/components/shapeshift-form.js
  36. 10
      ui/app/components/signature-request.js
  37. 1
      ui/app/components/transaction-activity-log/index.js
  38. 63
      ui/app/components/transaction-activity-log/index.scss
  39. 35
      ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
  40. 27
      ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js
  41. 208
      ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
  42. 91
      ui/app/components/transaction-activity-log/transaction-activity-log.component.js
  43. 11
      ui/app/components/transaction-activity-log/transaction-activity-log.container.js
  44. 82
      ui/app/components/transaction-activity-log/transaction-activity-log.util.js
  45. 1
      ui/app/components/transaction-breakdown/index.js
  46. 23
      ui/app/components/transaction-breakdown/index.scss
  47. 37
      ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
  48. 1
      ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js
  49. 19
      ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss
  50. 39
      ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js
  51. 26
      ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
  52. 82
      ui/app/components/transaction-breakdown/transaction-breakdown.component.js
  53. 1
      ui/app/components/transaction-list-item-details/index.js
  54. 49
      ui/app/components/transaction-list-item-details/index.scss
  55. 66
      ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
  56. 80
      ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
  57. 37
      ui/app/components/transaction-list-item/index.scss
  58. 52
      ui/app/components/transaction-list-item/transaction-list-item.component.js
  59. 5
      ui/app/components/transaction-list-item/transaction-list-item.container.js
  60. 3
      ui/app/components/transaction-list/index.scss
  61. 6
      ui/app/components/wallet-view.js
  62. 2
      ui/app/constants/common.js
  63. 3
      ui/app/conversion-util.js
  64. 15
      ui/app/css/itcss/components/buttons.scss
  65. 4
      ui/app/css/itcss/components/send.scss
  66. 2
      ui/app/helpers/confirm-transaction/util.js
  67. 38
      ui/app/helpers/conversions.util.js
  68. 12
      ui/app/helpers/transactions.util.js
  69. 8
      ui/i18n-helper.js

@ -17,6 +17,9 @@
"accountSelectionRequired": {
"message": "You need to select an account!"
},
"activityLog": {
"message": "activity log"
},
"address": {
"message": "Address"
},
@ -857,6 +860,9 @@
"save": {
"message": "Save"
},
"speedUp": {
"message": "speed up"
},
"speedUpTitle": {
"message": "Speed Up Transaction"
},
@ -1085,6 +1091,27 @@
"total": {
"message": "Total"
},
"transaction": {
"message": "transaction"
},
"transactionConfirmed": {
"message": "Transaction confirmed on $2."
},
"transactionCreated": {
"message": "Transaction created with a value of $1 on $2."
},
"transactionDropped": {
"message": "Transaction dropped on $2."
},
"transactionSubmitted": {
"message": "Transaction submitted on $2."
},
"transactionUpdated": {
"message": "Transaction updated on $2."
},
"transactionUpdatedGas": {
"message": "Transaction updated with a gas price of $1 on $2."
},
"transactions": {
"message": "transactions"
},
@ -1131,6 +1158,9 @@
"unavailable": {
"message": "Unavailable"
},
"units": {
"message": "units"
},
"unknown": {
"message": "Unknown"
},

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.67589 0.641872C8.65169 0.642635 8.62756 0.644749 8.6036 0.648202H4.79279C4.55863 0.644896 4.34082 0.767704 4.22278 0.969601C4.10473 1.1715 4.10473 1.4212 4.22278 1.6231C4.34082 1.825 4.55863 1.9478 4.79279 1.9445H7.12113L0.437932 8.61587C0.268309 8.77843 0.19998 9.01984 0.259298 9.24697C0.318616 9.47411 0.496311 9.65149 0.723852 9.71071C0.951393 9.76992 1.19322 9.70171 1.35608 9.53239L8.03927 2.86102V5.18524C8.03596 5.41898 8.15899 5.6364 8.36124 5.75424C8.56349 5.87208 8.81364 5.87208 9.0159 5.75424C9.21815 5.6364 9.34118 5.41898 9.33787 5.18524V1.37863C9.36404 1.18976 9.30558 0.998955 9.17804 0.857009C9.0505 0.715062 8.86682 0.636369 8.67589 0.641872Z" fill="#359BDD"/>
</svg>

After

Width:  |  Height:  |  Size: 795 B

@ -317,7 +317,7 @@ describe('Using MetaMask with an existing account', function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '-1 ETH')
})

@ -408,7 +408,7 @@ describe('MetaMask', function () {
assert.equal(transactions.length, 1)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
}
})
@ -450,7 +450,7 @@ describe('MetaMask', function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 2)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
})
})
@ -528,7 +528,7 @@ describe('MetaMask', function () {
await delay(largeDelayMs)
await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@ -562,7 +562,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 4
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
// const txAccounts = await findElements(driver, By.css('.tx-list-account'))
@ -594,7 +594,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 5
}, 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
await closeAllWindowHandlesExcept(driver, [extension, dapp])

@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests')
reactTriggerChange(selectState[0])
const pendingRequestItem = $.find('.transaction-list-item')
const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid')
if (pendingRequestItem[0]) {
pendingRequestItem[0].click()

@ -32,9 +32,11 @@ async function runTxListItemsTest (assert, done) {
const txListItems = await queryAsync($, '.transaction-list-item')
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
const retryTx = txListItems[1]
const retryTxLink = await findAsync($(retryTx), '.transaction-list-item__retry')
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
const retryTxGrid = await findAsync($(txListItems[1]), '.transaction-list-item__grid')
retryTxGrid[0].click()
const retryTxDetails = await findAsync($(txListItems[1]), '.transaction-list-item-details')
const headerButtons = await findAsync($(retryTxDetails[0]), '.transaction-list-item-details__header-button')
assert.equal(headerButtons[0].textContent, 'speed up')
const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')

@ -6,6 +6,7 @@ const CLASSNAME_DEFAULT = 'btn-default'
const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_SECONDARY = 'btn-secondary'
const CLASSNAME_CONFIRM = 'btn-confirm'
const CLASSNAME_RAISED = 'btn-raised'
const CLASSNAME_LARGE = 'btn--large'
const typeHash = {
@ -13,6 +14,7 @@ const typeHash = {
primary: CLASSNAME_PRIMARY,
secondary: CLASSNAME_SECONDARY,
confirm: CLASSNAME_CONFIRM,
raised: CLASSNAME_RAISED,
}
export default class Button extends Component {
@ -20,7 +22,7 @@ export default class Button extends Component {
type: PropTypes.string,
large: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
}
render () {
@ -29,6 +31,7 @@ export default class Button extends Component {
return (
<button
className={classnames(
'button',
typeHash[type],
large && CLASSNAME_LARGE,
className

@ -0,0 +1,25 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class Card extends PureComponent {
static propTypes = {
className: PropTypes.string,
overrideClassName: PropTypes.bool,
title: PropTypes.string,
children: PropTypes.node,
}
render () {
const { className, overrideClassName, title } = this.props
return (
<div className={classnames({ 'card': !overrideClassName }, className)}>
<div className="card__title">
{ title }
</div>
{ this.props.children }
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './card.component'

@ -0,0 +1,11 @@
.card {
border-radius: 4px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
padding: 8px;
&__title {
border-bottom: 1px solid #d8d8d8;
padding-bottom: 4px;
text-transform: capitalize;
}
}

@ -0,0 +1,25 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import Card from '../card.component'
describe('Card Component', () => {
it('should render a card with a title and child element', () => {
const wrapper = shallow(
<Card
title="Test"
className="card-test-class"
>
<div className="child-test-class">Child</div>
</Card>
)
assert.ok(wrapper.hasClass('card-test-class'))
const title = wrapper.find('.card__title')
assert.ok(title)
assert.equal(title.text(), 'Test')
const child = wrapper.find('.child-test-class')
assert.ok(child)
assert.equal(child.text(), 'Child')
})
})

@ -1,13 +1,18 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { ETH } from '../../constants/common'
import { ETH, GWEI } from '../../constants/common'
export default class CurrencyDisplay extends PureComponent {
static propTypes = {
className: PropTypes.string,
displayValue: PropTypes.string,
prefix: PropTypes.string,
// Used in container
currency: PropTypes.oneOf([ETH]),
denomination: PropTypes.oneOf([GWEI]),
value: PropTypes.string,
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hideLabel: PropTypes.bool,
}
render () {

@ -3,13 +3,15 @@ import CurrencyDisplay from './currency-display.component'
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
const mapStateToProps = (state, ownProps) => {
const { value, numberOfDecimals = 2, currency } = ownProps
const { value, numberOfDecimals = 2, currency, denomination, hideLabel } = ownProps
const { metamask: { currentCurrency, conversionRate } } = state
const toCurrency = currency || currentCurrency
const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals })
const convertedValue = getValueFromWeiHex({
value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
})
const formattedValue = formatCurrency(convertedValue, toCurrency)
const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}`
const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}`
return {
displayValue,

@ -51,6 +51,50 @@ describe('CurrencyDisplay container', () => {
displayValue: '1.266 ETH',
},
},
{
props: {
value: '0x1193461d01595930',
currency: 'ETH',
numberOfDecimals: 3,
hideLabel: true,
},
result: {
displayValue: '1.266',
},
},
{
props: {
value: '0x3b9aca00',
currency: 'ETH',
denomination: 'GWEI',
hideLabel: true,
},
result: {
displayValue: '1',
},
},
{
props: {
value: '0x3b9aca00',
currency: 'ETH',
denomination: 'WEI',
hideLabel: true,
},
result: {
displayValue: '1000000000',
},
},
{
props: {
value: '0x3b9aca00',
currency: 'ETH',
numberOfDecimals: 100,
hideLabel: true,
},
result: {
displayValue: '1e-9',
},
},
]
tests.forEach(({ props, result }) => {

@ -5,6 +5,7 @@ const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const GasModalCard = require('./gas-modal-card')
import Button from '../button'
const ethUtil = require('ethereumjs-util')
@ -353,16 +354,16 @@ CustomizeGasModal.prototype.render = function () {
}, [this.context.t('revert')]),
h('div.send-v2__customize-gas__buttons', [
h('button.btn-default.send-v2__customize-gas__cancel', {
h(Button, {
type: 'default',
className: 'send-v2__customize-gas__cancel',
onClick: this.props.hideModal,
style: {
marginRight: '10px',
},
}, [this.context.t('cancel')]),
h('button.btn-primary.send-v2__customize-gas__save', {
h(Button, {
type: 'primary',
className: 'send-v2__customize-gas__save',
onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal),
className: error && 'btn-primary--disabled',
disabled: error,
}, [this.context.t('save')]),
]),

@ -1,161 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const extend = require('xtend')
const connect = require('react-redux').connect
HexAsDecimalInput.contextTypes = {
t: PropTypes.func,
}
module.exports = connect()(HexAsDecimalInput)
inherits(HexAsDecimalInput, Component)
function HexAsDecimalInput () {
this.state = { invalid: null }
Component.call(this)
}
/* Hex as Decimal Input
*
* A component for allowing easy, decimal editing
* of a passed in hex string value.
*
* On change, calls back its `onChange` function parameter
* and passes it an updated hex string.
*/
HexAsDecimalInput.prototype.render = function () {
const props = this.props
const state = this.state
const { value, onChange, min, max } = props
const toEth = props.toEth
const suffix = props.suffix
const decimalValue = decimalize(value, toEth)
const style = props.style
return (
h('.flex-column', [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('input.hex-input', {
type: 'number',
required: true,
min: min,
max: max,
style: extend({
display: 'block',
textAlign: 'right',
backgroundColor: 'transparent',
border: '1px solid #bdbdbd',
}, style),
value: parseInt(decimalValue),
onBlur: (event) => {
this.updateValidity(event)
},
onChange: (event) => {
this.updateValidity(event)
const hexString = (event.target.value === '') ? '' : hexify(event.target.value)
onChange(hexString)
},
onInvalid: (event) => {
const msg = this.constructWarning()
if (msg === state.invalid) {
return
}
this.setState({ invalid: msg })
event.preventDefault()
return false
},
}),
h('div', {
style: {
color: ' #AEAEAE',
fontSize: '12px',
marginLeft: '5px',
marginRight: '6px',
width: '20px',
},
}, suffix),
]),
state.invalid ? h('span.error', {
style: {
position: 'absolute',
right: '0px',
textAlign: 'right',
transform: 'translateY(26px)',
padding: '3px',
background: 'rgba(255,255,255,0.85)',
zIndex: '1',
textTransform: 'capitalize',
border: '2px solid #E20202',
},
}, state.invalid) : null,
])
)
}
HexAsDecimalInput.prototype.setValid = function (message) {
this.setState({ invalid: null })
}
HexAsDecimalInput.prototype.updateValidity = function (event) {
const target = event.target
const value = this.props.value
const newValue = target.value
if (value === newValue) {
return
}
const valid = target.checkValidity()
if (valid) {
this.setState({ invalid: null })
}
}
HexAsDecimalInput.prototype.constructWarning = function () {
const { name, min, max } = this.props
let message = name ? name + ' ' : ''
if (min && max) {
message += this.context.t('betweenMinAndMax', [min, max])
} else if (min) {
message += this.context.t('greaterThanMin', [min])
} else if (max) {
message += this.context.t('lessThanMax', [max])
} else {
message += this.context.t('invalidInput')
}
return message
}
function hexify (decimalString) {
const hexBN = new BN(parseInt(decimalString), 10)
return '0x' + hexBN.toString('hex')
}
function decimalize (input, toEth) {
if (input === '') {
return ''
} else {
const strippedInput = ethUtil.stripHexPrefix(input)
const inputBN = new BN(strippedInput, 'hex')
return inputBN.toString(10)
}
}

@ -0,0 +1,21 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { hexToDecimal } from '../../helpers/conversions.util'
export default class HexToDecimal extends PureComponent {
static propTypes = {
className: PropTypes.string,
value: PropTypes.string,
}
render () {
const { className, value } = this.props
const decimalValue = hexToDecimal(value)
return (
<div className={className}>
{ decimalValue }
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './hex-to-decimal.component'

@ -0,0 +1,26 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import HexToDecimal from '../hex-to-decimal.component'
describe('HexToDecimal Component', () => {
it('should render a prefixed hex as a decimal with a className', () => {
const wrapper = shallow(<HexToDecimal
value="0x3039"
className="hex-to-decimal"
/>)
assert.ok(wrapper.hasClass('hex-to-decimal'))
assert.equal(wrapper.text(), '12345')
})
it('should render an unprefixed hex as a decimal with a className', () => {
const wrapper = shallow(<HexToDecimal
value="1A85"
className="hex-to-decimal"
/>)
assert.ok(wrapper.hasClass('hex-to-decimal'))
assert.equal(wrapper.text(), '6789')
})
})

@ -56,6 +56,7 @@ IdenticonComponent.prototype.render = function () {
})
} else {
return h('img.balance-icon', {
className,
src: './images/eth_logo.svg',
style: {
...style,

@ -2,6 +2,8 @@
@import './button-group/index';
@import './card/index';
@import './confirm-page-container/index';
@import './export-text-container/index';
@ -24,6 +26,10 @@
@import './tabs/index';
@import './transaction-activity-log/index';
@import './transaction-breakdown/index';
@import './transaction-view/index';
@import './transaction-view-balance/index';
@ -32,6 +38,8 @@
@import './transaction-list-item/index';
@import './transaction-list-item-details/index';
@import './transaction-status/index';
@import './app-header/index';

@ -10,6 +10,8 @@ const genAccountLink = require('../../../lib/account-link.js')
const QrView = require('../qr-code')
const EditableLabel = require('../editable-label')
import Button from '../button'
function mapStateToProps (state) {
return {
network: state.metamask.network,
@ -80,12 +82,17 @@ AccountDetailsModal.prototype.render = function () {
h('div.account-modal-divider'),
h('button.btn-primary.account-modal__button', {
h(Button, {
type: 'primary',
className: 'account-modal__button',
onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }),
}, this.context.t('etherscanView')),
// Holding on redesign for Export Private Key functionality
exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', {
exportPrivateKeyFeatureEnabled ? h(Button, {
type: 'primary',
className: 'account-modal__button',
onClick: () => showExportPrivateKeyModal(),
}, this.context.t('exportPrivateKey')) : null,

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import GasModalCard from '../../customize-gas-modal/gas-modal-card'
import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
import Button from '../../button'
import {
getDecimalGasLimit,
@ -116,21 +117,23 @@ export default class CustomizeGas extends Component {
{ t('revert') }
</div>
<div className="customize-gas__buttons">
<button
className="btn-default customize-gas__cancel"
<Button
type="default"
className="customize-gas__cancel"
onClick={() => hideModal()}
style={{ marginRight: '10px' }}
>
{ t('cancel') }
</button>
<button
className="btn-primary customize-gas__save"
</Button>
<Button
type="primary"
className="customize-gas__save"
onClick={() => this.handleSave()}
style={{ marginRight: '10px' }}
disabled={!valid}
>
{ t('save') }
</button>
</Button>
</div>
</div>
</div>

@ -7,6 +7,8 @@ const actions = require('../../actions')
const { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util')
const ShapeshiftForm = require('../shapeshift-form')
import Button from '../button'
let DIRECT_DEPOSIT_ROW_TITLE
let DIRECT_DEPOSIT_ROW_TEXT
let COINBASE_ROW_TITLE
@ -109,7 +111,10 @@ DepositEtherModal.prototype.renderRow = function ({
]),
!hideButton && h('div.deposit-ether-modal__buy-row__button', [
h('button.btn-primary.btn--large.deposit-ether-modal__deposit-button', {
h(Button, {
type: 'primary',
className: 'deposit-ether-modal__deposit-button',
large: true,
onClick: onButtonClick,
}, [buttonLabel]),
]),

@ -11,6 +11,7 @@ const { getSelectedIdentity } = require('../../selectors')
const ReadOnlyInput = require('../readonly-input')
const copyToClipboard = require('copy-to-clipboard')
const { checksumAddress } = require('../../util')
import Button from '../button'
function mapStateToPropsFactory () {
let selectedIdentity = null
@ -97,24 +98,31 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) {
})
}
ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, label) {
return h('button', {
className,
onClick,
}, label)
}
ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) {
return h('div.export-private-key-buttons', {}, [
!privateKey && this.renderButton(
'btn-default btn--large export-private-key__button export-private-key__button--cancel',
() => hideModal(),
'Cancel'
),
!privateKey && h(Button, {
type: 'default',
large: true,
className: 'export-private-key__button export-private-key__button--cancel',
onClick: () => hideModal(),
}, this.context.t('cancel')),
(privateKey
? this.renderButton('btn-primary btn--large export-private-key__button', () => hideModal(), this.context.t('done'))
: this.renderButton('btn-primary btn--large export-private-key__button', () => this.exportAccountAndGetPrivateKey(this.state.password, address), this.context.t('confirm'))
? (
h(Button, {
type: 'primary',
large: true,
className: 'export-private-key__button',
onClick: () => hideModal(),
}, this.context.t('done'))
) : (
h(Button, {
type: 'primary',
large: true,
className: 'export-private-key__button',
onClick: () => this.exportAccountAndGetPrivateKey(this.state.password, address),
}, this.context.t('confirm'))
)
),
])

@ -3,6 +3,7 @@ const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const genAccountLink = require('../../../../../lib/account-link.js')
const Select = require('react-select').default
import Button from '../../../button'
class AccountList extends Component {
constructor (props, context) {
@ -143,22 +144,20 @@ class AccountList extends Component {
}
return h('div.new-account-connect-form__buttons', {}, [
h(
'button.btn-default.btn--large.new-account-connect-form__button',
{
onClick: this.props.onCancel.bind(this),
},
[this.context.t('cancel')]
),
h(
`button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
{
onClick: this.props.onUnlockAccount.bind(this, this.props.device),
...buttonProps,
},
[this.context.t('unlock')]
),
h(Button, {
type: 'default',
large: true,
className: 'new-account-connect-form__button',
onClick: this.props.onCancel.bind(this),
}, [this.context.t('cancel')]),
h(Button, {
type: 'primary',
large: true,
className: 'new-account-connect-form__button unlock',
disabled,
onClick: this.props.onUnlockAccount.bind(this, this.props.device),
}, [this.context.t('unlock')]),
])
}

@ -1,6 +1,7 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
import Button from '../../../button'
class ConnectScreen extends Component {
constructor (props, context) {
@ -60,13 +61,13 @@ class ConnectScreen extends Component {
h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
]),
h(
'button.btn-primary.btn--large',
{
onClick: () => global.platform.openWindow({
url: 'https://google.com/chrome',
}),
},
h(Button, {
type: 'primary',
large: true,
onClick: () => global.platform.openWindow({
url: 'https://google.com/chrome',
}),
},
this.context.t('downloadGoogleChrome')
),
])

@ -8,6 +8,7 @@ const actions = require('../../../../actions')
const FileInput = require('react-simple-file-input').default
const { DEFAULT_ROUTE } = require('../../../../routes')
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
import Button from '../../../button'
class JsonImportSubview extends Component {
constructor (props) {
@ -51,17 +52,19 @@ class JsonImportSubview extends Component {
h('div.new-account-create-form__buttons', {}, [
h('button.btn-default.new-account-create-form__button', {
h(Button, {
type: 'default',
large: true,
className: 'new-account-create-form__button',
onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, [
this.context.t('cancel'),
]),
}, [this.context.t('cancel')]),
h('button.btn-primary.new-account-create-form__button', {
h(Button, {
type: 'primary',
large: true,
className: 'new-account-create-form__button',
onClick: () => this.createNewKeychain(),
}, [
this.context.t('import'),
]),
}, [this.context.t('import')]),
]),

@ -7,6 +7,7 @@ const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const actions = require('../../../../actions')
const { DEFAULT_ROUTE } = require('../../../../routes')
import Button from '../../../button'
PrivateKeyImportView.contextTypes = {
t: PropTypes.func,
@ -61,20 +62,22 @@ PrivateKeyImportView.prototype.render = function () {
h('div.new-account-import-form__buttons', {}, [
h('button.btn-default.btn--large.new-account-create-form__button', {
h(Button, {
type: 'default',
large: true,
className: 'new-account-create-form__button',
onClick: () => {
displayWarning(null)
this.props.history.push(DEFAULT_ROUTE)
},
}, [
this.context.t('cancel'),
]),
}, [this.context.t('cancel')]),
h('button.btn-primary.btn--large.new-account-create-form__button', {
h(Button, {
type: 'primary',
large: true,
className: 'new-account-create-form__button',
onClick: () => this.createNewKeychain(),
}, [
this.context.t('import'),
]),
}, [this.context.t('import')]),
]),

@ -4,6 +4,7 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
import Button from '../../button'
class NewAccountCreateForm extends Component {
constructor (props, context) {
@ -38,20 +39,22 @@ class NewAccountCreateForm extends Component {
h('div.new-account-create-form__buttons', {}, [
h('button.btn-default.btn--large.new-account-create-form__button', {
h(Button, {
type: 'default',
large: true,
className: 'new-account-create-form__button',
onClick: () => history.push(DEFAULT_ROUTE),
}, [
this.context.t('cancel'),
]),
}, [this.context.t('cancel')]),
h('button.btn-primary.btn--large.new-account-create-form__button', {
h(Button, {
type: 'primary',
large: true,
className:'new-account-create-form__button',
onClick: () => {
createAccount(newAccountName || defaultAccountName)
.then(() => history.push(DEFAULT_ROUTE))
},
}, [
this.context.t('create'),
]),
}, [this.context.t('create')]),
]),

@ -8,6 +8,8 @@ const { requestRevealSeedWords } = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
const ExportTextContainer = require('../../export-text-container')
import Button from '../../button'
const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
@ -106,10 +108,16 @@ class RevealSeedPage extends Component {
renderPasswordPromptFooter () {
return (
h('.page-container__footer', [
h('button.btn-default.btn--large.page-container__footer-button', {
h(Button, {
type: 'default',
large: true,
className: 'page-container__footer-button',
onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, this.context.t('cancel')),
h('button.btn-primary.btn--large.page-container__footer-button', {
h(Button, {
type: 'primary',
large: true,
className: 'page-container__footer-button',
onClick: event => this.handleSubmit(event),
disabled: this.state.password === '',
}, this.context.t('next')),
@ -120,7 +128,10 @@ class RevealSeedPage extends Component {
renderRevealSeedFooter () {
return (
h('.page-container__footer', [
h('button.btn-default.btn--large.page-container__footer-button', {
h(Button, {
type: 'default',
large: true,
className: 'page-container__footer-button',
onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, this.context.t('close')),
])

@ -13,6 +13,8 @@ const ToggleButton = require('react-toggle-button')
const { REVEAL_SEED_ROUTE } = require('../../../routes')
const locales = require('../../../../../app/_locales/index.json')
import Button from '../../button'
const getInfuraCurrencyOptions = () => {
const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
@ -241,7 +243,10 @@ class Settings extends Component {
]),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('button.btn-primary.btn--large.settings__button', {
h(Button, {
type: 'primary',
large: true,
className: 'settings__button',
onClick (event) {
window.logStateString((err, result) => {
if (err) {
@ -266,7 +271,10 @@ class Settings extends Component {
h('div.settings__content-item', this.context.t('revealSeedWords')),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('button.btn-primary.btn--large.settings__button--red', {
h(Button, {
type: 'primary',
large: true,
className: 'settings__button--red',
onClick: event => {
event.preventDefault()
history.push(REVEAL_SEED_ROUTE)
@ -286,7 +294,10 @@ class Settings extends Component {
h('div.settings__content-item', this.context.t('useOldUI')),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('button.btn-primary.btn--large.settings__button--orange', {
h(Button, {
type: 'primary',
large: true,
className: 'settings__button--orange',
onClick (event) {
event.preventDefault()
setFeatureFlagToBeta()
@ -305,7 +316,10 @@ class Settings extends Component {
h('div.settings__content-item', this.context.t('resetAccount')),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('button.btn-primary.btn--large.settings__button--orange', {
h(Button, {
type: 'primary',
large: true,
className: 'settings__button--orange',
onClick (event) {
event.preventDefault()
showResetAccountConfirmationModal()

@ -80,13 +80,13 @@
justify-content: center;
position: relative;
flex: 0 0 auto;
padding: 8px;
.sender-to-recipient {
&__party {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex: 1;
border-radius: 4px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
@ -111,7 +111,6 @@
}
&__arrow-container {
padding: 0 2px;
display: flex;
justify-content: center;
align-items: center;

@ -115,7 +115,7 @@ export default class SenderToRecipient extends PureComponent {
renderRecipientWithoutAddress () {
return (
<div className="sender-to-recipient__party sender-to-recipient__party--recipient">
<i className="fa fa-file-text-o" />
{ !this.props.addressOnly && <i className="fa fa-file-text-o" /> }
<div className="sender-to-recipient__name">
{ this.context.t('newContract') }
</div>

@ -9,6 +9,8 @@ const { shapeShiftSubview, pairUpdate, buyWithShapeShift } = require('../actions
const { isValidAddress } = require('../util')
const SimpleDropdown = require('./dropdowns/simple-dropdown')
import Button from './button'
function mapStateToProps (state) {
const {
coinOptions,
@ -242,8 +244,10 @@ ShapeshiftForm.prototype.render = function () {
]),
!depositAddress && h('button.btn-primary.btn--large.shapeshift-form__shapeshift-buy-btn', {
className: btnClass,
!depositAddress && h(Button, {
type: 'primary',
large: true,
className: `${btnClass} shapeshift-form__shapeshift-buy-btn`,
disabled: !token,
onClick: () => this.onBuyWithShapeShift(),
}, [this.context.t('buy')]),

@ -23,6 +23,7 @@ const {
} = require('../selectors.js')
import { clearConfirmTransaction } from '../ducks/confirm-transaction.duck'
import Button from './button'
const { DEFAULT_ROUTE } = require('../routes')
@ -248,7 +249,10 @@ SignatureRequest.prototype.renderFooter = function () {
}
return h('div.request-signature__footer', [
h('button.btn-default.btn--large.request-signature__footer__cancel-button', {
h(Button, {
type: 'default',
large: true,
className: 'request-signature__footer__cancel-button',
onClick: event => {
cancel(event).then(() => {
this.props.clearConfirmTransaction()
@ -256,7 +260,9 @@ SignatureRequest.prototype.renderFooter = function () {
})
},
}, this.context.t('cancel')),
h('button.btn-primary.btn--large', {
h(Button, {
type: 'primary',
large: true,
onClick: event => {
sign(event).then(() => {
this.props.clearConfirmTransaction()

@ -0,0 +1 @@
export { default } from './transaction-activity-log.container'

@ -0,0 +1,63 @@
.transaction-activity-log {
&__card {
background: $white;
height: 100%;
}
&__activities-container {
padding-top: 8px;
}
&__activity {
padding: 4px 0;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
&::after {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 6px;
border-right: 1px solid $scorpion;
}
&:first-child::after {
height: 50%;
top: 50%;
}
&:last-child::after {
height: 50%;
}
}
&__activity-icon {
width: 13px;
height: 13px;
margin-right: 6px;
border-radius: 50%;
background: $scorpion;
flex: 0 0 auto;
}
&__activity-text {
color: $scorpion;
font-size: .75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__value {
display: inline;
font-weight: 500;
}
b {
font-weight: 500;
}
}

@ -0,0 +1,35 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import TransactionActivityLog from '../transaction-activity-log.component'
import Card from '../../card'
describe('TransactionActivityLog Component', () => {
it('should render properly', () => {
const transaction = {
history: [],
id: 1,
status: 'confirmed',
txParams: {
from: '0x1',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(
<TransactionActivityLog
transaction={transaction}
className="test-class"
/>,
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
)
assert.ok(wrapper.hasClass('transaction-activity-log'))
assert.ok(wrapper.hasClass('test-class'))
assert.equal(wrapper.find(Card).length, 1)
})
})

@ -0,0 +1,27 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../transaction-activity-log.container.js', {
'react-redux': {
connect: ms => {
mapStateToProps = ms
return () => ({})
},
},
})
describe('TransactionActivityLog container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
conversionRate: 280.45,
},
}
assert.deepEqual(mapStateToProps(mockState), { conversionRate: 280.45 })
})
})
})

@ -0,0 +1,208 @@
import assert from 'assert'
import { getActivities } from '../transaction-activity-log.util'
describe('getActivities', () => {
it('should return no activities for an empty history', () => {
const transaction = {
history: [],
id: 1,
status: 'confirmed',
txParams: {
from: '0x1',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
value: '0x2386f26fc10000',
},
}
assert.deepEqual(getActivities(transaction), [])
})
it('should return activities for a transaction\'s history', () => {
const transaction = {
history: [
{
id: 5559712943815343,
loadingDefaults: true,
metamaskNetworkId: '3',
status: 'unapproved',
time: 1535507561452,
txParams: {
from: '0x1',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
value: '0x2386f26fc10000',
},
},
[
{
op: 'replace',
path: '/loadingDefaults',
timestamp: 1535507561515,
value: false,
},
{
op: 'add',
path: '/gasPriceSpecified',
value: true,
},
{
op: 'add',
path: '/gasLimitSpecified',
value: true,
},
{
op: 'add',
path: '/estimatedGas',
value: '0x5208',
},
],
[
{
note: '#newUnapprovedTransaction - adding the origin',
op: 'add',
path: '/origin',
timestamp: 1535507561516,
value: 'MetaMask',
},
[],
],
[
{
note: 'confTx: user approved transaction',
op: 'replace',
path: '/txParams/gasPrice',
timestamp: 1535664571504,
value: '0x77359400',
},
],
[
{
note: 'txStateManager: setting status to approved',
op: 'replace',
path: '/status',
timestamp: 1535507564302,
value: 'approved',
},
],
[
{
note: 'transactions#approveTransaction',
op: 'add',
path: '/txParams/nonce',
timestamp: 1535507564439,
value: '0xa4',
},
{
op: 'add',
path: '/nonceDetails',
value: {
local: {},
network: {},
params: {},
},
},
],
[
{
note: 'transactions#publishTransaction',
op: 'replace',
path: '/status',
timestamp: 1535507564518,
value: 'signed',
},
{
op: 'add',
path: '/rawTx',
value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf',
},
],
[],
[
{
note: 'transactions#setTxHash',
op: 'add',
path: '/hash',
timestamp: 1535507564658,
value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7',
},
],
[
{
note: 'txStateManager - add submitted time stamp',
op: 'add',
path: '/submittedTime',
timestamp: 1535507564660,
value: 1535507564660,
},
],
[
{
note: 'txStateManager: setting status to submitted',
op: 'replace',
path: '/status',
timestamp: 1535507564665,
value: 'submitted',
},
],
[
{
note: 'transactions/pending-tx-tracker#event: tx:block-update',
op: 'add',
path: '/firstRetryBlockNumber',
timestamp: 1535507575476,
value: '0x3bf624',
},
],
[
{
note: 'txStateManager: setting status to confirmed',
op: 'replace',
path: '/status',
timestamp: 1535507615993,
value: 'confirmed',
},
],
],
id: 1,
status: 'confirmed',
txParams: {
from: '0x1',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
value: '0x2386f26fc10000',
},
}
const expectedResult = [
{
'eventKey': 'transactionCreated',
'timestamp': 1535507561452,
'value': '0x2386f26fc10000',
},
{
'eventKey': 'transactionUpdatedGas',
'timestamp': 1535664571504,
'value': '0x77359400',
},
{
'eventKey': 'transactionSubmitted',
'timestamp': 1535507564665,
'value': undefined,
},
{
'eventKey': 'transactionConfirmed',
'timestamp': 1535507615993,
'value': undefined,
},
]
assert.deepEqual(getActivities(transaction), expectedResult)
})
})

@ -0,0 +1,91 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { getActivities } from './transaction-activity-log.util'
import Card from '../card'
import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util'
import { ETH } from '../../constants/common'
import { formatDate } from '../../util'
export default class TransactionActivityLog extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
transaction: PropTypes.object,
className: PropTypes.string,
conversionRate: PropTypes.number,
}
state = {
activities: [],
}
componentDidMount () {
this.setActivites()
}
componentDidUpdate (prevProps) {
const { transaction: { history: prevHistory = [] } = {} } = prevProps
const { transaction: { history = [] } = {} } = this.props
if (prevHistory.length !== history.length) {
this.setActivites()
}
}
setActivites () {
const activities = getActivities(this.props.transaction)
this.setState({ activities })
}
renderActivity (activity, index) {
const { conversionRate } = this.props
const { eventKey, value, timestamp } = activity
const ethValue = index === 0
? `${getValueFromWeiHex({
value,
toCurrency: ETH,
conversionRate,
numberOfDecimals: 6,
})} ${ETH}`
: getEthConversionFromWeiHex({ value, toCurrency: ETH, conversionRate })
const formattedTimestamp = formatDate(timestamp)
const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp])
return (
<div
key={index}
className="transaction-activity-log__activity"
>
<div className="transaction-activity-log__activity-icon" />
<div
className="transaction-activity-log__activity-text"
title={activityText}
>
{ activityText }
</div>
</div>
)
}
render () {
const { t } = this.context
const { className } = this.props
const { activities } = this.state
return (
<div className={classnames('transaction-activity-log', className)}>
<Card
title={t('activityLog')}
className="transaction-activity-log__card"
>
<div className="transaction-activity-log__activities-container">
{ activities.map((activity, index) => this.renderActivity(activity, index)) }
</div>
</Card>
</div>
)
}
}

@ -0,0 +1,11 @@
import { connect } from 'react-redux'
import TransactionActivityLog from './transaction-activity-log.component'
import { conversionRateSelector } from '../../selectors'
const mapStateToProps = state => {
return {
conversionRate: conversionRateSelector(state),
}
}
export default connect(mapStateToProps)(TransactionActivityLog)

@ -0,0 +1,82 @@
// path constants
const STATUS_PATH = '/status'
const GAS_PRICE_PATH = '/txParams/gasPrice'
// status constants
const UNAPPROVED_STATUS = 'unapproved'
const SUBMITTED_STATUS = 'submitted'
const CONFIRMED_STATUS = 'confirmed'
const DROPPED_STATUS = 'dropped'
// op constants
const REPLACE_OP = 'replace'
// event constants
const TRANSACTION_CREATED_EVENT = 'transactionCreated'
const TRANSACTION_UPDATED_GAS_EVENT = 'transactionUpdatedGas'
const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted'
const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed'
const TRANSACTION_DROPPED_EVENT = 'transactionDropped'
const TRANSACTION_UPDATED_EVENT = 'transactionUpdated'
const eventPathsHash = {
[STATUS_PATH]: true,
[GAS_PRICE_PATH]: true,
}
const statusHash = {
[SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT,
[CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT,
[DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
}
function eventCreator (eventKey, timestamp, value) {
return {
eventKey,
timestamp,
value,
}
}
export function getActivities (transaction) {
const { history = [] } = transaction
return history.reduce((acc, base) => {
// First history item should be transaction creation
if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) {
const { time, txParams: { value } = {} } = base
return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value))
} else if (Array.isArray(base)) {
const events = []
base.forEach(entry => {
const { op, path, value, timestamp } = entry
if (path in eventPathsHash && op === REPLACE_OP) {
switch (path) {
case STATUS_PATH: {
if (value in statusHash) {
events.push(eventCreator(statusHash[value], timestamp))
}
break
}
case GAS_PRICE_PATH: {
events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value))
break
}
default: {
events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp))
}
}
}
})
return acc.concat(events)
}
return acc
}, [])
}

@ -0,0 +1 @@
export { default } from './transaction-breakdown.component'

@ -0,0 +1,23 @@
@import './transaction-breakdown-row/index';
.transaction-breakdown {
&__card {
background: $white;
height: 100%;
}
&__row-title {
text-transform: capitalize;
}
&__value {
text-align: end;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&--eth-total {
font-weight: 500;
}
}
}

@ -0,0 +1,37 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import TransactionBreakdown from '../transaction-breakdown.component'
import TransactionBreakdownRow from '../transaction-breakdown-row'
import Card from '../../card'
describe('TransactionBreakdown Component', () => {
it('should render properly', () => {
const transaction = {
history: [],
id: 1,
status: 'confirmed',
txParams: {
from: '0x1',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(
<TransactionBreakdown
transaction={transaction}
className="test-class"
/>,
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
)
assert.ok(wrapper.hasClass('transaction-breakdown'))
assert.ok(wrapper.hasClass('test-class'))
assert.equal(wrapper.find(Card).length, 1)
assert.equal(wrapper.find(Card).find(TransactionBreakdownRow).length, 4)
})
})

@ -0,0 +1 @@
export { default } from './transaction-breakdown-row.component'

@ -0,0 +1,19 @@
.transaction-breakdown-row {
font-size: .75rem;
color: $scorpion;
display: flex;
justify-content: space-between;
padding: 8px 0;
&:not(:last-child) {
border-bottom: 1px solid #d8d8d8;
}
&__title {
padding-right: 8px;
}
&__value {
min-width: 0;
}
}

@ -0,0 +1,39 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import TransactionBreakdownRow from '../transaction-breakdown-row.component'
import Button from '../../../button'
describe('TransactionBreakdownRow Component', () => {
it('should render text properly', () => {
const wrapper = shallow(
<TransactionBreakdownRow
title="test"
className="test-class"
>
Test
</TransactionBreakdownRow>,
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
)
assert.ok(wrapper.hasClass('transaction-breakdown-row'))
assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test')
assert.equal(wrapper.find('.transaction-breakdown-row__value').text(), 'Test')
})
it('should render components properly', () => {
const wrapper = shallow(
<TransactionBreakdownRow
title="test"
className="test-class"
>
<Button onClick={() => {}} >Button</Button>
</TransactionBreakdownRow>,
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
)
assert.ok(wrapper.hasClass('transaction-breakdown-row'))
assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test')
assert.ok(wrapper.find('.transaction-breakdown-row__value').find(Button))
})
})

@ -0,0 +1,26 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class TransactionBreakdownRow extends PureComponent {
static propTypes = {
title: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
}
render () {
const { title, children, className } = this.props
return (
<div className={classnames('transaction-breakdown-row', className)}>
<div className="transaction-breakdown-row__title">
{ title }
</div>
<div className="transaction-breakdown-row__value">
{ children }
</div>
</div>
)
}
}

@ -0,0 +1,82 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import TransactionBreakdownRow from './transaction-breakdown-row'
import Card from '../card'
import CurrencyDisplay from '../currency-display'
import HexToDecimal from '../hex-to-decimal'
import { ETH, GWEI } from '../../constants/common'
import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
import { sumHexes } from '../../helpers/transactions.util'
export default class TransactionBreakdown extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
transaction: PropTypes.object,
className: PropTypes.string,
}
static defaultProps = {
transaction: {},
}
render () {
const { t } = this.context
const { transaction, className } = this.props
const { txParams: { gas, gasPrice, value } = {} } = transaction
const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice })
const totalInHex = sumHexes(hexGasTotal, value)
return (
<div className={classnames('transaction-breakdown', className)}>
<Card
title={t('transaction')}
className="transaction-breakdown__card"
>
<TransactionBreakdownRow title={t('amount')}>
<CurrencyDisplay
className="transaction-breakdown__value"
currency={ETH}
value={value}
/>
</TransactionBreakdownRow>
<TransactionBreakdownRow
title={`${t('gasLimit')} (${t('units')})`}
className="transaction-breakdown__row-title"
>
<HexToDecimal
className="transaction-breakdown__value"
value={gas}
/>
</TransactionBreakdownRow>
<TransactionBreakdownRow title={t('gasPrice')}>
<CurrencyDisplay
className="transaction-breakdown__value"
currency={ETH}
denomination={GWEI}
value={gasPrice}
hideLabel
/>
</TransactionBreakdownRow>
<TransactionBreakdownRow title={t('total')}>
<div>
<CurrencyDisplay
className="transaction-breakdown__value transaction-breakdown__value--eth-total"
currency={ETH}
value={totalInHex}
numberOfDecimals={6}
/>
<CurrencyDisplay
className="transaction-breakdown__value"
value={totalInHex}
/>
</div>
</TransactionBreakdownRow>
</Card>
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './transaction-list-item-details.component'

@ -0,0 +1,49 @@
.transaction-list-item-details {
&__header {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
&__header-buttons {
display: flex;
flex-direction: row;
}
&__header-button {
font-size: .625rem;
&:not(:last-child) {
margin-right: 8px;
}
}
&__sender-to-recipient-container {
margin-bottom: 8px;
}
&__cards-container {
display: flex;
flex-direction: row;
@media screen and (max-width: $break-small) {
flex-direction: column;
}
}
&__transaction-breakdown {
flex: 1;
margin-right: 8px;
min-width: 0;
@media screen and (max-width: $break-small) {
margin: 0 0 8px 0;
}
}
&__transaction-activity-log {
flex: 2;
min-width: 0;
}
}

@ -0,0 +1,66 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import TransactionListItemDetails from '../transaction-list-item-details.component'
import Button from '../../button'
import SenderToRecipient from '../../sender-to-recipient'
import TransactionBreakdown from '../../transaction-breakdown'
import TransactionActivityLog from '../../transaction-activity-log'
describe('TransactionListItemDetails Component', () => {
it('should render properly', () => {
const transaction = {
history: [],
id: 1,
status: 'confirmed',
txParams: {
from: '0x1',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(
<TransactionListItemDetails
transaction={transaction}
/>,
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
)
assert.ok(wrapper.hasClass('transaction-list-item-details'))
assert.equal(wrapper.find(Button).length, 1)
assert.equal(wrapper.find(SenderToRecipient).length, 1)
assert.equal(wrapper.find(TransactionBreakdown).length, 1)
assert.equal(wrapper.find(TransactionActivityLog).length, 1)
})
it('should render a retry button', () => {
const transaction = {
history: [],
id: 1,
status: 'confirmed',
txParams: {
from: '0x1',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(
<TransactionListItemDetails
transaction={transaction}
showRetry={true}
/>,
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
)
assert.ok(wrapper.hasClass('transaction-list-item-details'))
assert.equal(wrapper.find(Button).length, 2)
})
})

@ -0,0 +1,80 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import SenderToRecipient from '../sender-to-recipient'
import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants'
import TransactionActivityLog from '../transaction-activity-log'
import TransactionBreakdown from '../transaction-breakdown'
import Button from '../button'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
export default class TransactionListItemDetails extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
transaction: PropTypes.object,
showRetry: PropTypes.bool,
}
handleEtherscanClick = () => {
const { hash, metamaskNetworkId } = this.props.transaction
const prefix = prefixForNetwork(metamaskNetworkId)
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
global.platform.openWindow({ url: etherscanUrl })
this.setState({ showTransactionDetails: true })
}
render () {
const { t } = this.context
const { transaction, showRetry } = this.props
const { txParams: { to, from } = {} } = transaction
return (
<div className="transaction-list-item-details">
<div className="transaction-list-item-details__header">
<div>Details</div>
<div className="transaction-list-item-details__header-buttons">
{
showRetry && (
<Button
type="raised"
onClick={this.handleEtherscanClick}
className="transaction-list-item-details__header-button"
>
{ t('speedUp') }
</Button>
)
}
<Button
type="raised"
onClick={this.handleEtherscanClick}
className="transaction-list-item-details__header-button"
>
<img src="/images/arrow-popout.svg" />
</Button>
</div>
</div>
<div className="transaction-list-item-details__sender-to-recipient-container">
<SenderToRecipient
variant={CARDS_VARIANT}
addressOnly
recipientAddress={to}
senderAddress={from}
/>
</div>
<div className="transaction-list-item-details__cards-container">
<TransactionBreakdown
transaction={transaction}
className="transaction-list-item-details__transaction-breakdown"
/>
<TransactionActivityLog
transaction={transaction}
className="transaction-list-item-details__transaction-activity-log"
/>
</div>
</div>
)
}
}

@ -1,37 +1,34 @@
.transaction-list-item {
box-sizing: border-box;
min-height: 74px;
padding: 8px 20px;
border-bottom: 1px solid $geyser;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
@media screen and (max-width: $break-small) {
padding: 8px 20px 12px;
}
&:hover {
background: rgba($alto, .2);
}
&__grid {
cursor: pointer;
width: 100%;
padding: 16px 20px;
display: grid;
grid-template-columns: 45px 1fr 1fr 1fr;
grid-template-areas:
"identicon action status primary-amount"
"identicon nonce status secondary-amount";
@media screen and (max-width: $break-small) {
grid-template-columns: 45px 5fr 3fr;
grid-template-areas:
"nonce nonce nonce"
"identicon action primary-amount"
"identicon status secondary-amount";
}
@media screen and (max-width: $break-small) {
padding: 8px 20px 12px;
grid-template-columns: 45px 5fr 3fr;
grid-template-areas:
"nonce nonce nonce"
"identicon action primary-amount"
"identicon status secondary-amount";
}
&:hover {
background: rgba($alto, .2);
}
}
&__identicon {
@ -114,4 +111,10 @@
font-size: .5rem;
}
}
&__details-container {
padding: 8px 16px 16px;
background: #f3f4f7;
width: 100%;
}
}

@ -5,7 +5,7 @@ import TransactionStatus from '../transaction-status'
import TransactionAction from '../transaction-action'
import CurrencyDisplay from '../currency-display'
import TokenCurrencyDisplay from '../token-currency-display'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
import TransactionListItemDetails from '../transaction-list-item-details'
import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
import { ETH } from '../../constants/common'
@ -22,19 +22,24 @@ export default class TransactionListItem extends PureComponent {
nonceAndDate: PropTypes.string,
token: PropTypes.object,
assetImages: PropTypes.object,
tokenData: PropTypes.object,
}
state = {
showTransactionDetails: false,
}
handleClick = () => {
const { transaction, history } = this.props
const { id, status, hash, metamaskNetworkId } = transaction
const { id, status } = transaction
const { showTransactionDetails } = this.state
if (status === UNAPPROVED_STATUS) {
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
} else if (hash) {
const prefix = prefixForNetwork(metamaskNetworkId)
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
global.platform.openWindow({ url: etherscanUrl })
return
}
this.setState({ showTransactionDetails: !showTransactionDetails })
}
handleRetryClick = event => {
@ -75,6 +80,8 @@ export default class TransactionListItem extends PureComponent {
className="transaction-list-item__amount transaction-list-item__amount--primary"
value={value}
prefix="-"
numberOfDecimals={2}
currency={ETH}
/>
)
}
@ -89,8 +96,6 @@ export default class TransactionListItem extends PureComponent {
className="transaction-list-item__amount transaction-list-item__amount--secondary"
prefix="-"
value={value}
numberOfDecimals={2}
currency={ETH}
/>
)
}
@ -102,20 +107,25 @@ export default class TransactionListItem extends PureComponent {
showRetry,
nonceAndDate,
assetImages,
tokenData,
} = this.props
const { txParams = {} } = transaction
const { showTransactionDetails } = this.state
const toAddress = tokenData
? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to
: txParams.to
return (
<div
className="transaction-list-item"
onClick={this.handleClick}
>
<div className="transaction-list-item__grid">
<div className="transaction-list-item">
<div
className="transaction-list-item__grid"
onClick={this.handleClick}
>
<Identicon
className="transaction-list-item__identicon"
address={txParams.to}
address={toAddress}
diameter={34}
image={assetImages[txParams.to]}
image={assetImages[toAddress]}
/>
<TransactionAction
transaction={transaction}
@ -141,12 +151,12 @@ export default class TransactionListItem extends PureComponent {
{ this.renderSecondaryCurrency() }
</div>
{
showRetry && methodData.done && (
<div
className="transaction-list-item__retry"
onClick={this.handleRetryClick}
>
<span>Taking too long? Increase the gas price on your transaction</span>
showTransactionDetails && (
<div className="transaction-list-item__details-container">
<TransactionListItemDetails
transaction={transaction}
showRetry={showRetry && methodData.done}
/>
</div>
)
}

@ -5,16 +5,19 @@ import withMethodData from '../../higher-order-components/with-method-data'
import TransactionListItem from './transaction-list-item.component'
import { setSelectedToken, retryTransaction } from '../../actions'
import { hexToDecimal } from '../../helpers/conversions.util'
import { getTokenData } from '../../helpers/transactions.util'
import { formatDate } from '../../util'
const mapStateToProps = (state, ownProps) => {
const { transaction: { txParams: { value, nonce } = {}, time } = {} } = ownProps
const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps
const tokenData = data && getTokenData(data)
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
return {
value,
nonceAndDate,
tokenData,
}
}

@ -7,7 +7,7 @@
&__completed-transactions {
display: flex;
flex-direction: column;
height: 100%;
flex: 1;
}
&__header {
@ -35,6 +35,7 @@
flex: 1;
display: grid;
grid-template-rows: 35% 1fr;
padding-top: 8px;
}
&__empty-text {

@ -17,6 +17,8 @@ const TokenList = require('./token-list')
const selectors = require('../selectors')
const { ADD_TOKEN_ROUTE } = require('../routes')
import Button from './button'
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
@ -199,7 +201,9 @@ WalletView.prototype.render = function () {
h(TokenList),
h('button.btn-primary.wallet-view__add-token-button', {
h(Button, {
type: 'primary',
className: 'wallet-view__add-token-button',
onClick: () => {
history.push(ADD_TOKEN_ROUTE)
sidebarOpen && hideSidebar()

@ -1 +1,3 @@
export const ETH = 'ETH'
export const GWEI = 'GWEI'
export const WEI = 'WEI'

@ -35,6 +35,7 @@ BigNumber.config({
// Big Number Constants
const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000')
const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1')
// Individual Setters
const convert = R.invoker(1, 'times')
@ -52,10 +53,12 @@ const toBigNumber = {
const toNormalizedDenomination = {
WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER),
GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER),
ETH: bigNumber => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER),
}
const toSpecifiedDenomination = {
WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(),
GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9),
ETH: bigNumber => bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).round(9),
}
const baseChange = {
hex: n => n.toString(16),

@ -2,10 +2,7 @@
Buttons
*/
.btn-default,
.btn-primary,
.btn-secondary,
.btn-confirm {
.button {
height: 44px;
background: $white;
display: flex;
@ -79,6 +76,16 @@
background-color: $curious-blue;
}
.btn-raised {
color: $curious-blue;
background-color: $white;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
padding: 6px;
height: initial;
width: initial;
min-width: initial;
}
.btn--large {
height: 54px;
}

@ -837,6 +837,10 @@
line-height: 12px;
color: $red;
}
&__cancel {
margin-right: 10px;
}
}
&__gas-modal-card {

@ -58,6 +58,7 @@ export function getValueFromWeiHex ({
toCurrency,
conversionRate,
numberOfDecimals,
toDenomination,
}) {
return conversionUtil(value, {
fromNumericBase: 'hex',
@ -66,6 +67,7 @@ export function getValueFromWeiHex ({
toCurrency,
numberOfDecimals,
fromDenomination: 'WEI',
toDenomination,
conversionRate,
})
}

@ -1,4 +1,5 @@
import { conversionUtil } from '../conversion-util'
import { ETH, GWEI, WEI } from '../constants/common'
export function hexToDecimal (hexValue) {
return conversionUtil(hexValue, {
@ -7,16 +8,27 @@ export function hexToDecimal (hexValue) {
})
}
export function getEthFromWeiHex ({
value,
conversionRate,
}) {
return getValueFromWeiHex({
value,
conversionRate,
toCurrency: 'ETH',
numberOfDecimals: 6,
})
export function getEthConversionFromWeiHex ({ value, conversionRate, numberOfDecimals = 6 }) {
const denominations = [ETH, GWEI, WEI]
let nonZeroDenomination
for (let i = 0; i < denominations.length; i++) {
const convertedValue = getValueFromWeiHex({
value,
conversionRate,
toCurrency: ETH,
numberOfDecimals,
toDenomination: denominations[i],
})
if (convertedValue !== '0' || i === denominations.length - 1) {
nonZeroDenomination = `${convertedValue} ${denominations[i]}`
break
}
}
return nonZeroDenomination
}
export function getValueFromWeiHex ({
@ -24,14 +36,16 @@ export function getValueFromWeiHex ({
toCurrency,
conversionRate,
numberOfDecimals,
toDenomination,
}) {
return conversionUtil(value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
fromCurrency: ETH,
toCurrency,
numberOfDecimals,
fromDenomination: 'WEI',
fromDenomination: WEI,
toDenomination,
conversionRate,
})
}

@ -16,6 +16,8 @@ import {
UNKNOWN_FUNCTION_KEY,
} from '../constants/transactions'
import { addCurrencies } from '../conversion-util'
abiDecoder.addABI(abi)
export function getTokenData (data = {}) {
@ -103,3 +105,13 @@ export async function isSmartContractAddress (address) {
const code = await global.eth.getCode(address)
return code && code !== '0x'
}
export function sumHexes (...args) {
const total = args.reduce((acc, base) => {
return addCurrencies(acc, base, {
toNumericBase: 'hex',
})
})
return ethUtil.addHexPrefix(total)
}

@ -20,10 +20,10 @@ const getMessage = (locale, key, substitutions) => {
let phrase = entry.message
// perform substitutions
if (substitutions && substitutions.length) {
phrase = phrase.replace(/\$1/g, substitutions[0])
if (substitutions.length > 1) {
phrase = phrase.replace(/\$2/g, substitutions[1])
}
substitutions.forEach((substitution, index) => {
const regex = new RegExp(`\\$${index + 1}`, 'g')
phrase = phrase.replace(regex, substitution)
})
}
return phrase
}

Loading…
Cancel
Save