aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/app/components/button/button.component.js5
-rw-r--r--ui/app/components/card/card.component.js25
-rw-r--r--ui/app/components/card/index.js1
-rw-r--r--ui/app/components/card/index.scss11
-rw-r--r--ui/app/components/card/tests/card.component.test.js25
-rw-r--r--ui/app/components/currency-display/currency-display.component.js7
-rw-r--r--ui/app/components/currency-display/currency-display.container.js8
-rw-r--r--ui/app/components/currency-display/tests/currency-display.container.test.js44
-rw-r--r--ui/app/components/customize-gas-modal/index.js15
-rw-r--r--ui/app/components/hex-as-decimal-input.js161
-rw-r--r--ui/app/components/hex-to-decimal/hex-to-decimal.component.js21
-rw-r--r--ui/app/components/hex-to-decimal/index.js1
-rw-r--r--ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js26
-rw-r--r--ui/app/components/identicon.js1
-rw-r--r--ui/app/components/index.scss8
-rw-r--r--ui/app/components/modals/account-details-modal.js11
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.component.js15
-rw-r--r--ui/app/components/modals/deposit-ether-modal.js7
-rw-r--r--ui/app/components/modals/export-private-key-modal.js36
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js31
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js15
-rw-r--r--ui/app/components/pages/create-account/import-account/json.js19
-rw-r--r--ui/app/components/pages/create-account/import-account/private-key.js19
-rw-r--r--ui/app/components/pages/create-account/new-account.js19
-rw-r--r--ui/app/components/pages/keychains/reveal-seed.js17
-rw-r--r--ui/app/components/pages/settings/settings.js22
-rw-r--r--ui/app/components/sender-to-recipient/index.scss3
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js2
-rw-r--r--ui/app/components/shapeshift-form.js8
-rw-r--r--ui/app/components/signature-request.js10
-rw-r--r--ui/app/components/transaction-activity-log/index.js1
-rw-r--r--ui/app/components/transaction-activity-log/index.scss63
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js35
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js27
-rw-r--r--ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js208
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.component.js91
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.container.js11
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.util.js82
-rw-r--r--ui/app/components/transaction-breakdown/index.js1
-rw-r--r--ui/app/components/transaction-breakdown/index.scss23
-rw-r--r--ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js37
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js1
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss19
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js39
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js26
-rw-r--r--ui/app/components/transaction-breakdown/transaction-breakdown.component.js82
-rw-r--r--ui/app/components/transaction-list-item-details/index.js1
-rw-r--r--ui/app/components/transaction-list-item-details/index.scss49
-rw-r--r--ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js66
-rw-r--r--ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js80
-rw-r--r--ui/app/components/transaction-list-item/index.scss37
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.component.js52
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.container.js5
-rw-r--r--ui/app/components/transaction-list/index.scss3
-rw-r--r--ui/app/components/wallet-view.js6
-rw-r--r--ui/app/constants/common.js2
-rw-r--r--ui/app/conversion-util.js3
-rw-r--r--ui/app/css/itcss/components/buttons.scss15
-rw-r--r--ui/app/css/itcss/components/send.scss4
-rw-r--r--ui/app/helpers/confirm-transaction/util.js2
-rw-r--r--ui/app/helpers/conversions.util.js38
-rw-r--r--ui/app/helpers/transactions.util.js12
-rw-r--r--ui/i18n-helper.js8
63 files changed, 1404 insertions, 318 deletions
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
index 1e0ef1b64..4a06333e7 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/button/button.component.js
@@ -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
diff --git a/ui/app/components/card/card.component.js b/ui/app/components/card/card.component.js
new file mode 100644
index 000000000..bb7241da1
--- /dev/null
+++ b/ui/app/components/card/card.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/card/index.js b/ui/app/components/card/index.js
new file mode 100644
index 000000000..c3ca6e3f4
--- /dev/null
+++ b/ui/app/components/card/index.js
@@ -0,0 +1 @@
+export { default } from './card.component'
diff --git a/ui/app/components/card/index.scss b/ui/app/components/card/index.scss
new file mode 100644
index 000000000..bde54a15e
--- /dev/null
+++ b/ui/app/components/card/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/card/tests/card.component.test.js b/ui/app/components/card/tests/card.component.test.js
new file mode 100644
index 000000000..cea05033f
--- /dev/null
+++ b/ui/app/components/card/tests/card.component.test.js
@@ -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')
+ })
+})
diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js
index 389791b42..e4eb58a2a 100644
--- a/ui/app/components/currency-display/currency-display.component.js
+++ b/ui/app/components/currency-display/currency-display.component.js
@@ -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 () {
diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js
index b8a738c65..6644a1099 100644
--- a/ui/app/components/currency-display/currency-display.container.js
+++ b/ui/app/components/currency-display/currency-display.container.js
@@ -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,
diff --git a/ui/app/components/currency-display/tests/currency-display.container.test.js b/ui/app/components/currency-display/tests/currency-display.container.test.js
index 474ce5378..5265bbb04 100644
--- a/ui/app/components/currency-display/tests/currency-display.container.test.js
+++ b/ui/app/components/currency-display/tests/currency-display.container.test.js
@@ -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 }) => {
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
index c255fd64d..e67fbe45b 100644
--- a/ui/app/components/customize-gas-modal/index.js
+++ b/ui/app/components/customize-gas-modal/index.js
@@ -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')]),
]),
diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js
deleted file mode 100644
index 75303a34a..000000000
--- a/ui/app/components/hex-as-decimal-input.js
+++ /dev/null
@@ -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)
- }
-}
diff --git a/ui/app/components/hex-to-decimal/hex-to-decimal.component.js b/ui/app/components/hex-to-decimal/hex-to-decimal.component.js
new file mode 100644
index 000000000..6847a6919
--- /dev/null
+++ b/ui/app/components/hex-to-decimal/hex-to-decimal.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/hex-to-decimal/index.js b/ui/app/components/hex-to-decimal/index.js
new file mode 100644
index 000000000..6e8567ca9
--- /dev/null
+++ b/ui/app/components/hex-to-decimal/index.js
@@ -0,0 +1 @@
+export { default } from './hex-to-decimal.component'
diff --git a/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js b/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js
new file mode 100644
index 000000000..c98da9ad4
--- /dev/null
+++ b/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js
@@ -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')
+ })
+})
diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js
index 076e65b81..7bd921892 100644
--- a/ui/app/components/identicon.js
+++ b/ui/app/components/identicon.js
@@ -56,6 +56,7 @@ IdenticonComponent.prototype.render = function () {
})
} else {
return h('img.balance-icon', {
+ className,
src: './images/eth_logo.svg',
style: {
...style,
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
index cb4065fd9..983d6b98a 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -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';
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
index bc577fda0..248ffe008 100644
--- a/ui/app/components/modals/account-details-modal.js
+++ b/ui/app/components/modals/account-details-modal.js
@@ -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,
diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js
index 0337c5413..3f526bd43 100644
--- a/ui/app/components/modals/customize-gas/customize-gas.component.js
+++ b/ui/app/components/modals/customize-gas/customize-gas.component.js
@@ -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>
diff --git a/ui/app/components/modals/deposit-ether-modal.js b/ui/app/components/modals/deposit-ether-modal.js
index 2daa7fa1d..09137d39a 100644
--- a/ui/app/components/modals/deposit-ether-modal.js
+++ b/ui/app/components/modals/deposit-ether-modal.js
@@ -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]),
]),
diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js
index 60a416304..d3e3c9a56 100644
--- a/ui/app/components/modals/export-private-key-modal.js
+++ b/ui/app/components/modals/export-private-key-modal.js
@@ -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'))
+ )
),
])
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
index 488a189ea..2767b2e1f 100644
--- a/ui/app/components/pages/create-account/connect-hardware/account-list.js
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -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')]),
])
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
index b3dfa4ee2..d3abf3119 100644
--- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -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')
),
])
diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js
index dd57256a3..32b1065aa 100644
--- a/ui/app/components/pages/create-account/import-account/json.js
+++ b/ui/app/components/pages/create-account/import-account/json.js
@@ -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')]),
]),
diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js
index 1db999f2f..8db1bfbdd 100644
--- a/ui/app/components/pages/create-account/import-account/private-key.js
+++ b/ui/app/components/pages/create-account/import-account/private-key.js
@@ -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')]),
]),
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 402b8f03b..c9e4b113c 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -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')]),
]),
diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js
index 7d7d3f462..7782b541c 100644
--- a/ui/app/components/pages/keychains/reveal-seed.js
+++ b/ui/app/components/pages/keychains/reveal-seed.js
@@ -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')),
])
diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js
index a5ea1b89c..423276cf3 100644
--- a/ui/app/components/pages/settings/settings.js
+++ b/ui/app/components/pages/settings/settings.js
@@ -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()
diff --git a/ui/app/components/sender-to-recipient/index.scss b/ui/app/components/sender-to-recipient/index.scss
index 656e30ddf..0ab0413be 100644
--- a/ui/app/components/sender-to-recipient/index.scss
+++ b/ui/app/components/sender-to-recipient/index.scss
@@ -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;
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
index 445a11d8a..61f77224d 100644
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
@@ -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>
diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js
index 2c4ba40bf..a842bcc8b 100644
--- a/ui/app/components/shapeshift-form.js
+++ b/ui/app/components/shapeshift-form.js
@@ -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')]),
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index 2e0102d1a..2bfa350d3 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -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()
diff --git a/ui/app/components/transaction-activity-log/index.js b/ui/app/components/transaction-activity-log/index.js
new file mode 100644
index 000000000..a33da15a3
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-activity-log.container'
diff --git a/ui/app/components/transaction-activity-log/index.scss b/ui/app/components/transaction-activity-log/index.scss
new file mode 100644
index 000000000..2324d44b1
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
new file mode 100644
index 000000000..8687dbbc7
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js
@@ -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)
+ })
+})
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js
new file mode 100644
index 000000000..85d56a6a2
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js
@@ -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 })
+ })
+ })
+})
diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
new file mode 100644
index 000000000..586500408
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js
@@ -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)
+ })
+})
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
new file mode 100644
index 000000000..c4cf57d14
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js
new file mode 100644
index 000000000..4c8b6d971
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js
@@ -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)
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
new file mode 100644
index 000000000..32834ff47
--- /dev/null
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
@@ -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
+ }, [])
+}
diff --git a/ui/app/components/transaction-breakdown/index.js b/ui/app/components/transaction-breakdown/index.js
new file mode 100644
index 000000000..c887f504f
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-breakdown.component'
diff --git a/ui/app/components/transaction-breakdown/index.scss b/ui/app/components/transaction-breakdown/index.scss
new file mode 100644
index 000000000..1bb108943
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/index.scss
@@ -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;
+ }
+ }
+}
diff --git a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js b/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
new file mode 100644
index 000000000..d18cd420c
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js
@@ -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)
+ })
+})
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js
new file mode 100644
index 000000000..557bf75fb
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-breakdown-row.component'
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss
new file mode 100644
index 000000000..8c73be1a6
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js b/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js
new file mode 100644
index 000000000..c19399dbb
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js
@@ -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))
+ })
+})
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
new file mode 100644
index 000000000..c11ff8efa
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
new file mode 100644
index 000000000..bb6075e9f
--- /dev/null
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item-details/index.js b/ui/app/components/transaction-list-item-details/index.js
new file mode 100644
index 000000000..0e878d032
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-list-item-details.component'
diff --git a/ui/app/components/transaction-list-item-details/index.scss b/ui/app/components/transaction-list-item-details/index.scss
new file mode 100644
index 000000000..54cf834cc
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
new file mode 100644
index 000000000..f2bbe8789
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js
@@ -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)
+ })
+})
diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
new file mode 100644
index 000000000..d57ff130a
--- /dev/null
+++ b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss
index 9c53c8960..427686c29 100644
--- a/ui/app/components/transaction-list-item/index.scss
+++ b/ui/app/components/transaction-list-item/index.scss
@@ -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%;
+ }
}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js
index 75b41a477..5564f0883 100644
--- a/ui/app/components/transaction-list-item/transaction-list-item.component.js
+++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js
@@ -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>
)
}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js
index 47644241a..3db9d40ec 100644
--- a/ui/app/components/transaction-list-item/transaction-list-item.container.js
+++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js
@@ -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,
}
}
diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/transaction-list/index.scss
index 0e8db485c..d944ef20e 100644
--- a/ui/app/components/transaction-list/index.scss
+++ b/ui/app/components/transaction-list/index.scss
@@ -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 {
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index 77c7f3a4b..064a6ab55 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -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()
diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js
index 28731ce33..a20f6cc02 100644
--- a/ui/app/constants/common.js
+++ b/ui/app/constants/common.js
@@ -1 +1,3 @@
export const ETH = 'ETH'
+export const GWEI = 'GWEI'
+export const WEI = 'WEI'
diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js
index 38f5f1c50..f271b5683 100644
--- a/ui/app/conversion-util.js
+++ b/ui/app/conversion-util.js
@@ -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),
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index 34565767f..655188a3e 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -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;
}
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
index 03c7e457c..4f97fc662 100644
--- a/ui/app/css/itcss/components/send.scss
+++ b/ui/app/css/itcss/components/send.scss
@@ -837,6 +837,10 @@
line-height: 12px;
color: $red;
}
+
+ &__cancel {
+ margin-right: 10px;
+ }
}
&__gas-modal-card {
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
index d1a4994e4..bcac22500 100644
--- a/ui/app/helpers/confirm-transaction/util.js
+++ b/ui/app/helpers/confirm-transaction/util.js
@@ -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,
})
}
diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js
index 1dec216fa..5204faa1f 100644
--- a/ui/app/helpers/conversions.util.js
+++ b/ui/app/helpers/conversions.util.js
@@ -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,
})
}
diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js
index 54df54aa8..0e1a6ca37 100644
--- a/ui/app/helpers/transactions.util.js
+++ b/ui/app/helpers/transactions.util.js
@@ -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)
+}
diff --git a/ui/i18n-helper.js b/ui/i18n-helper.js
index bc927ee65..c6a7d0bf1 100644
--- a/ui/i18n-helper.js
+++ b/ui/i18n-helper.js
@@ -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
}