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:
commit
16d6cd5eb9
@ -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"
|
||||
},
|
||||
|
3
app/images/arrow-popout.svg
Normal file
3
app/images/arrow-popout.svg
Normal 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 |
@ -317,7 +317,7 @@ describe('Using MetaMask with an existing account', function () {
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
assert.equal(txValues.length, 1)
|
||||
assert.equal(await txValues[0].getText(), '-1 ETH')
|
||||
})
|
||||
|
@ -408,7 +408,7 @@ describe('MetaMask', function () {
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
|
||||
}
|
||||
})
|
||||
@ -450,7 +450,7 @@ describe('MetaMask', function () {
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
|
||||
})
|
||||
})
|
||||
@ -528,7 +528,7 @@ describe('MetaMask', function () {
|
||||
await delay(largeDelayMs)
|
||||
|
||||
await findElements(driver, By.css('.transaction-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
|
||||
await txListValue.click()
|
||||
await delay(regularDelayMs)
|
||||
@ -562,7 +562,7 @@ describe('MetaMask', function () {
|
||||
return confirmedTxes.length === 4
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
|
||||
|
||||
// const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
@ -594,7 +594,7 @@ describe('MetaMask', function () {
|
||||
return confirmedTxes.length === 5
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
|
||||
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
|
@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
|
||||
selectState.val('confirm sig requests')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const pendingRequestItem = $.find('.transaction-list-item')
|
||||
const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid')
|
||||
|
||||
if (pendingRequestItem[0]) {
|
||||
pendingRequestItem[0].click()
|
||||
|
@ -32,9 +32,11 @@ async function runTxListItemsTest (assert, done) {
|
||||
const txListItems = await queryAsync($, '.transaction-list-item')
|
||||
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
|
||||
|
||||
const retryTx = txListItems[1]
|
||||
const retryTxLink = await findAsync($(retryTx), '.transaction-list-item__retry')
|
||||
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
|
||||
const retryTxGrid = await findAsync($(txListItems[1]), '.transaction-list-item__grid')
|
||||
retryTxGrid[0].click()
|
||||
const retryTxDetails = await findAsync($(txListItems[1]), '.transaction-list-item-details')
|
||||
const headerButtons = await findAsync($(retryTxDetails[0]), '.transaction-list-item-details__header-button')
|
||||
assert.equal(headerButtons[0].textContent, 'speed up')
|
||||
|
||||
const approvedTx = txListItems[2]
|
||||
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
|
||||
|
@ -6,6 +6,7 @@ const CLASSNAME_DEFAULT = 'btn-default'
|
||||
const CLASSNAME_PRIMARY = 'btn-primary'
|
||||
const CLASSNAME_SECONDARY = 'btn-secondary'
|
||||
const CLASSNAME_CONFIRM = 'btn-confirm'
|
||||
const CLASSNAME_RAISED = 'btn-raised'
|
||||
const CLASSNAME_LARGE = 'btn--large'
|
||||
|
||||
const typeHash = {
|
||||
@ -13,6 +14,7 @@ const typeHash = {
|
||||
primary: CLASSNAME_PRIMARY,
|
||||
secondary: CLASSNAME_SECONDARY,
|
||||
confirm: CLASSNAME_CONFIRM,
|
||||
raised: CLASSNAME_RAISED,
|
||||
}
|
||||
|
||||
export default class Button extends Component {
|
||||
@ -20,7 +22,7 @@ export default class Button extends Component {
|
||||
type: PropTypes.string,
|
||||
large: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -29,6 +31,7 @@ export default class Button extends Component {
|
||||
return (
|
||||
<button
|
||||
className={classnames(
|
||||
'button',
|
||||
typeHash[type],
|
||||
large && CLASSNAME_LARGE,
|
||||
className
|
||||
|
25
ui/app/components/card/card.component.js
Normal file
25
ui/app/components/card/card.component.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/card/index.js
Normal file
1
ui/app/components/card/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './card.component'
|
11
ui/app/components/card/index.scss
Normal file
11
ui/app/components/card/index.scss
Normal 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;
|
||||
}
|
||||
}
|
25
ui/app/components/card/tests/card.component.test.js
Normal file
25
ui/app/components/card/tests/card.component.test.js
Normal 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')
|
||||
})
|
||||
})
|
@ -1,13 +1,18 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ETH } from '../../constants/common'
|
||||
import { ETH, GWEI } from '../../constants/common'
|
||||
|
||||
export default class CurrencyDisplay extends PureComponent {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
displayValue: PropTypes.string,
|
||||
prefix: PropTypes.string,
|
||||
// Used in container
|
||||
currency: PropTypes.oneOf([ETH]),
|
||||
denomination: PropTypes.oneOf([GWEI]),
|
||||
value: PropTypes.string,
|
||||
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
hideLabel: PropTypes.bool,
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -3,13 +3,15 @@ import CurrencyDisplay from './currency-display.component'
|
||||
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { value, numberOfDecimals = 2, currency } = ownProps
|
||||
const { value, numberOfDecimals = 2, currency, denomination, hideLabel } = ownProps
|
||||
const { metamask: { currentCurrency, conversionRate } } = state
|
||||
|
||||
const toCurrency = currency || currentCurrency
|
||||
const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals })
|
||||
const convertedValue = getValueFromWeiHex({
|
||||
value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
|
||||
})
|
||||
const formattedValue = formatCurrency(convertedValue, toCurrency)
|
||||
const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}`
|
||||
const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}`
|
||||
|
||||
return {
|
||||
displayValue,
|
||||
|
@ -51,6 +51,50 @@ describe('CurrencyDisplay container', () => {
|
||||
displayValue: '1.266 ETH',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x1193461d01595930',
|
||||
currency: 'ETH',
|
||||
numberOfDecimals: 3,
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1.266',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x3b9aca00',
|
||||
currency: 'ETH',
|
||||
denomination: 'GWEI',
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x3b9aca00',
|
||||
currency: 'ETH',
|
||||
denomination: 'WEI',
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1000000000',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x3b9aca00',
|
||||
currency: 'ETH',
|
||||
numberOfDecimals: 100,
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1e-9',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
tests.forEach(({ props, result }) => {
|
||||
|
@ -5,6 +5,7 @@ const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../actions')
|
||||
const GasModalCard = require('./gas-modal-card')
|
||||
import Button from '../button'
|
||||
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
|
||||
@ -353,16 +354,16 @@ CustomizeGasModal.prototype.render = function () {
|
||||
}, [this.context.t('revert')]),
|
||||
|
||||
h('div.send-v2__customize-gas__buttons', [
|
||||
h('button.btn-default.send-v2__customize-gas__cancel', {
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
className: 'send-v2__customize-gas__cancel',
|
||||
onClick: this.props.hideModal,
|
||||
style: {
|
||||
marginRight: '10px',
|
||||
},
|
||||
}, [this.context.t('cancel')]),
|
||||
|
||||
h('button.btn-primary.send-v2__customize-gas__save', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
className: 'send-v2__customize-gas__save',
|
||||
onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal),
|
||||
className: error && 'btn-primary--disabled',
|
||||
disabled: error,
|
||||
}, [this.context.t('save')]),
|
||||
]),
|
||||
|
||||
|
@ -1,161 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const BN = ethUtil.BN
|
||||
const extend = require('xtend')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
HexAsDecimalInput.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(HexAsDecimalInput)
|
||||
|
||||
|
||||
inherits(HexAsDecimalInput, Component)
|
||||
function HexAsDecimalInput () {
|
||||
this.state = { invalid: null }
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
/* Hex as Decimal Input
|
||||
*
|
||||
* A component for allowing easy, decimal editing
|
||||
* of a passed in hex string value.
|
||||
*
|
||||
* On change, calls back its `onChange` function parameter
|
||||
* and passes it an updated hex string.
|
||||
*/
|
||||
|
||||
HexAsDecimalInput.prototype.render = function () {
|
||||
const props = this.props
|
||||
const state = this.state
|
||||
|
||||
const { value, onChange, min, max } = props
|
||||
|
||||
const toEth = props.toEth
|
||||
const suffix = props.suffix
|
||||
const decimalValue = decimalize(value, toEth)
|
||||
const style = props.style
|
||||
|
||||
return (
|
||||
h('.flex-column', [
|
||||
h('.flex-row', {
|
||||
style: {
|
||||
alignItems: 'flex-end',
|
||||
lineHeight: '13px',
|
||||
fontFamily: 'Montserrat Light',
|
||||
textRendering: 'geometricPrecision',
|
||||
},
|
||||
}, [
|
||||
h('input.hex-input', {
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: min,
|
||||
max: max,
|
||||
style: extend({
|
||||
display: 'block',
|
||||
textAlign: 'right',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #bdbdbd',
|
||||
|
||||
}, style),
|
||||
value: parseInt(decimalValue),
|
||||
onBlur: (event) => {
|
||||
this.updateValidity(event)
|
||||
},
|
||||
onChange: (event) => {
|
||||
this.updateValidity(event)
|
||||
const hexString = (event.target.value === '') ? '' : hexify(event.target.value)
|
||||
onChange(hexString)
|
||||
},
|
||||
onInvalid: (event) => {
|
||||
const msg = this.constructWarning()
|
||||
if (msg === state.invalid) {
|
||||
return
|
||||
}
|
||||
this.setState({ invalid: msg })
|
||||
event.preventDefault()
|
||||
return false
|
||||
},
|
||||
}),
|
||||
h('div', {
|
||||
style: {
|
||||
color: ' #AEAEAE',
|
||||
fontSize: '12px',
|
||||
marginLeft: '5px',
|
||||
marginRight: '6px',
|
||||
width: '20px',
|
||||
},
|
||||
}, suffix),
|
||||
]),
|
||||
|
||||
state.invalid ? h('span.error', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
right: '0px',
|
||||
textAlign: 'right',
|
||||
transform: 'translateY(26px)',
|
||||
padding: '3px',
|
||||
background: 'rgba(255,255,255,0.85)',
|
||||
zIndex: '1',
|
||||
textTransform: 'capitalize',
|
||||
border: '2px solid #E20202',
|
||||
},
|
||||
}, state.invalid) : null,
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
HexAsDecimalInput.prototype.setValid = function (message) {
|
||||
this.setState({ invalid: null })
|
||||
}
|
||||
|
||||
HexAsDecimalInput.prototype.updateValidity = function (event) {
|
||||
const target = event.target
|
||||
const value = this.props.value
|
||||
const newValue = target.value
|
||||
|
||||
if (value === newValue) {
|
||||
return
|
||||
}
|
||||
|
||||
const valid = target.checkValidity()
|
||||
if (valid) {
|
||||
this.setState({ invalid: null })
|
||||
}
|
||||
}
|
||||
|
||||
HexAsDecimalInput.prototype.constructWarning = function () {
|
||||
const { name, min, max } = this.props
|
||||
let message = name ? name + ' ' : ''
|
||||
|
||||
if (min && max) {
|
||||
message += this.context.t('betweenMinAndMax', [min, max])
|
||||
} else if (min) {
|
||||
message += this.context.t('greaterThanMin', [min])
|
||||
} else if (max) {
|
||||
message += this.context.t('lessThanMax', [max])
|
||||
} else {
|
||||
message += this.context.t('invalidInput')
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
function hexify (decimalString) {
|
||||
const hexBN = new BN(parseInt(decimalString), 10)
|
||||
return '0x' + hexBN.toString('hex')
|
||||
}
|
||||
|
||||
function decimalize (input, toEth) {
|
||||
if (input === '') {
|
||||
return ''
|
||||
} else {
|
||||
const strippedInput = ethUtil.stripHexPrefix(input)
|
||||
const inputBN = new BN(strippedInput, 'hex')
|
||||
return inputBN.toString(10)
|
||||
}
|
||||
}
|
21
ui/app/components/hex-to-decimal/hex-to-decimal.component.js
Normal file
21
ui/app/components/hex-to-decimal/hex-to-decimal.component.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/hex-to-decimal/index.js
Normal file
1
ui/app/components/hex-to-decimal/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './hex-to-decimal.component'
|
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import HexToDecimal from '../hex-to-decimal.component'
|
||||
|
||||
describe('HexToDecimal Component', () => {
|
||||
it('should render a prefixed hex as a decimal with a className', () => {
|
||||
const wrapper = shallow(<HexToDecimal
|
||||
value="0x3039"
|
||||
className="hex-to-decimal"
|
||||
/>)
|
||||
|
||||
assert.ok(wrapper.hasClass('hex-to-decimal'))
|
||||
assert.equal(wrapper.text(), '12345')
|
||||
})
|
||||
|
||||
it('should render an unprefixed hex as a decimal with a className', () => {
|
||||
const wrapper = shallow(<HexToDecimal
|
||||
value="1A85"
|
||||
className="hex-to-decimal"
|
||||
/>)
|
||||
|
||||
assert.ok(wrapper.hasClass('hex-to-decimal'))
|
||||
assert.equal(wrapper.text(), '6789')
|
||||
})
|
||||
})
|
@ -56,6 +56,7 @@ IdenticonComponent.prototype.render = function () {
|
||||
})
|
||||
} else {
|
||||
return h('img.balance-icon', {
|
||||
className,
|
||||
src: './images/eth_logo.svg',
|
||||
style: {
|
||||
...style,
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
@import './button-group/index';
|
||||
|
||||
@import './card/index';
|
||||
|
||||
@import './confirm-page-container/index';
|
||||
|
||||
@import './export-text-container/index';
|
||||
@ -24,6 +26,10 @@
|
||||
|
||||
@import './tabs/index';
|
||||
|
||||
@import './transaction-activity-log/index';
|
||||
|
||||
@import './transaction-breakdown/index';
|
||||
|
||||
@import './transaction-view/index';
|
||||
|
||||
@import './transaction-view-balance/index';
|
||||
@ -32,6 +38,8 @@
|
||||
|
||||
@import './transaction-list-item/index';
|
||||
|
||||
@import './transaction-list-item-details/index';
|
||||
|
||||
@import './transaction-status/index';
|
||||
|
||||
@import './app-header/index';
|
||||
|
@ -10,6 +10,8 @@ const genAccountLink = require('../../../lib/account-link.js')
|
||||
const QrView = require('../qr-code')
|
||||
const EditableLabel = require('../editable-label')
|
||||
|
||||
import Button from '../button'
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
network: state.metamask.network,
|
||||
@ -80,12 +82,17 @@ AccountDetailsModal.prototype.render = function () {
|
||||
|
||||
h('div.account-modal-divider'),
|
||||
|
||||
h('button.btn-primary.account-modal__button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
className: 'account-modal__button',
|
||||
onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }),
|
||||
}, this.context.t('etherscanView')),
|
||||
|
||||
// Holding on redesign for Export Private Key functionality
|
||||
exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', {
|
||||
|
||||
exportPrivateKeyFeatureEnabled ? h(Button, {
|
||||
type: 'primary',
|
||||
className: 'account-modal__button',
|
||||
onClick: () => showExportPrivateKeyModal(),
|
||||
}, this.context.t('exportPrivateKey')) : null,
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import GasModalCard from '../../customize-gas-modal/gas-modal-card'
|
||||
import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
|
||||
import Button from '../../button'
|
||||
|
||||
import {
|
||||
getDecimalGasLimit,
|
||||
@ -116,21 +117,23 @@ export default class CustomizeGas extends Component {
|
||||
{ t('revert') }
|
||||
</div>
|
||||
<div className="customize-gas__buttons">
|
||||
<button
|
||||
className="btn-default customize-gas__cancel"
|
||||
<Button
|
||||
type="default"
|
||||
className="customize-gas__cancel"
|
||||
onClick={() => hideModal()}
|
||||
style={{ marginRight: '10px' }}
|
||||
>
|
||||
{ t('cancel') }
|
||||
</button>
|
||||
<button
|
||||
className="btn-primary customize-gas__save"
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
className="customize-gas__save"
|
||||
onClick={() => this.handleSave()}
|
||||
style={{ marginRight: '10px' }}
|
||||
disabled={!valid}
|
||||
>
|
||||
{ t('save') }
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,6 +7,8 @@ const actions = require('../../actions')
|
||||
const { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util')
|
||||
const ShapeshiftForm = require('../shapeshift-form')
|
||||
|
||||
import Button from '../button'
|
||||
|
||||
let DIRECT_DEPOSIT_ROW_TITLE
|
||||
let DIRECT_DEPOSIT_ROW_TEXT
|
||||
let COINBASE_ROW_TITLE
|
||||
@ -109,7 +111,10 @@ DepositEtherModal.prototype.renderRow = function ({
|
||||
]),
|
||||
|
||||
!hideButton && h('div.deposit-ether-modal__buy-row__button', [
|
||||
h('button.btn-primary.btn--large.deposit-ether-modal__deposit-button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
className: 'deposit-ether-modal__deposit-button',
|
||||
large: true,
|
||||
onClick: onButtonClick,
|
||||
}, [buttonLabel]),
|
||||
]),
|
||||
|
@ -11,6 +11,7 @@ const { getSelectedIdentity } = require('../../selectors')
|
||||
const ReadOnlyInput = require('../readonly-input')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const { checksumAddress } = require('../../util')
|
||||
import Button from '../button'
|
||||
|
||||
function mapStateToPropsFactory () {
|
||||
let selectedIdentity = null
|
||||
@ -97,24 +98,31 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) {
|
||||
})
|
||||
}
|
||||
|
||||
ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, label) {
|
||||
return h('button', {
|
||||
className,
|
||||
onClick,
|
||||
}, label)
|
||||
}
|
||||
|
||||
ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) {
|
||||
return h('div.export-private-key-buttons', {}, [
|
||||
!privateKey && this.renderButton(
|
||||
'btn-default btn--large export-private-key__button export-private-key__button--cancel',
|
||||
() => hideModal(),
|
||||
'Cancel'
|
||||
),
|
||||
!privateKey && h(Button, {
|
||||
type: 'default',
|
||||
large: true,
|
||||
className: 'export-private-key__button export-private-key__button--cancel',
|
||||
onClick: () => hideModal(),
|
||||
}, this.context.t('cancel')),
|
||||
|
||||
(privateKey
|
||||
? this.renderButton('btn-primary btn--large export-private-key__button', () => hideModal(), this.context.t('done'))
|
||||
: this.renderButton('btn-primary btn--large export-private-key__button', () => this.exportAccountAndGetPrivateKey(this.state.password, address), this.context.t('confirm'))
|
||||
? (
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'export-private-key__button',
|
||||
onClick: () => hideModal(),
|
||||
}, this.context.t('done'))
|
||||
) : (
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'export-private-key__button',
|
||||
onClick: () => this.exportAccountAndGetPrivateKey(this.state.password, address),
|
||||
}, this.context.t('confirm'))
|
||||
)
|
||||
),
|
||||
|
||||
])
|
||||
|
@ -3,6 +3,7 @@ const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const genAccountLink = require('../../../../../lib/account-link.js')
|
||||
const Select = require('react-select').default
|
||||
import Button from '../../../button'
|
||||
|
||||
class AccountList extends Component {
|
||||
constructor (props, context) {
|
||||
@ -143,22 +144,20 @@ class AccountList extends Component {
|
||||
}
|
||||
|
||||
return h('div.new-account-connect-form__buttons', {}, [
|
||||
h(
|
||||
'button.btn-default.btn--large.new-account-connect-form__button',
|
||||
{
|
||||
onClick: this.props.onCancel.bind(this),
|
||||
},
|
||||
[this.context.t('cancel')]
|
||||
),
|
||||
h(Button, {
|
||||
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')]),
|
||||
])
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
const { Component } = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
import Button from '../../../button'
|
||||
|
||||
class ConnectScreen extends Component {
|
||||
constructor (props, context) {
|
||||
@ -60,13 +61,13 @@ class ConnectScreen extends Component {
|
||||
h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
|
||||
h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
|
||||
]),
|
||||
h(
|
||||
'button.btn-primary.btn--large',
|
||||
{
|
||||
onClick: () => global.platform.openWindow({
|
||||
url: 'https://google.com/chrome',
|
||||
}),
|
||||
},
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
onClick: () => global.platform.openWindow({
|
||||
url: 'https://google.com/chrome',
|
||||
}),
|
||||
},
|
||||
this.context.t('downloadGoogleChrome')
|
||||
),
|
||||
])
|
||||
|
@ -8,6 +8,7 @@ const actions = require('../../../../actions')
|
||||
const FileInput = require('react-simple-file-input').default
|
||||
const { DEFAULT_ROUTE } = require('../../../../routes')
|
||||
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
|
||||
import Button from '../../../button'
|
||||
|
||||
class JsonImportSubview extends Component {
|
||||
constructor (props) {
|
||||
@ -51,17 +52,19 @@ class JsonImportSubview extends Component {
|
||||
|
||||
h('div.new-account-create-form__buttons', {}, [
|
||||
|
||||
h('button.btn-default.new-account-create-form__button', {
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
large: true,
|
||||
className: 'new-account-create-form__button',
|
||||
onClick: () => this.props.history.push(DEFAULT_ROUTE),
|
||||
}, [
|
||||
this.context.t('cancel'),
|
||||
]),
|
||||
}, [this.context.t('cancel')]),
|
||||
|
||||
h('button.btn-primary.new-account-create-form__button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'new-account-create-form__button',
|
||||
onClick: () => this.createNewKeychain(),
|
||||
}, [
|
||||
this.context.t('import'),
|
||||
]),
|
||||
}, [this.context.t('import')]),
|
||||
|
||||
]),
|
||||
|
||||
|
@ -7,6 +7,7 @@ const PropTypes = require('prop-types')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../../../actions')
|
||||
const { DEFAULT_ROUTE } = require('../../../../routes')
|
||||
import Button from '../../../button'
|
||||
|
||||
PrivateKeyImportView.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
@ -61,20 +62,22 @@ PrivateKeyImportView.prototype.render = function () {
|
||||
|
||||
h('div.new-account-import-form__buttons', {}, [
|
||||
|
||||
h('button.btn-default.btn--large.new-account-create-form__button', {
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
large: true,
|
||||
className: 'new-account-create-form__button',
|
||||
onClick: () => {
|
||||
displayWarning(null)
|
||||
this.props.history.push(DEFAULT_ROUTE)
|
||||
},
|
||||
}, [
|
||||
this.context.t('cancel'),
|
||||
]),
|
||||
}, [this.context.t('cancel')]),
|
||||
|
||||
h('button.btn-primary.btn--large.new-account-create-form__button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'new-account-create-form__button',
|
||||
onClick: () => this.createNewKeychain(),
|
||||
}, [
|
||||
this.context.t('import'),
|
||||
]),
|
||||
}, [this.context.t('import')]),
|
||||
|
||||
]),
|
||||
|
||||
|
@ -4,6 +4,7 @@ const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../../actions')
|
||||
const { DEFAULT_ROUTE } = require('../../../routes')
|
||||
import Button from '../../button'
|
||||
|
||||
class NewAccountCreateForm extends Component {
|
||||
constructor (props, context) {
|
||||
@ -38,20 +39,22 @@ class NewAccountCreateForm extends Component {
|
||||
|
||||
h('div.new-account-create-form__buttons', {}, [
|
||||
|
||||
h('button.btn-default.btn--large.new-account-create-form__button', {
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
large: true,
|
||||
className: 'new-account-create-form__button',
|
||||
onClick: () => history.push(DEFAULT_ROUTE),
|
||||
}, [
|
||||
this.context.t('cancel'),
|
||||
]),
|
||||
}, [this.context.t('cancel')]),
|
||||
|
||||
h('button.btn-primary.btn--large.new-account-create-form__button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className:'new-account-create-form__button',
|
||||
onClick: () => {
|
||||
createAccount(newAccountName || defaultAccountName)
|
||||
.then(() => history.push(DEFAULT_ROUTE))
|
||||
},
|
||||
}, [
|
||||
this.context.t('create'),
|
||||
]),
|
||||
}, [this.context.t('create')]),
|
||||
|
||||
]),
|
||||
|
||||
|
@ -8,6 +8,8 @@ const { requestRevealSeedWords } = require('../../../actions')
|
||||
const { DEFAULT_ROUTE } = require('../../../routes')
|
||||
const ExportTextContainer = require('../../export-text-container')
|
||||
|
||||
import Button from '../../button'
|
||||
|
||||
const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
|
||||
const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
|
||||
|
||||
@ -106,10 +108,16 @@ class RevealSeedPage extends Component {
|
||||
renderPasswordPromptFooter () {
|
||||
return (
|
||||
h('.page-container__footer', [
|
||||
h('button.btn-default.btn--large.page-container__footer-button', {
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
large: true,
|
||||
className: 'page-container__footer-button',
|
||||
onClick: () => this.props.history.push(DEFAULT_ROUTE),
|
||||
}, this.context.t('cancel')),
|
||||
h('button.btn-primary.btn--large.page-container__footer-button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'page-container__footer-button',
|
||||
onClick: event => this.handleSubmit(event),
|
||||
disabled: this.state.password === '',
|
||||
}, this.context.t('next')),
|
||||
@ -120,7 +128,10 @@ class RevealSeedPage extends Component {
|
||||
renderRevealSeedFooter () {
|
||||
return (
|
||||
h('.page-container__footer', [
|
||||
h('button.btn-default.btn--large.page-container__footer-button', {
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
large: true,
|
||||
className: 'page-container__footer-button',
|
||||
onClick: () => this.props.history.push(DEFAULT_ROUTE),
|
||||
}, this.context.t('close')),
|
||||
])
|
||||
|
@ -13,6 +13,8 @@ const ToggleButton = require('react-toggle-button')
|
||||
const { REVEAL_SEED_ROUTE } = require('../../../routes')
|
||||
const locales = require('../../../../../app/_locales/index.json')
|
||||
|
||||
import Button from '../../button'
|
||||
|
||||
const getInfuraCurrencyOptions = () => {
|
||||
const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
|
||||
return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
|
||||
@ -241,7 +243,10 @@ class Settings extends Component {
|
||||
]),
|
||||
h('div.settings__content-item', [
|
||||
h('div.settings__content-item-col', [
|
||||
h('button.btn-primary.btn--large.settings__button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'settings__button',
|
||||
onClick (event) {
|
||||
window.logStateString((err, result) => {
|
||||
if (err) {
|
||||
@ -266,7 +271,10 @@ class Settings extends Component {
|
||||
h('div.settings__content-item', this.context.t('revealSeedWords')),
|
||||
h('div.settings__content-item', [
|
||||
h('div.settings__content-item-col', [
|
||||
h('button.btn-primary.btn--large.settings__button--red', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'settings__button--red',
|
||||
onClick: event => {
|
||||
event.preventDefault()
|
||||
history.push(REVEAL_SEED_ROUTE)
|
||||
@ -286,7 +294,10 @@ class Settings extends Component {
|
||||
h('div.settings__content-item', this.context.t('useOldUI')),
|
||||
h('div.settings__content-item', [
|
||||
h('div.settings__content-item-col', [
|
||||
h('button.btn-primary.btn--large.settings__button--orange', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'settings__button--orange',
|
||||
onClick (event) {
|
||||
event.preventDefault()
|
||||
setFeatureFlagToBeta()
|
||||
@ -305,7 +316,10 @@ class Settings extends Component {
|
||||
h('div.settings__content-item', this.context.t('resetAccount')),
|
||||
h('div.settings__content-item', [
|
||||
h('div.settings__content-item-col', [
|
||||
h('button.btn-primary.btn--large.settings__button--orange', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: 'settings__button--orange',
|
||||
onClick (event) {
|
||||
event.preventDefault()
|
||||
showResetAccountConfirmationModal()
|
||||
|
@ -80,13 +80,13 @@
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
padding: 8px;
|
||||
|
||||
.sender-to-recipient {
|
||||
&__party {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
|
||||
@ -111,7 +111,6 @@
|
||||
}
|
||||
|
||||
&__arrow-container {
|
||||
padding: 0 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -115,7 +115,7 @@ export default class SenderToRecipient extends PureComponent {
|
||||
renderRecipientWithoutAddress () {
|
||||
return (
|
||||
<div className="sender-to-recipient__party sender-to-recipient__party--recipient">
|
||||
<i className="fa fa-file-text-o" />
|
||||
{ !this.props.addressOnly && <i className="fa fa-file-text-o" /> }
|
||||
<div className="sender-to-recipient__name">
|
||||
{ this.context.t('newContract') }
|
||||
</div>
|
||||
|
@ -9,6 +9,8 @@ const { shapeShiftSubview, pairUpdate, buyWithShapeShift } = require('../actions
|
||||
const { isValidAddress } = require('../util')
|
||||
const SimpleDropdown = require('./dropdowns/simple-dropdown')
|
||||
|
||||
import Button from './button'
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const {
|
||||
coinOptions,
|
||||
@ -242,8 +244,10 @@ ShapeshiftForm.prototype.render = function () {
|
||||
|
||||
]),
|
||||
|
||||
!depositAddress && h('button.btn-primary.btn--large.shapeshift-form__shapeshift-buy-btn', {
|
||||
className: btnClass,
|
||||
!depositAddress && h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
className: `${btnClass} shapeshift-form__shapeshift-buy-btn`,
|
||||
disabled: !token,
|
||||
onClick: () => this.onBuyWithShapeShift(),
|
||||
}, [this.context.t('buy')]),
|
||||
|
@ -23,6 +23,7 @@ const {
|
||||
} = require('../selectors.js')
|
||||
|
||||
import { clearConfirmTransaction } from '../ducks/confirm-transaction.duck'
|
||||
import Button from './button'
|
||||
|
||||
const { DEFAULT_ROUTE } = require('../routes')
|
||||
|
||||
@ -248,7 +249,10 @@ SignatureRequest.prototype.renderFooter = function () {
|
||||
}
|
||||
|
||||
return h('div.request-signature__footer', [
|
||||
h('button.btn-default.btn--large.request-signature__footer__cancel-button', {
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
large: true,
|
||||
className: 'request-signature__footer__cancel-button',
|
||||
onClick: event => {
|
||||
cancel(event).then(() => {
|
||||
this.props.clearConfirmTransaction()
|
||||
@ -256,7 +260,9 @@ SignatureRequest.prototype.renderFooter = function () {
|
||||
})
|
||||
},
|
||||
}, this.context.t('cancel')),
|
||||
h('button.btn-primary.btn--large', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
large: true,
|
||||
onClick: event => {
|
||||
sign(event).then(() => {
|
||||
this.props.clearConfirmTransaction()
|
||||
|
1
ui/app/components/transaction-activity-log/index.js
Normal file
1
ui/app/components/transaction-activity-log/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-activity-log.container'
|
63
ui/app/components/transaction-activity-log/index.scss
Normal file
63
ui/app/components/transaction-activity-log/index.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import TransactionActivityLog from '../transaction-activity-log.component'
|
||||
import Card from '../../card'
|
||||
|
||||
describe('TransactionActivityLog Component', () => {
|
||||
it('should render properly', () => {
|
||||
const transaction = {
|
||||
history: [],
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionActivityLog
|
||||
transaction={transaction}
|
||||
className="test-class"
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-activity-log'))
|
||||
assert.ok(wrapper.hasClass('test-class'))
|
||||
assert.equal(wrapper.find(Card).length, 1)
|
||||
})
|
||||
})
|
@ -0,0 +1,27 @@
|
||||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
|
||||
let mapStateToProps
|
||||
|
||||
proxyquire('../transaction-activity-log.container.js', {
|
||||
'react-redux': {
|
||||
connect: ms => {
|
||||
mapStateToProps = ms
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe('TransactionActivityLog container', () => {
|
||||
describe('mapStateToProps()', () => {
|
||||
it('should return the correct props', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
conversionRate: 280.45,
|
||||
},
|
||||
}
|
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { conversionRate: 280.45 })
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,208 @@
|
||||
import assert from 'assert'
|
||||
import { getActivities } from '../transaction-activity-log.util'
|
||||
|
||||
describe('getActivities', () => {
|
||||
it('should return no activities for an empty history', () => {
|
||||
const transaction = {
|
||||
history: [],
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
assert.deepEqual(getActivities(transaction), [])
|
||||
})
|
||||
|
||||
it('should return activities for a transaction\'s history', () => {
|
||||
const transaction = {
|
||||
history: [
|
||||
{
|
||||
id: 5559712943815343,
|
||||
loadingDefaults: true,
|
||||
metamaskNetworkId: '3',
|
||||
status: 'unapproved',
|
||||
time: 1535507561452,
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/loadingDefaults',
|
||||
timestamp: 1535507561515,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/gasPriceSpecified',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/gasLimitSpecified',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/estimatedGas',
|
||||
value: '0x5208',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: '#newUnapprovedTransaction - adding the origin',
|
||||
op: 'add',
|
||||
path: '/origin',
|
||||
timestamp: 1535507561516,
|
||||
value: 'MetaMask',
|
||||
},
|
||||
[],
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'confTx: user approved transaction',
|
||||
op: 'replace',
|
||||
path: '/txParams/gasPrice',
|
||||
timestamp: 1535664571504,
|
||||
value: '0x77359400',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'txStateManager: setting status to approved',
|
||||
op: 'replace',
|
||||
path: '/status',
|
||||
timestamp: 1535507564302,
|
||||
value: 'approved',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'transactions#approveTransaction',
|
||||
op: 'add',
|
||||
path: '/txParams/nonce',
|
||||
timestamp: 1535507564439,
|
||||
value: '0xa4',
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/nonceDetails',
|
||||
value: {
|
||||
local: {},
|
||||
network: {},
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'transactions#publishTransaction',
|
||||
op: 'replace',
|
||||
path: '/status',
|
||||
timestamp: 1535507564518,
|
||||
value: 'signed',
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/rawTx',
|
||||
value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf',
|
||||
},
|
||||
],
|
||||
[],
|
||||
[
|
||||
{
|
||||
note: 'transactions#setTxHash',
|
||||
op: 'add',
|
||||
path: '/hash',
|
||||
timestamp: 1535507564658,
|
||||
value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'txStateManager - add submitted time stamp',
|
||||
op: 'add',
|
||||
path: '/submittedTime',
|
||||
timestamp: 1535507564660,
|
||||
value: 1535507564660,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'txStateManager: setting status to submitted',
|
||||
op: 'replace',
|
||||
path: '/status',
|
||||
timestamp: 1535507564665,
|
||||
value: 'submitted',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'transactions/pending-tx-tracker#event: tx:block-update',
|
||||
op: 'add',
|
||||
path: '/firstRetryBlockNumber',
|
||||
timestamp: 1535507575476,
|
||||
value: '0x3bf624',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
note: 'txStateManager: setting status to confirmed',
|
||||
op: 'replace',
|
||||
path: '/status',
|
||||
timestamp: 1535507615993,
|
||||
value: 'confirmed',
|
||||
},
|
||||
],
|
||||
],
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
const expectedResult = [
|
||||
{
|
||||
'eventKey': 'transactionCreated',
|
||||
'timestamp': 1535507561452,
|
||||
'value': '0x2386f26fc10000',
|
||||
},
|
||||
{
|
||||
'eventKey': 'transactionUpdatedGas',
|
||||
'timestamp': 1535664571504,
|
||||
'value': '0x77359400',
|
||||
},
|
||||
{
|
||||
'eventKey': 'transactionSubmitted',
|
||||
'timestamp': 1535507564665,
|
||||
'value': undefined,
|
||||
},
|
||||
{
|
||||
'eventKey': 'transactionConfirmed',
|
||||
'timestamp': 1535507615993,
|
||||
'value': undefined,
|
||||
},
|
||||
]
|
||||
|
||||
assert.deepEqual(getActivities(transaction), expectedResult)
|
||||
})
|
||||
})
|
@ -0,0 +1,91 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { getActivities } from './transaction-activity-log.util'
|
||||
import Card from '../card'
|
||||
import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util'
|
||||
import { ETH } from '../../constants/common'
|
||||
import { formatDate } from '../../util'
|
||||
|
||||
export default class TransactionActivityLog extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
transaction: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
conversionRate: PropTypes.number,
|
||||
}
|
||||
|
||||
state = {
|
||||
activities: [],
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.setActivites()
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { transaction: { history: prevHistory = [] } = {} } = prevProps
|
||||
const { transaction: { history = [] } = {} } = this.props
|
||||
|
||||
if (prevHistory.length !== history.length) {
|
||||
this.setActivites()
|
||||
}
|
||||
}
|
||||
|
||||
setActivites () {
|
||||
const activities = getActivities(this.props.transaction)
|
||||
this.setState({ activities })
|
||||
}
|
||||
|
||||
renderActivity (activity, index) {
|
||||
const { conversionRate } = this.props
|
||||
const { eventKey, value, timestamp } = activity
|
||||
const ethValue = index === 0
|
||||
? `${getValueFromWeiHex({
|
||||
value,
|
||||
toCurrency: ETH,
|
||||
conversionRate,
|
||||
numberOfDecimals: 6,
|
||||
})} ${ETH}`
|
||||
: getEthConversionFromWeiHex({ value, toCurrency: ETH, conversionRate })
|
||||
const formattedTimestamp = formatDate(timestamp)
|
||||
const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp])
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="transaction-activity-log__activity"
|
||||
>
|
||||
<div className="transaction-activity-log__activity-icon" />
|
||||
<div
|
||||
className="transaction-activity-log__activity-text"
|
||||
title={activityText}
|
||||
>
|
||||
{ activityText }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { className } = this.props
|
||||
const { activities } = this.state
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-activity-log', className)}>
|
||||
<Card
|
||||
title={t('activityLog')}
|
||||
className="transaction-activity-log__card"
|
||||
>
|
||||
<div className="transaction-activity-log__activities-container">
|
||||
{ activities.map((activity, index) => this.renderActivity(activity, index)) }
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux'
|
||||
import TransactionActivityLog from './transaction-activity-log.component'
|
||||
import { conversionRateSelector } from '../../selectors'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
conversionRate: conversionRateSelector(state),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TransactionActivityLog)
|
@ -0,0 +1,82 @@
|
||||
// path constants
|
||||
const STATUS_PATH = '/status'
|
||||
const GAS_PRICE_PATH = '/txParams/gasPrice'
|
||||
|
||||
// status constants
|
||||
const UNAPPROVED_STATUS = 'unapproved'
|
||||
const SUBMITTED_STATUS = 'submitted'
|
||||
const CONFIRMED_STATUS = 'confirmed'
|
||||
const DROPPED_STATUS = 'dropped'
|
||||
|
||||
// op constants
|
||||
const REPLACE_OP = 'replace'
|
||||
|
||||
// event constants
|
||||
const TRANSACTION_CREATED_EVENT = 'transactionCreated'
|
||||
const TRANSACTION_UPDATED_GAS_EVENT = 'transactionUpdatedGas'
|
||||
const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted'
|
||||
const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed'
|
||||
const TRANSACTION_DROPPED_EVENT = 'transactionDropped'
|
||||
const TRANSACTION_UPDATED_EVENT = 'transactionUpdated'
|
||||
|
||||
const eventPathsHash = {
|
||||
[STATUS_PATH]: true,
|
||||
[GAS_PRICE_PATH]: true,
|
||||
}
|
||||
|
||||
const statusHash = {
|
||||
[SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT,
|
||||
[CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT,
|
||||
[DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
|
||||
}
|
||||
|
||||
function eventCreator (eventKey, timestamp, value) {
|
||||
return {
|
||||
eventKey,
|
||||
timestamp,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
export function getActivities (transaction) {
|
||||
const { history = [] } = transaction
|
||||
|
||||
return history.reduce((acc, base) => {
|
||||
// First history item should be transaction creation
|
||||
if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) {
|
||||
const { time, txParams: { value } = {} } = base
|
||||
return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value))
|
||||
} else if (Array.isArray(base)) {
|
||||
const events = []
|
||||
|
||||
base.forEach(entry => {
|
||||
const { op, path, value, timestamp } = entry
|
||||
|
||||
if (path in eventPathsHash && op === REPLACE_OP) {
|
||||
switch (path) {
|
||||
case STATUS_PATH: {
|
||||
if (value in statusHash) {
|
||||
events.push(eventCreator(statusHash[value], timestamp))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case GAS_PRICE_PATH: {
|
||||
events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value))
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return acc.concat(events)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
1
ui/app/components/transaction-breakdown/index.js
Normal file
1
ui/app/components/transaction-breakdown/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-breakdown.component'
|
23
ui/app/components/transaction-breakdown/index.scss
Normal file
23
ui/app/components/transaction-breakdown/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import TransactionBreakdown from '../transaction-breakdown.component'
|
||||
import TransactionBreakdownRow from '../transaction-breakdown-row'
|
||||
import Card from '../../card'
|
||||
|
||||
describe('TransactionBreakdown Component', () => {
|
||||
it('should render properly', () => {
|
||||
const transaction = {
|
||||
history: [],
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionBreakdown
|
||||
transaction={transaction}
|
||||
className="test-class"
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-breakdown'))
|
||||
assert.ok(wrapper.hasClass('test-class'))
|
||||
assert.equal(wrapper.find(Card).length, 1)
|
||||
assert.equal(wrapper.find(Card).find(TransactionBreakdownRow).length, 4)
|
||||
})
|
||||
})
|
@ -0,0 +1 @@
|
||||
export { default } from './transaction-breakdown-row.component'
|
@ -0,0 +1,19 @@
|
||||
.transaction-breakdown-row {
|
||||
font-size: .75rem;
|
||||
color: $scorpion;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
}
|
||||
|
||||
&__title {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import TransactionBreakdownRow from '../transaction-breakdown-row.component'
|
||||
import Button from '../../../button'
|
||||
|
||||
describe('TransactionBreakdownRow Component', () => {
|
||||
it('should render text properly', () => {
|
||||
const wrapper = shallow(
|
||||
<TransactionBreakdownRow
|
||||
title="test"
|
||||
className="test-class"
|
||||
>
|
||||
Test
|
||||
</TransactionBreakdownRow>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-breakdown-row'))
|
||||
assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test')
|
||||
assert.equal(wrapper.find('.transaction-breakdown-row__value').text(), 'Test')
|
||||
})
|
||||
|
||||
it('should render components properly', () => {
|
||||
const wrapper = shallow(
|
||||
<TransactionBreakdownRow
|
||||
title="test"
|
||||
className="test-class"
|
||||
>
|
||||
<Button onClick={() => {}} >Button</Button>
|
||||
</TransactionBreakdownRow>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-breakdown-row'))
|
||||
assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test')
|
||||
assert.ok(wrapper.find('.transaction-breakdown-row__value').find(Button))
|
||||
})
|
||||
})
|
@ -0,0 +1,26 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class TransactionBreakdownRow extends PureComponent {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, children, className } = this.props
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-breakdown-row', className)}>
|
||||
<div className="transaction-breakdown-row__title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="transaction-breakdown-row__value">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import TransactionBreakdownRow from './transaction-breakdown-row'
|
||||
import Card from '../card'
|
||||
import CurrencyDisplay from '../currency-display'
|
||||
import HexToDecimal from '../hex-to-decimal'
|
||||
import { ETH, GWEI } from '../../constants/common'
|
||||
import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
|
||||
import { sumHexes } from '../../helpers/transactions.util'
|
||||
|
||||
export default class TransactionBreakdown extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
transaction: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
transaction: {},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { transaction, className } = this.props
|
||||
const { txParams: { gas, gasPrice, value } = {} } = transaction
|
||||
const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice })
|
||||
const totalInHex = sumHexes(hexGasTotal, value)
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-breakdown', className)}>
|
||||
<Card
|
||||
title={t('transaction')}
|
||||
className="transaction-breakdown__card"
|
||||
>
|
||||
<TransactionBreakdownRow title={t('amount')}>
|
||||
<CurrencyDisplay
|
||||
className="transaction-breakdown__value"
|
||||
currency={ETH}
|
||||
value={value}
|
||||
/>
|
||||
</TransactionBreakdownRow>
|
||||
<TransactionBreakdownRow
|
||||
title={`${t('gasLimit')} (${t('units')})`}
|
||||
className="transaction-breakdown__row-title"
|
||||
>
|
||||
<HexToDecimal
|
||||
className="transaction-breakdown__value"
|
||||
value={gas}
|
||||
/>
|
||||
</TransactionBreakdownRow>
|
||||
<TransactionBreakdownRow title={t('gasPrice')}>
|
||||
<CurrencyDisplay
|
||||
className="transaction-breakdown__value"
|
||||
currency={ETH}
|
||||
denomination={GWEI}
|
||||
value={gasPrice}
|
||||
hideLabel
|
||||
/>
|
||||
</TransactionBreakdownRow>
|
||||
<TransactionBreakdownRow title={t('total')}>
|
||||
<div>
|
||||
<CurrencyDisplay
|
||||
className="transaction-breakdown__value transaction-breakdown__value--eth-total"
|
||||
currency={ETH}
|
||||
value={totalInHex}
|
||||
numberOfDecimals={6}
|
||||
/>
|
||||
<CurrencyDisplay
|
||||
className="transaction-breakdown__value"
|
||||
value={totalInHex}
|
||||
/>
|
||||
</div>
|
||||
</TransactionBreakdownRow>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/transaction-list-item-details/index.js
Normal file
1
ui/app/components/transaction-list-item-details/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-list-item-details.component'
|
49
ui/app/components/transaction-list-item-details/index.scss
Normal file
49
ui/app/components/transaction-list-item-details/index.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import TransactionListItemDetails from '../transaction-list-item-details.component'
|
||||
import Button from '../../button'
|
||||
import SenderToRecipient from '../../sender-to-recipient'
|
||||
import TransactionBreakdown from '../../transaction-breakdown'
|
||||
import TransactionActivityLog from '../../transaction-activity-log'
|
||||
|
||||
describe('TransactionListItemDetails Component', () => {
|
||||
it('should render properly', () => {
|
||||
const transaction = {
|
||||
history: [],
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionListItemDetails
|
||||
transaction={transaction}
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-list-item-details'))
|
||||
assert.equal(wrapper.find(Button).length, 1)
|
||||
assert.equal(wrapper.find(SenderToRecipient).length, 1)
|
||||
assert.equal(wrapper.find(TransactionBreakdown).length, 1)
|
||||
assert.equal(wrapper.find(TransactionActivityLog).length, 1)
|
||||
})
|
||||
|
||||
it('should render a retry button', () => {
|
||||
const transaction = {
|
||||
history: [],
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionListItemDetails
|
||||
transaction={transaction}
|
||||
showRetry={true}
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-list-item-details'))
|
||||
assert.equal(wrapper.find(Button).length, 2)
|
||||
})
|
||||
})
|
@ -0,0 +1,80 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SenderToRecipient from '../sender-to-recipient'
|
||||
import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants'
|
||||
import TransactionActivityLog from '../transaction-activity-log'
|
||||
import TransactionBreakdown from '../transaction-breakdown'
|
||||
import Button from '../button'
|
||||
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
|
||||
|
||||
export default class TransactionListItemDetails extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
transaction: PropTypes.object,
|
||||
showRetry: PropTypes.bool,
|
||||
}
|
||||
|
||||
handleEtherscanClick = () => {
|
||||
const { hash, metamaskNetworkId } = this.props.transaction
|
||||
|
||||
const prefix = prefixForNetwork(metamaskNetworkId)
|
||||
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
|
||||
global.platform.openWindow({ url: etherscanUrl })
|
||||
this.setState({ showTransactionDetails: true })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { transaction, showRetry } = this.props
|
||||
const { txParams: { to, from } = {} } = transaction
|
||||
|
||||
return (
|
||||
<div className="transaction-list-item-details">
|
||||
<div className="transaction-list-item-details__header">
|
||||
<div>Details</div>
|
||||
<div className="transaction-list-item-details__header-buttons">
|
||||
{
|
||||
showRetry && (
|
||||
<Button
|
||||
type="raised"
|
||||
onClick={this.handleEtherscanClick}
|
||||
className="transaction-list-item-details__header-button"
|
||||
>
|
||||
{ t('speedUp') }
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
type="raised"
|
||||
onClick={this.handleEtherscanClick}
|
||||
className="transaction-list-item-details__header-button"
|
||||
>
|
||||
<img src="/images/arrow-popout.svg" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="transaction-list-item-details__sender-to-recipient-container">
|
||||
<SenderToRecipient
|
||||
variant={CARDS_VARIANT}
|
||||
addressOnly
|
||||
recipientAddress={to}
|
||||
senderAddress={from}
|
||||
/>
|
||||
</div>
|
||||
<div className="transaction-list-item-details__cards-container">
|
||||
<TransactionBreakdown
|
||||
transaction={transaction}
|
||||
className="transaction-list-item-details__transaction-breakdown"
|
||||
/>
|
||||
<TransactionActivityLog
|
||||
transaction={transaction}
|
||||
className="transaction-list-item-details__transaction-activity-log"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,37 +1,34 @@
|
||||
.transaction-list-item {
|
||||
box-sizing: border-box;
|
||||
min-height: 74px;
|
||||
padding: 8px 20px;
|
||||
border-bottom: 1px solid $geyser;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: 8px 20px 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba($alto, .2);
|
||||
}
|
||||
|
||||
&__grid {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: 45px 1fr 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"identicon action status primary-amount"
|
||||
"identicon nonce status secondary-amount";
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
grid-template-columns: 45px 5fr 3fr;
|
||||
grid-template-areas:
|
||||
"nonce nonce nonce"
|
||||
"identicon action primary-amount"
|
||||
"identicon status secondary-amount";
|
||||
}
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: 8px 20px 12px;
|
||||
grid-template-columns: 45px 5fr 3fr;
|
||||
grid-template-areas:
|
||||
"nonce nonce nonce"
|
||||
"identicon action primary-amount"
|
||||
"identicon status secondary-amount";
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba($alto, .2);
|
||||
}
|
||||
}
|
||||
|
||||
&__identicon {
|
||||
@ -114,4 +111,10 @@
|
||||
font-size: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__details-container {
|
||||
padding: 8px 16px 16px;
|
||||
background: #f3f4f7;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import TransactionStatus from '../transaction-status'
|
||||
import TransactionAction from '../transaction-action'
|
||||
import CurrencyDisplay from '../currency-display'
|
||||
import TokenCurrencyDisplay from '../token-currency-display'
|
||||
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
|
||||
import TransactionListItemDetails from '../transaction-list-item-details'
|
||||
import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
|
||||
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
|
||||
import { ETH } from '../../constants/common'
|
||||
@ -22,19 +22,24 @@ export default class TransactionListItem extends PureComponent {
|
||||
nonceAndDate: PropTypes.string,
|
||||
token: PropTypes.object,
|
||||
assetImages: PropTypes.object,
|
||||
tokenData: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
showTransactionDetails: false,
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
const { transaction, history } = this.props
|
||||
const { id, status, hash, metamaskNetworkId } = transaction
|
||||
const { id, status } = transaction
|
||||
const { showTransactionDetails } = this.state
|
||||
|
||||
if (status === UNAPPROVED_STATUS) {
|
||||
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
|
||||
} else if (hash) {
|
||||
const prefix = prefixForNetwork(metamaskNetworkId)
|
||||
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
|
||||
global.platform.openWindow({ url: etherscanUrl })
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ showTransactionDetails: !showTransactionDetails })
|
||||
}
|
||||
|
||||
handleRetryClick = event => {
|
||||
@ -75,6 +80,8 @@ export default class TransactionListItem extends PureComponent {
|
||||
className="transaction-list-item__amount transaction-list-item__amount--primary"
|
||||
value={value}
|
||||
prefix="-"
|
||||
numberOfDecimals={2}
|
||||
currency={ETH}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -89,8 +96,6 @@ export default class TransactionListItem extends PureComponent {
|
||||
className="transaction-list-item__amount transaction-list-item__amount--secondary"
|
||||
prefix="-"
|
||||
value={value}
|
||||
numberOfDecimals={2}
|
||||
currency={ETH}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -102,20 +107,25 @@ export default class TransactionListItem extends PureComponent {
|
||||
showRetry,
|
||||
nonceAndDate,
|
||||
assetImages,
|
||||
tokenData,
|
||||
} = this.props
|
||||
const { txParams = {} } = transaction
|
||||
const { showTransactionDetails } = this.state
|
||||
const toAddress = tokenData
|
||||
? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to
|
||||
: txParams.to
|
||||
|
||||
return (
|
||||
<div
|
||||
className="transaction-list-item"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div className="transaction-list-item__grid">
|
||||
<div className="transaction-list-item">
|
||||
<div
|
||||
className="transaction-list-item__grid"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<Identicon
|
||||
className="transaction-list-item__identicon"
|
||||
address={txParams.to}
|
||||
address={toAddress}
|
||||
diameter={34}
|
||||
image={assetImages[txParams.to]}
|
||||
image={assetImages[toAddress]}
|
||||
/>
|
||||
<TransactionAction
|
||||
transaction={transaction}
|
||||
@ -141,12 +151,12 @@ export default class TransactionListItem extends PureComponent {
|
||||
{ this.renderSecondaryCurrency() }
|
||||
</div>
|
||||
{
|
||||
showRetry && methodData.done && (
|
||||
<div
|
||||
className="transaction-list-item__retry"
|
||||
onClick={this.handleRetryClick}
|
||||
>
|
||||
<span>Taking too long? Increase the gas price on your transaction</span>
|
||||
showTransactionDetails && (
|
||||
<div className="transaction-list-item__details-container">
|
||||
<TransactionListItemDetails
|
||||
transaction={transaction}
|
||||
showRetry={showRetry && methodData.done}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -5,16 +5,19 @@ import withMethodData from '../../higher-order-components/with-method-data'
|
||||
import TransactionListItem from './transaction-list-item.component'
|
||||
import { setSelectedToken, retryTransaction } from '../../actions'
|
||||
import { hexToDecimal } from '../../helpers/conversions.util'
|
||||
import { getTokenData } from '../../helpers/transactions.util'
|
||||
import { formatDate } from '../../util'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { transaction: { txParams: { value, nonce } = {}, time } = {} } = ownProps
|
||||
const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps
|
||||
|
||||
const tokenData = data && getTokenData(data)
|
||||
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
|
||||
|
||||
return {
|
||||
value,
|
||||
nonceAndDate,
|
||||
tokenData,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
&__completed-transactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
@ -35,6 +35,7 @@
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-rows: 35% 1fr;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
|
@ -17,6 +17,8 @@ const TokenList = require('./token-list')
|
||||
const selectors = require('../selectors')
|
||||
const { ADD_TOKEN_ROUTE } = require('../routes')
|
||||
|
||||
import Button from './button'
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
@ -199,7 +201,9 @@ WalletView.prototype.render = function () {
|
||||
|
||||
h(TokenList),
|
||||
|
||||
h('button.btn-primary.wallet-view__add-token-button', {
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
className: 'wallet-view__add-token-button',
|
||||
onClick: () => {
|
||||
history.push(ADD_TOKEN_ROUTE)
|
||||
sidebarOpen && hideSidebar()
|
||||
|
@ -1 +1,3 @@
|
||||
export const ETH = 'ETH'
|
||||
export const GWEI = 'GWEI'
|
||||
export const WEI = 'WEI'
|
||||
|
@ -35,6 +35,7 @@ BigNumber.config({
|
||||
// Big Number Constants
|
||||
const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000')
|
||||
const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
|
||||
const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1')
|
||||
|
||||
// Individual Setters
|
||||
const convert = R.invoker(1, 'times')
|
||||
@ -52,10 +53,12 @@ const toBigNumber = {
|
||||
const toNormalizedDenomination = {
|
||||
WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER),
|
||||
GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER),
|
||||
ETH: bigNumber => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER),
|
||||
}
|
||||
const toSpecifiedDenomination = {
|
||||
WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(),
|
||||
GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9),
|
||||
ETH: bigNumber => bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).round(9),
|
||||
}
|
||||
const baseChange = {
|
||||
hex: n => n.toString(16),
|
||||
|
@ -2,10 +2,7 @@
|
||||
Buttons
|
||||
*/
|
||||
|
||||
.btn-default,
|
||||
.btn-primary,
|
||||
.btn-secondary,
|
||||
.btn-confirm {
|
||||
.button {
|
||||
height: 44px;
|
||||
background: $white;
|
||||
display: flex;
|
||||
@ -79,6 +76,16 @@
|
||||
background-color: $curious-blue;
|
||||
}
|
||||
|
||||
.btn-raised {
|
||||
color: $curious-blue;
|
||||
background-color: $white;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
|
||||
padding: 6px;
|
||||
height: initial;
|
||||
width: initial;
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
.btn--large {
|
||||
height: 54px;
|
||||
}
|
||||
|
@ -837,6 +837,10 @@
|
||||
line-height: 12px;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__gas-modal-card {
|
||||
|
@ -58,6 +58,7 @@ export function getValueFromWeiHex ({
|
||||
toCurrency,
|
||||
conversionRate,
|
||||
numberOfDecimals,
|
||||
toDenomination,
|
||||
}) {
|
||||
return conversionUtil(value, {
|
||||
fromNumericBase: 'hex',
|
||||
@ -66,6 +67,7 @@ export function getValueFromWeiHex ({
|
||||
toCurrency,
|
||||
numberOfDecimals,
|
||||
fromDenomination: 'WEI',
|
||||
toDenomination,
|
||||
conversionRate,
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { conversionUtil } from '../conversion-util'
|
||||
import { ETH, GWEI, WEI } from '../constants/common'
|
||||
|
||||
export function hexToDecimal (hexValue) {
|
||||
return conversionUtil(hexValue, {
|
||||
@ -7,16 +8,27 @@ export function hexToDecimal (hexValue) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getEthFromWeiHex ({
|
||||
value,
|
||||
conversionRate,
|
||||
}) {
|
||||
return getValueFromWeiHex({
|
||||
value,
|
||||
conversionRate,
|
||||
toCurrency: 'ETH',
|
||||
numberOfDecimals: 6,
|
||||
})
|
||||
export function getEthConversionFromWeiHex ({ value, conversionRate, numberOfDecimals = 6 }) {
|
||||
const denominations = [ETH, GWEI, WEI]
|
||||
|
||||
let nonZeroDenomination
|
||||
|
||||
for (let i = 0; i < denominations.length; i++) {
|
||||
const convertedValue = getValueFromWeiHex({
|
||||
value,
|
||||
conversionRate,
|
||||
toCurrency: ETH,
|
||||
numberOfDecimals,
|
||||
toDenomination: denominations[i],
|
||||
})
|
||||
|
||||
if (convertedValue !== '0' || i === denominations.length - 1) {
|
||||
nonZeroDenomination = `${convertedValue} ${denominations[i]}`
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nonZeroDenomination
|
||||
}
|
||||
|
||||
export function getValueFromWeiHex ({
|
||||
@ -24,14 +36,16 @@ export function getValueFromWeiHex ({
|
||||
toCurrency,
|
||||
conversionRate,
|
||||
numberOfDecimals,
|
||||
toDenomination,
|
||||
}) {
|
||||
return conversionUtil(value, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
fromCurrency: 'ETH',
|
||||
fromCurrency: ETH,
|
||||
toCurrency,
|
||||
numberOfDecimals,
|
||||
fromDenomination: 'WEI',
|
||||
fromDenomination: WEI,
|
||||
toDenomination,
|
||||
conversionRate,
|
||||
})
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
UNKNOWN_FUNCTION_KEY,
|
||||
} from '../constants/transactions'
|
||||
|
||||
import { addCurrencies } from '../conversion-util'
|
||||
|
||||
abiDecoder.addABI(abi)
|
||||
|
||||
export function getTokenData (data = {}) {
|
||||
@ -103,3 +105,13 @@ export async function isSmartContractAddress (address) {
|
||||
const code = await global.eth.getCode(address)
|
||||
return code && code !== '0x'
|
||||
}
|
||||
|
||||
export function sumHexes (...args) {
|
||||
const total = args.reduce((acc, base) => {
|
||||
return addCurrencies(acc, base, {
|
||||
toNumericBase: 'hex',
|
||||
})
|
||||
})
|
||||
|
||||
return ethUtil.addHexPrefix(total)
|
||||
}
|
||||
|
@ -20,10 +20,10 @@ const getMessage = (locale, key, substitutions) => {
|
||||
let phrase = entry.message
|
||||
// perform substitutions
|
||||
if (substitutions && substitutions.length) {
|
||||
phrase = phrase.replace(/\$1/g, substitutions[0])
|
||||
if (substitutions.length > 1) {
|
||||
phrase = phrase.replace(/\$2/g, substitutions[1])
|
||||
}
|
||||
substitutions.forEach((substitution, index) => {
|
||||
const regex = new RegExp(`\\$${index + 1}`, 'g')
|
||||
phrase = phrase.replace(regex, substitution)
|
||||
})
|
||||
}
|
||||
return phrase
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user