1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-26 12:29:06 +01:00

Merge pull request #5182 from MetaMask/tx-activity

Add Transaction Details to the Transaction List view
This commit is contained in:
Alexander Tseung 2018-09-12 20:07:59 -07:00 committed by GitHub
commit 16d6cd5eb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1448 additions and 327 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, {
type: 'default',
large: true,
className: '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: 'primary',
large: true,
className: 'new-account-connect-form__button unlock',
disabled,
onClick: this.props.onUnlockAccount.bind(this, this.props.device),
}, [this.context.t('unlock')]),
])
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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