1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Merge pull request #5282 from MetaMask/tx-cancel-ui

Add Cancel Transaction feature. Refactor modals. Add Transaction Details modal.
This commit is contained in:
Alexander Tseung 2018-09-24 11:34:12 -07:00 committed by GitHub
commit 5d57c7c4fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1220 additions and 374 deletions

View File

@ -61,6 +61,12 @@
"attemptingConnect": {
"message": "Attempting to connect to blockchain."
},
"attemptToCancel": {
"message": "Attempt to Cancel?"
},
"attemptToCancelDescription": {
"message": "Attempting to cancel does not guarantee your original transaction will be cancelled. If cancelled, you are still required to pay a transaction fee to the network."
},
"attributions": {
"message": "Attributions"
},
@ -116,6 +122,12 @@
"cancel": {
"message": "Cancel"
},
"cancelAttempt": {
"message": "Cancel Attempt"
},
"cancellationGasFee": {
"message": "Cancellation Gas Fee"
},
"classicInterface": {
"message": "Use classic interface"
},
@ -1109,6 +1121,9 @@
"transactionCreated": {
"message": "Transaction created with a value of $1 on $2."
},
"transactionWithNonce": {
"message": "Transaction $1"
},
"transactionDropped": {
"message": "Transaction dropped on $2."
},
@ -1240,6 +1255,9 @@
"whatsThis": {
"message": "What's this?"
},
"yesLetsTry": {
"message": "Yes, let's try"
},
"youNeedToAllowCameraAccess": {
"message": "You need to allow camera access to use this feature."
},

View File

@ -18,7 +18,7 @@ const {
TRANSACTION_STATUS_APPROVED,
} = require('./enums')
const { hexToBn, bnToHex } = require('../../lib/util')
const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util')
/**
Transaction Controller is an aggregate of sub-controllers and trackers
@ -244,7 +244,8 @@ class TransactionController extends EventEmitter {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta
const { gasPrice: lastGasPrice, from, nonce } = txParams
const newGasPrice = customGasPrice || bnToHex(hexToBn(lastGasPrice).mul(1.1))
const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
const newTxMeta = this.txStateManager.generateTxMeta({
txParams: {
from,

View File

@ -34,7 +34,7 @@ async function runTxListItemsTest (assert, done) {
const retryTxGrid = await findAsync($(txListItems[1]), '.transaction-list-item__grid')
retryTxGrid[0].click()
const retryTxDetails = await findAsync($(txListItems[1]), '.transaction-list-item-details')
const retryTxDetails = await findAsync($, '.transaction-list-item-details')
const headerButtons = await findAsync($(retryTxDetails[0]), '.transaction-list-item-details__header-button')
assert.equal(headerButtons[0].textContent, 'speed up')

View File

@ -2,11 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Tabs, Tab } from '../../tabs'
import {
ConfirmPageContainerSummary,
ConfirmPageContainerError,
ConfirmPageContainerWarning,
} from './'
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from './'
import ErrorMessage from '../../error-message'
export default class ConfirmPageContainerContent extends Component {
static propTypes = {
@ -95,7 +92,7 @@ export default class ConfirmPageContainerContent extends Component {
{
(errorKey || errorMessage) && (
<div className="confirm-page-container-content__error-container">
<ConfirmPageContainerError
<ErrorMessage
errorMessage={errorMessage}
errorKey={errorKey}
/>

View File

@ -1 +0,0 @@
export { default } from './confirm-page-container-error.component'

View File

@ -1,4 +1,3 @@
export { default } from './confirm-page-container-content.component'
export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary'
export { default as ConfirmPageContainerError } from './confirm-page-container-error'
export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning'

View File

@ -1,5 +1,3 @@
@import './confirm-page-container-error/index';
@import './confirm-page-container-warning/index';
@import './confirm-page-container-summary/index';

View File

@ -1,30 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
const ConfirmPageContainerError = (props, context) => {
const ErrorMessage = (props, context) => {
const { errorMessage, errorKey } = props
const error = errorKey ? context.t(errorKey) : errorMessage
return (
<div className="confirm-page-container-error">
<div className="error-message">
<img
src="/images/alert-red.svg"
className="confirm-page-container-error__icon"
className="error-message__icon"
/>
<div className="confirm-page-container-error__text">
<div className="error-message__text">
{ `ALERT: ${error}` }
</div>
</div>
)
}
ConfirmPageContainerError.propTypes = {
ErrorMessage.propTypes = {
errorMessage: PropTypes.string,
errorKey: PropTypes.string,
}
ConfirmPageContainerError.contextTypes = {
ErrorMessage.contextTypes = {
t: PropTypes.func,
}
export default ConfirmPageContainerError
export default ErrorMessage

View File

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

View File

@ -1,4 +1,4 @@
.confirm-page-container-error {
.error-message {
min-height: 32px;
border: 1px solid $monzo;
color: $monzo;

View File

@ -0,0 +1,36 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import ErrorMessage from '../error-message.component'
describe('ErrorMessage Component', () => {
const t = key => `translate ${key}`
it('should render a message from props.errorMessage', () => {
const wrapper = shallow(
<ErrorMessage
errorMessage="This is an error."
/>,
{ context: { t }}
)
assert.ok(wrapper)
assert.equal(wrapper.find('.error-message').length, 1)
assert.equal(wrapper.find('.error-message__icon').length, 1)
assert.equal(wrapper.find('.error-message__text').text(), 'ALERT: This is an error.')
})
it('should render a message translated from props.errorKey', () => {
const wrapper = shallow(
<ErrorMessage
errorKey="testKey"
/>,
{ context: { t }}
)
assert.ok(wrapper)
assert.equal(wrapper.find('.error-message').length, 1)
assert.equal(wrapper.find('.error-message__icon').length, 1)
assert.equal(wrapper.find('.error-message__text').text(), 'ALERT: translate testKey')
})
})

View File

@ -6,12 +6,16 @@
@import './confirm-page-container/index';
@import './error-message/index';
@import './export-text-container/index';
@import './info-box/index';
@import './menu-bar/index';
@import './modal/index';
@import './modals/index';
@import './network-display/index';

View File

@ -0,0 +1,2 @@
export { default } from './modal.component'
export { default as ModalContent } from './modal-content'

View File

@ -0,0 +1,62 @@
@import './modal-content/index';
.modal-container {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-flow: column;
border-radius: 8px;
@media screen and (max-width: 575px) {
max-height: 450px;
}
&__content {
overflow-y: auto;
flex: 1;
padding: 16px 32px;
@media screen and (max-width: 575px) {
justify-content: center;
padding: 28px 20px;
}
}
&__header {
position: relative;
display: flex;
padding: 12px;
justify-content: center;
border-bottom: 1px solid #d2d8dd;
flex: 0 0 auto;
}
&__header-close::after {
content: '\00D7';
font-size: 40px;
color: $dusty-gray;
position: absolute;
top: -5px;
right: 10px;
cursor: pointer;
}
&__footer {
display: flex;
flex-flow: row;
justify-content: center;
border-top: 1px solid #d2d8dd;
padding: 16px;
flex: 0 0 auto;
&-button {
min-width: 0;
margin-right: 16px;
&:last-of-type {
margin-right: 0;
}
}
}
}

View File

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

View File

@ -0,0 +1,19 @@
.modal-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 0;
&__title {
font-size: 1.5rem;
font-weight: 500;
padding: 16px 0;
text-align: center;
}
&__description {
text-align: center;
font-size: .875rem;
}
}

View File

@ -0,0 +1,32 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
export default class ModalContent extends PureComponent {
static propTypes = {
title: PropTypes.string,
description: PropTypes.string,
}
render () {
const { title, description } = this.props
return (
<div className="modal-content">
{
title && (
<div className="modal-content__title">
{ title }
</div>
)
}
{
description && (
<div className="modal-content__description">
{ description }
</div>
)
}
</div>
)
}
}

View File

@ -0,0 +1,44 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import ModalContent from '../modal-content.component'
describe('ModalContent Component', () => {
it('should render a title', () => {
const wrapper = shallow(
<ModalContent
title="Modal Title"
/>
)
assert.equal(wrapper.find('.modal-content__title').length, 1)
assert.equal(wrapper.find('.modal-content__title').text(), 'Modal Title')
assert.equal(wrapper.find('.modal-content__description').length, 0)
})
it('should render a description', () => {
const wrapper = shallow(
<ModalContent
description="Modal Description"
/>
)
assert.equal(wrapper.find('.modal-content__title').length, 0)
assert.equal(wrapper.find('.modal-content__description').length, 1)
assert.equal(wrapper.find('.modal-content__description').text(), 'Modal Description')
})
it('should render both a title and a description', () => {
const wrapper = shallow(
<ModalContent
title="Modal Title"
description="Modal Description"
/>
)
assert.equal(wrapper.find('.modal-content__title').length, 1)
assert.equal(wrapper.find('.modal-content__title').text(), 'Modal Title')
assert.equal(wrapper.find('.modal-content__description').length, 1)
assert.equal(wrapper.find('.modal-content__description').text(), 'Modal Description')
})
})

View File

@ -0,0 +1,80 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../button'
export default class Modal extends PureComponent {
static propTypes = {
children: PropTypes.node,
// Header text
headerText: PropTypes.string,
onClose: PropTypes.func,
// Submit button (right button)
onSubmit: PropTypes.func,
submitType: PropTypes.string,
submitText: PropTypes.string,
// Cancel button (left button)
onCancel: PropTypes.func,
cancelType: PropTypes.string,
cancelText: PropTypes.string,
}
static defaultProps = {
submitType: 'primary',
cancelType: 'default',
}
render () {
const {
children,
headerText,
onClose,
onSubmit,
submitType,
submitText,
onCancel,
cancelType,
cancelText,
} = this.props
return (
<div className="modal-container">
{
headerText && (
<div className="modal-container__header">
<div className="modal-container__header-text">
{ headerText }
</div>
<div
className="modal-container__header-close"
onClick={onClose}
/>
</div>
)
}
<div className="modal-container__content">
{ children }
</div>
<div className="modal-container__footer">
{
onCancel && (
<Button
type={cancelType}
onClick={onCancel}
className="modal-container__footer-button"
>
{ cancelText }
</Button>
)
}
<Button
type={submitType}
onClick={onSubmit}
className="modal-container__footer-button"
>
{ submitText }
</Button>
</div>
</div>
)
}
}

View File

@ -0,0 +1,103 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import Modal from '../modal.component'
import Button from '../../button'
describe('Modal Component', () => {
it('should render a modal with a submit button', () => {
const wrapper = shallow(<Modal />)
assert.equal(wrapper.find('.modal-container').length, 1)
const buttons = wrapper.find(Button)
assert.equal(buttons.length, 1)
assert.equal(buttons.at(0).props().type, 'primary')
})
it('should render a modal with a cancel and a submit button', () => {
const handleCancel = sinon.spy()
const handleSubmit = sinon.spy()
const wrapper = shallow(
<Modal
onCancel={handleCancel}
cancelText="Cancel"
onSubmit={handleSubmit}
submitText="Submit"
/>
)
const buttons = wrapper.find(Button)
assert.equal(buttons.length, 2)
const cancelButton = buttons.at(0)
const submitButton = buttons.at(1)
assert.equal(cancelButton.props().type, 'default')
assert.equal(cancelButton.props().children, 'Cancel')
assert.equal(handleCancel.callCount, 0)
cancelButton.simulate('click')
assert.equal(handleCancel.callCount, 1)
assert.equal(submitButton.props().type, 'primary')
assert.equal(submitButton.props().children, 'Submit')
assert.equal(handleSubmit.callCount, 0)
submitButton.simulate('click')
assert.equal(handleSubmit.callCount, 1)
})
it('should render a modal with different button types', () => {
const wrapper = shallow(
<Modal
onCancel={() => {}}
cancelText="Cancel"
cancelType="secondary"
onSubmit={() => {}}
submitText="Submit"
submitType="confirm"
/>
)
const buttons = wrapper.find(Button)
assert.equal(buttons.length, 2)
assert.equal(buttons.at(0).props().type, 'secondary')
assert.equal(buttons.at(1).props().type, 'confirm')
})
it('should render a modal with children', () => {
const wrapper = shallow(
<Modal
onCancel={() => {}}
cancelText="Cancel"
onSubmit={() => {}}
submitText="Submit"
>
<div className="test-child" />
</Modal>
)
assert.ok(wrapper.find('.test-class'))
})
it('should render a modal with a header', () => {
const handleCancel = sinon.spy()
const handleSubmit = sinon.spy()
const wrapper = shallow(
<Modal
onCancel={handleCancel}
cancelText="Cancel"
onSubmit={handleSubmit}
submitText="Submit"
headerText="My Header"
onClose={handleCancel}
/>
)
assert.ok(wrapper.find('.modal-container__header'))
assert.equal(wrapper.find('.modal-container__header-text').text(), 'My Header')
assert.equal(handleCancel.callCount, 0)
assert.equal(handleSubmit.callCount, 0)
wrapper.find('.modal-container__header-close').simulate('click')
assert.equal(handleCancel.callCount, 1)
assert.equal(handleSubmit.callCount, 0)
})
})

View File

@ -0,0 +1,29 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../../../currency-display'
import { ETH } from '../../../../constants/common'
export default class CancelTransaction extends PureComponent {
static propTypes = {
value: PropTypes.string,
}
render () {
const { value } = this.props
return (
<div className="cancel-transaction-gas-fee">
<CurrencyDisplay
className="cancel-transaction-gas-fee__eth"
currency={ETH}
value={value}
numberOfDecimals={6}
/>
<CurrencyDisplay
className="cancel-transaction-gas-fee__fiat"
value={value}
/>
</div>
)
}
}

View File

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

View File

@ -0,0 +1,17 @@
.cancel-transaction-gas-fee {
background: #F1F4F9;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
&__eth {
font-size: 1.5rem;
font-weight: 500;
}
&__fiat {
font-size: .75rem;
}
}

View File

@ -0,0 +1,27 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component'
import CurrencyDisplay from '../../../../currency-display'
describe('CancelTransactionGasFee Component', () => {
it('should render', () => {
const wrapper = shallow(
<CancelTransactionGasFee
value="0x3b9aca00"
/>
)
assert.ok(wrapper)
assert.equal(wrapper.find(CurrencyDisplay).length, 2)
const ethDisplay = wrapper.find(CurrencyDisplay).at(0)
const fiatDisplay = wrapper.find(CurrencyDisplay).at(1)
assert.equal(ethDisplay.props().value, '0x3b9aca00')
assert.equal(ethDisplay.props().currency, 'ETH')
assert.equal(ethDisplay.props().className, 'cancel-transaction-gas-fee__eth')
assert.equal(fiatDisplay.props().value, '0x3b9aca00')
assert.equal(fiatDisplay.props().className, 'cancel-transaction-gas-fee__fiat')
})
})

View File

@ -0,0 +1,68 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Modal from '../../modal'
import CancelTransactionGasFee from './cancel-transaction-gas-fee'
import { SUBMITTED_STATUS } from '../../../constants/transactions'
export default class CancelTransaction extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
createCancelTransaction: PropTypes.func,
hideModal: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func,
transactionStatus: PropTypes.string,
newGasFee: PropTypes.string,
}
componentDidUpdate () {
const { transactionStatus, showTransactionConfirmedModal } = this.props
if (transactionStatus !== SUBMITTED_STATUS) {
showTransactionConfirmedModal()
return
}
}
handleSubmit = async () => {
const { createCancelTransaction, hideModal } = this.props
await createCancelTransaction()
hideModal()
}
handleCancel = () => {
this.props.hideModal()
}
render () {
const { t } = this.context
const { newGasFee } = this.props
return (
<Modal
headerText={t('attemptToCancel')}
onClose={this.handleCancel}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
submitText={t('yesLetsTry')}
cancelText={t('nevermind')}
submitType="secondary"
>
<div>
<div className="cancel-transaction__title">
{ t('cancellationGasFee') }
</div>
<div className="cancel-transaction__cancel-transaction-gas-fee-container">
<CancelTransactionGasFee value={newGasFee} />
</div>
<div className="cancel-transaction__description">
{ t('attemptToCancelDescription') }
</div>
</div>
</Modal>
)
}
}

View File

@ -0,0 +1,62 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import ethUtil from 'ethereumjs-util'
import { multiplyCurrencies } from '../../../conversion-util'
import withModalProps from '../../../higher-order-components/with-modal-props'
import CancelTransaction from './cancel-transaction.component'
import { showModal, createCancelTransaction } from '../../../actions'
import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
const mapStateToProps = (state, ownProps) => {
const { metamask } = state
const { transactionId, originalGasPrice } = ownProps
const { selectedAddressTxList } = metamask
const transaction = selectedAddressTxList.find(({ id }) => id === transactionId)
const transactionStatus = transaction ? transaction.status : ''
const defaultNewGasPrice = ethUtil.addHexPrefix(
multiplyCurrencies(originalGasPrice, 1.1, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 10,
})
)
const newGasFee = getHexGasTotal({ gasPrice: defaultNewGasPrice, gasLimit: '0x5208' })
return {
transactionId,
transactionStatus,
originalGasPrice,
newGasFee,
}
}
const mapDispatchToProps = dispatch => {
return {
createCancelTransaction: txId => dispatch(createCancelTransaction(txId)),
showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { transactionId, ...restStateProps } = stateProps
const {
createCancelTransaction: dispatchCreateCancelTransaction,
...restDispatchProps
} = dispatchProps
return {
...restStateProps,
...restDispatchProps,
...ownProps,
createCancelTransaction: newGasPrice => {
return dispatchCreateCancelTransaction(transactionId, newGasPrice)
},
}
}
export default compose(
withModalProps,
connect(mapStateToProps, mapDispatchToProps, mergeProps),
)(CancelTransaction)

View File

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

View File

@ -0,0 +1,18 @@
@import './cancel-transaction-gas-fee/index';
.cancel-transaction {
&__title {
font-weight: 500;
padding-bottom: 16px;
text-align: center;
}
&__description {
text-align: center;
font-size: .875rem;
}
&__cancel-transaction-gas-fee-container {
margin-bottom: 16px;
}
}

View File

@ -0,0 +1,56 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import CancelTransaction from '../cancel-transaction.component'
import CancelTransactionGasFee from '../cancel-transaction-gas-fee'
import Modal from '../../../modal'
describe('CancelTransaction Component', () => {
const t = key => key
it('should render a CancelTransaction modal', () => {
const wrapper = shallow(
<CancelTransaction
newGasFee="0x1319718a5000"
/>,
{ context: { t }}
)
assert.ok(wrapper)
assert.equal(wrapper.find(Modal).length, 1)
assert.equal(wrapper.find(CancelTransactionGasFee).length, 1)
assert.equal(wrapper.find(CancelTransactionGasFee).props().value, '0x1319718a5000')
assert.equal(wrapper.find('.cancel-transaction__title').text(), 'cancellationGasFee')
assert.equal(wrapper.find('.cancel-transaction__description').text(), 'attemptToCancelDescription')
})
it('should pass the correct props to the Modal component', async () => {
const createCancelTransactionSpy = sinon.stub().callsFake(() => Promise.resolve())
const hideModalSpy = sinon.spy()
const wrapper = shallow(
<CancelTransaction
defaultNewGasPrice="0x3b9aca00"
createCancelTransaction={createCancelTransactionSpy}
hideModal={hideModalSpy}
/>,
{ context: { t }}
)
assert.equal(wrapper.find(Modal).length, 1)
const modalProps = wrapper.find(Modal).props()
assert.equal(modalProps.headerText, 'attemptToCancel')
assert.equal(modalProps.submitText, 'yesLetsTry')
assert.equal(modalProps.cancelText, 'nevermind')
assert.equal(createCancelTransactionSpy.callCount, 0)
assert.equal(hideModalSpy.callCount, 0)
await modalProps.onSubmit()
assert.equal(createCancelTransactionSpy.callCount, 1)
assert.equal(hideModalSpy.callCount, 1)
modalProps.onCancel()
assert.equal(hideModalSpy.callCount, 2)
})
})

View File

@ -1,11 +1,11 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Button from '../../button'
import Modal from '../../modal'
import { addressSummary } from '../../../util'
import Identicon from '../../identicon'
import genAccountLink from '../../../../lib/account-link'
class ConfirmRemoveAccount extends Component {
export default class ConfirmRemoveAccount extends Component {
static propTypes = {
hideModal: PropTypes.func.isRequired,
removeAccount: PropTypes.func.isRequired,
@ -17,30 +17,34 @@ class ConfirmRemoveAccount extends Component {
t: PropTypes.func,
}
handleRemove () {
handleRemove = () => {
this.props.removeAccount(this.props.identity.address)
.then(() => this.props.hideModal())
}
handleCancel = () => {
this.props.hideModal()
}
renderSelectedAccount () {
const { identity } = this.props
return (
<div className="modal-container__account">
<div className="modal-container__account__identicon">
<div className="confirm-remove-account__account">
<div className="confirm-remove-account__account__identicon">
<Identicon
address={identity.address}
diameter={32}
address={identity.address}
diameter={32}
/>
</div>
<div className="modal-container__account__name">
<span className="modal-container__account__label">Name</span>
<span className="account_value">{identity.name}</span>
<div className="confirm-remove-account__account__name">
<span className="confirm-remove-account__account__label">Name</span>
<span className="account_value">{identity.name}</span>
</div>
<div className="modal-container__account__address">
<span className="modal-container__account__label">Public Address</span>
<span className="account_value">{ addressSummary(identity.address, 4, 4) }</span>
<div className="confirm-remove-account__account__address">
<span className="confirm-remove-account__account__label">Public Address</span>
<span className="account_value">{ addressSummary(identity.address, 4, 4) }</span>
</div>
<div className="modal-container__account__link">
<div className="confirm-remove-account__account__link">
<a
className=""
href={genAccountLink(identity.address, this.props.network)}
@ -58,36 +62,28 @@ class ConfirmRemoveAccount extends Component {
const { t } = this.context
return (
<div className="modal-container">
<div className="modal-container__content">
<div className="modal-container__title">
{ `${t('removeAccount')}` }?
</div>
{ this.renderSelectedAccount() }
<div className="modal-container__description">
<Modal
headerText={`${t('removeAccount')}?`}
onClose={this.handleCancel}
onSubmit={this.handleRemove}
onCancel={this.handleCancel}
submitText={t('remove')}
cancelText={t('nevermind')}
submitType="secondary"
>
<div>
{ this.renderSelectedAccount() }
<div className="confirm-remove-account__description">
{ t('removeAccountDescription') }
<a className="modal-container__link" rel="noopener noreferrer" target="_blank" href="https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI-">{ t('learnMore') }</a>
<a
className="confirm-remove-account__link"
rel="noopener noreferrer"
target="_blank" href="https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI-">
{ t('learnMore') }
</a>
</div>
</div>
<div className="modal-container__footer">
<Button
type="default"
className="modal-container__footer-button"
onClick={() => this.props.hideModal()}
>
{ t('nevermind') }
</Button>
<Button
type="secondary"
className="modal-container__footer-button"
onClick={() => this.handleRemove()}
>
{ t('remove') }
</Button>
</div>
</div>
</Modal>
)
}
}
export default ConfirmRemoveAccount

View File

@ -1,20 +1,22 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import ConfirmRemoveAccount from './confirm-remove-account.component'
const { hideModal, removeAccount } = require('../../../actions')
import withModalProps from '../../../higher-order-components/with-modal-props'
import { removeAccount } from '../../../actions'
const mapStateToProps = state => {
return {
identity: state.appState.modal.modalState.props.identity,
network: state.metamask.network,
}
}
const mapDispatchToProps = dispatch => {
return {
hideModal: () => dispatch(hideModal()),
removeAccount: (address) => dispatch(removeAccount(address)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount)
export default compose(
withModalProps,
connect(mapStateToProps, mapDispatchToProps)
)(ConfirmRemoveAccount)

View File

@ -1,2 +1 @@
import ConfirmRemoveAccount from './confirm-remove-account.container'
module.exports = ConfirmRemoveAccount
export { default } from './confirm-remove-account.container'

View File

@ -0,0 +1,58 @@
.confirm-remove-account {
&__description {
text-align: center;
font-size: .875rem;
}
&__account {
border: 1px solid #b7b7b7;
border-radius: 4px;
padding: 10px;
display: flex;
margin-top: 10px;
margin-bottom: 20px;
width: 100%;
&__identicon {
margin-right: 10px;
}
&__name,
&__address {
margin-right: 10px;
font-size: 14px;
}
&__name {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__label {
font-size: 11px;
display: block;
color: #9b9b9b;
}
&__link {
margin-top: 14px;
img {
width: 15px;
height: 15px;
}
}
@media screen and (max-width: 575px) {
&__name {
width: 90px;
}
}
}
&__link {
color: #2f9ae0;
}
}

View File

@ -1,8 +1,8 @@
import React, { Component } from 'react'
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../button'
import Modal, { ModalContent } from '../../modal'
class ConfirmResetAccount extends Component {
export default class ConfirmResetAccount extends PureComponent {
static propTypes = {
hideModal: PropTypes.func.isRequired,
resetAccount: PropTypes.func.isRequired,
@ -12,7 +12,7 @@ class ConfirmResetAccount extends Component {
t: PropTypes.func,
}
handleReset () {
handleReset = () => {
this.props.resetAccount()
.then(() => this.props.hideModal())
}
@ -21,34 +21,18 @@ class ConfirmResetAccount extends Component {
const { t } = this.context
return (
<div className="modal-container">
<div className="modal-container__content">
<div className="modal-container__title">
{ `${t('resetAccount')}?` }
</div>
<div className="modal-container__description">
{ t('resetAccountDescription') }
</div>
</div>
<div className="modal-container__footer">
<Button
type="default"
className="modal-container__footer-button"
onClick={() => this.props.hideModal()}
>
{ t('nevermind') }
</Button>
<Button
type="secondary"
className="modal-container__footer-button"
onClick={() => this.handleReset()}
>
{ t('reset') }
</Button>
</div>
</div>
<Modal
onSubmit={this.handleReset}
onCancel={() => this.props.hideModal()}
submitText={t('reset')}
cancelText={t('nevermind')}
submitType="secondary"
>
<ModalContent
title={`${t('resetAccount')}?`}
description={t('resetAccountDescription')}
/>
</Modal>
)
}
}
export default ConfirmResetAccount

View File

@ -1,13 +1,16 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import withModalProps from '../../../higher-order-components/with-modal-props'
import ConfirmResetAccount from './confirm-reset-account.component'
const { hideModal, resetAccount } = require('../../../actions')
import { resetAccount } from '../../../actions'
const mapDispatchToProps = dispatch => {
return {
hideModal: () => dispatch(hideModal()),
resetAccount: () => dispatch(resetAccount()),
}
}
export default connect(null, mapDispatchToProps)(ConfirmResetAccount)
export default compose(
withModalProps,
connect(null, mapDispatchToProps)
)(ConfirmResetAccount)

View File

@ -1,2 +1 @@
import ConfirmResetAccount from './confirm-reset-account.container'
module.exports = ConfirmResetAccount
export { default } from './confirm-reset-account.container'

View File

@ -1,108 +1,9 @@
@import './cancel-transaction/index';
@import './confirm-remove-account/index';
@import './customize-gas/index';
@import './qr-scanner/index';
.modal-container {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-flow: column;
border-radius: 8px;
&__title {
font-size: 1.5rem;
font-weight: 500;
padding: 16px 0;
text-align: center;
}
&__description {
text-align: center;
font-size: .875rem;
}
&__account {
border: 1px solid #b7b7b7;
border-radius: 4px;
padding: 10px;
display: flex;
margin-top: 10px;
margin-bottom: 20px;
width: 100%;
&__identicon {
margin-right: 10px;
}
&__name,
&__address {
margin-right: 10px;
font-size: 14px;
}
&__name {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__label {
font-size: 11px;
display: block;
color: #9b9b9b;
}
&__link {
margin-top: 14px;
img {
width: 15px;
height: 15px;
}
}
@media screen and (max-width: 575px) {
&__name {
width: 90px;
}
}
}
&__link {
color: #2f9ae0;
}
&__content {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 32px;
@media screen and (max-width: 575px) {
justify-content: center;
padding: 28px 20px;
}
}
&__footer {
display: flex;
flex-flow: row;
justify-content: center;
border-top: 1px solid #d2d8dd;
padding: 16px;
flex: 0 0 auto;
&-button {
min-width: 0;
margin-right: 16px;
&:last-of-type {
margin-right: 0;
}
}
}
}
@import './transaction-confirmed/index';

View File

@ -19,14 +19,15 @@ const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal')
const ConfirmResetAccount = require('./confirm-reset-account')
const ConfirmRemoveAccount = require('./confirm-remove-account')
const QRScanner = require('./qr-scanner')
const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification')
import ConfirmRemoveAccount from './confirm-remove-account'
import ConfirmResetAccount from './confirm-reset-account'
import TransactionConfirmed from './transaction-confirmed'
import ConfirmCustomizeGasModal from './customize-gas'
import CancelTransaction from './cancel-transaction'
import WelcomeBeta from './welcome-beta'
import TransactionDetails from './transaction-details'
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
@ -199,11 +200,7 @@ const MODALS = {
},
BETA_UI_NOTIFICATION_MODAL: {
contents: [
h(Notification, [
h(WelcomeBeta),
]),
],
contents: h(WelcomeBeta),
mobileModalStyle: {
...modalContainerMobileStyle,
},
@ -307,9 +304,7 @@ const MODALS = {
},
CONFIRM_CUSTOMIZE_GAS: {
contents: [
h(ConfirmCustomizeGasModal),
],
contents: h(ConfirmCustomizeGasModal),
mobileModalStyle: {
width: '100vw',
height: '100vh',
@ -332,11 +327,7 @@ const MODALS = {
TRANSACTION_CONFIRMED: {
disableBackdropClick: true,
contents: [
h(Notification, [
h(TransactionConfirmed),
]),
],
contents: h(TransactionConfirmed),
mobileModalStyle: {
...modalContainerMobileStyle,
},
@ -347,6 +338,7 @@ const MODALS = {
borderRadius: '8px',
},
},
QR_SCANNER: {
contents: h(QRScanner),
mobileModalStyle: {
@ -360,6 +352,32 @@ const MODALS = {
},
},
CANCEL_TRANSACTION: {
contents: h(CancelTransaction),
mobileModalStyle: {
...modalContainerMobileStyle,
},
laptopModalStyle: {
...modalContainerLaptopStyle,
},
contentStyle: {
borderRadius: '8px',
},
},
TRANSACTION_DETAILS: {
contents: h(TransactionDetails),
mobileModalStyle: {
...modalContainerMobileStyle,
},
laptopModalStyle: {
...modalContainerLaptopStyle,
},
contentStyle: {
borderRadius: '8px',
},
},
DEFAULT: {
contents: [],
mobileModalStyle: {},

View File

@ -1,2 +0,0 @@
import Notification from './notification.container'
module.exports = Notification

View File

@ -1,30 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../../button'
const Notification = (props, context) => {
return (
<div className="modal-container">
{ props.children }
<div className="modal-container__footer">
<Button
type="primary"
onClick={() => props.onHide()}
>
{ context.t('ok') }
</Button>
</div>
</div>
)
}
Notification.propTypes = {
onHide: PropTypes.func.isRequired,
children: PropTypes.element,
}
Notification.contextTypes = {
t: PropTypes.func,
}
export default Notification

View File

@ -1,38 +0,0 @@
import { connect } from 'react-redux'
import Notification from './notification.component'
const { hideModal } = require('../../../actions')
const mapStateToProps = state => {
const { appState: { modal: { modalState: { props } } } } = state
const { onHide } = props
return {
onHide,
}
}
const mapDispatchToProps = dispatch => {
return {
hideModal: () => dispatch(hideModal()),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { onHide, ...otherStateProps } = stateProps
const { hideModal, ...otherDispatchProps } = dispatchProps
return {
...otherStateProps,
...otherDispatchProps,
...ownProps,
onHide: () => {
hideModal()
if (onHide && typeof onHide === 'function') {
onHide()
}
},
}
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notification)

View File

@ -1,2 +1 @@
import TransactionConfirmed from './transaction-confirmed.component'
module.exports = TransactionConfirmed
export { default } from './transaction-confirmed.container'

View File

@ -0,0 +1,22 @@
.transaction-confirmed {
&__title {
font-size: 1.5rem;
font-weight: 500;
padding: 16px 0;
text-align: center;
}
&__description {
text-align: center;
font-size: .875rem;
}
&__content {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
}
}

View File

@ -1,24 +1,45 @@
import React from 'react'
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Modal from '../../modal'
const TransactionConfirmed = (props, context) => {
const { t } = context
export default class TransactionConfirmed extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
return (
<div className="modal-container__content">
<img src="images/check-icon.svg" />
<div className="modal-container__title">
{ `${t('confirmed')}!` }
</div>
<div className="modal-container__description">
{ t('initialTransactionConfirmed') }
</div>
</div>
)
static propTypes = {
onSubmit: PropTypes.func,
hideModal: PropTypes.func,
}
handleSubmit = () => {
const { hideModal, onSubmit } = this.props
hideModal()
if (onSubmit && typeof onSubmit === 'function') {
onSubmit()
}
}
render () {
const { t } = this.context
return (
<Modal
onSubmit={this.handleSubmit}
submitText={t('ok')}
>
<div className="transaction-confirmed__content">
<img src="images/check-icon.svg" />
<div className="transaction-confirmed__title">
{ `${t('confirmed')}!` }
</div>
<div className="transaction-confirmed__description">
{ t('initialTransactionConfirmed') }
</div>
</div>
</Modal>
)
}
}
TransactionConfirmed.contextTypes = {
t: PropTypes.func,
}
export default TransactionConfirmed

View File

@ -0,0 +1,4 @@
import TransactionConfirmed from './transaction-confirmed.component'
import withModalProps from '../../../higher-order-components/with-modal-props'
export default withModalProps(TransactionConfirmed)

View File

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

View File

@ -0,0 +1,48 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Modal from '../../modal'
import TransactionListItemDetails from '../../transaction-list-item-details'
import { hexToDecimal } from '../../../helpers/conversions.util'
export default class TransactionConfirmed extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
hideModal: PropTypes.func,
transaction: PropTypes.object,
onRetry: PropTypes.func,
showRetry: PropTypes.bool,
onCancel: PropTypes.func,
showCancel: PropTypes.bool,
}
handleSubmit = () => {
this.props.hideModal()
}
render () {
const { t } = this.context
const { transaction, onRetry, showRetry, onCancel, showCancel } = this.props
const { txParams: { nonce } = {} } = transaction
const decimalNonce = nonce && hexToDecimal(nonce)
return (
<Modal
onSubmit={this.handleSubmit}
onClose={this.handleSubmit}
submitText={t('ok')}
headerText={t('transactionWithNonce', [`#${decimalNonce}`])}
>
<TransactionListItemDetails
transaction={transaction}
onRetry={() => onRetry()}
showRetry={showRetry}
onCancel={() => onCancel()}
showCancel={showCancel}
/>
</Modal>
)
}
}

View File

@ -0,0 +1,4 @@
import TransactionDetails from './transaction-details.component'
import withModalProps from '../../../higher-order-components/with-modal-props'
export default withModalProps(TransactionDetails)

View File

@ -1,2 +1 @@
import WelcomeBeta from './welcome-beta.component'
module.exports = WelcomeBeta
export { default } from './welcome-beta.container'

View File

@ -1,18 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import Modal, { ModalContent } from '../../modal'
const TransactionConfirmed = (props, context) => {
const { t } = context
const { hideModal } = props
return (
<div className="modal-container__content">
<div className="modal-container__title">
{ `${t('uiWelcome')}` }
</div>
<div className="modal-container__description">
{ t('uiWelcomeMessage') }
</div>
</div>
<Modal
onSubmit={() => hideModal()}
submitText={t('ok')}
>
<ModalContent
title={t('uiWelcome')}
description={t('uiWelcomeMessage')}
/>
</Modal>
)
}
@ -20,4 +23,8 @@ TransactionConfirmed.contextTypes = {
t: PropTypes.func,
}
TransactionConfirmed.propTypes = {
hideModal: PropTypes.func,
}
export default TransactionConfirmed

View File

@ -0,0 +1,4 @@
import WelcomeBeta from './welcome-beta.component'
import withModalProps from '../../../higher-order-components/with-modal-props'
export default withModalProps(WelcomeBeta)

View File

@ -8,6 +8,7 @@ import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY,
} from '../../../constants/error-keys'
import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions'
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
@ -85,9 +86,9 @@ export default class ConfirmTransactionBase extends Component {
clearConfirmTransaction,
} = this.props
if (transactionStatus === 'dropped') {
if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) {
showTransactionConfirmedModal({
onHide: () => {
onSubmit: () => {
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
},

View File

@ -97,8 +97,8 @@ const mapDispatchToProps = dispatch => {
return {
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
clearSend: () => dispatch(clearSend()),
showTransactionConfirmedModal: ({ onHide }) => {
return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onHide }))
showTransactionConfirmedModal: ({ onSubmit }) => {
return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit }))
},
showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate }))

View File

@ -5,10 +5,9 @@ import sinon from 'sinon'
import TransactionAction from '../transaction-action.component'
describe('TransactionAction Component', () => {
const tOrDefault = key => key
const t = key => key
global.eth = {
getCode: sinon.stub().callsFake(address => {
console.log('CALLED')
const code = address === 'approveAddress' ? 'contract' : '0x'
return Promise.resolve(code)
}),
@ -36,7 +35,7 @@ describe('TransactionAction Component', () => {
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
/>, { context: { t }})
assert.equal(wrapper.find('.transaction-action').length, 1)
assert.equal(wrapper.text(), '--')
@ -63,7 +62,7 @@ describe('TransactionAction Component', () => {
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
/>, { context: { t }})
assert.equal(wrapper.find('.transaction-action').length, 1)
wrapper.setState({ transactionAction: 'sentEther' })
@ -102,7 +101,7 @@ describe('TransactionAction Component', () => {
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
/>, { context: { t }})
assert.equal(wrapper.find('.transaction-action').length, 1)
wrapper.setState({ transactionAction: 'approve' })

View File

@ -4,7 +4,7 @@ import { getTransactionActionKey } from '../../helpers/transactions.util'
export default class TransactionAction extends PureComponent {
static contextTypes = {
tOrDefault: PropTypes.func,
t: PropTypes.func,
}
static propTypes = {
@ -35,7 +35,7 @@ export default class TransactionAction extends PureComponent {
}
const actionKey = await getTransactionActionKey(transaction, data)
const action = actionKey && this.context.tOrDefault(actionKey)
const action = actionKey && this.context.t(actionKey)
this.setState({ transactionAction: action })
}

View File

@ -33,6 +33,10 @@
&:last-child::after {
height: 50%;
}
&:first-child:last-child::after {
display: none;
}
}
&__activity-icon {
@ -47,9 +51,12 @@
&__activity-text {
color: $scorpion;
font-size: .75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media screen and (min-width: $break-large) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__value {

View File

@ -13,7 +13,9 @@ export default class TransactionListItemDetails extends PureComponent {
}
static propTypes = {
onCancel: PropTypes.func,
onRetry: PropTypes.func,
showCancel: PropTypes.bool,
showRetry: PropTypes.bool,
transaction: PropTypes.object,
}
@ -27,6 +29,13 @@ export default class TransactionListItemDetails extends PureComponent {
this.setState({ showTransactionDetails: true })
}
handleCancel = event => {
const { onCancel } = this.props
event.stopPropagation()
onCancel()
}
handleRetry = event => {
const { onRetry } = this.props
@ -36,7 +45,7 @@ export default class TransactionListItemDetails extends PureComponent {
render () {
const { t } = this.context
const { transaction, showRetry } = this.props
const { transaction, showCancel, showRetry } = this.props
const { txParams: { to, from } = {} } = transaction
return (
@ -55,6 +64,17 @@ export default class TransactionListItemDetails extends PureComponent {
</Button>
)
}
{
showCancel && (
<Button
type="raised"
onClick={this.handleCancel}
className="transaction-list-item-details__header-button"
>
{ t('cancel') }
</Button>
)
}
<Button
type="raised"
onClick={this.handleEtherscanClick}

View File

@ -6,6 +6,7 @@
justify-content: center;
align-items: center;
flex-direction: column;
background: $white;
&__grid {
cursor: pointer;
@ -117,4 +118,14 @@
background: #f3f4f7;
width: 100%;
}
&__expander {
max-height: 0px;
width: 100%;
&--show {
max-height: 1000px;
transition: max-height 700ms ease-out;
}
}
}

View File

@ -1,5 +1,6 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Identicon from '../identicon'
import TransactionStatus from '../transaction-status'
import TransactionAction from '../transaction-action'
@ -9,20 +10,24 @@ 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'
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums'
export default class TransactionListItem extends PureComponent {
static propTypes = {
assetImages: PropTypes.object,
history: PropTypes.object,
transaction: PropTypes.object,
value: PropTypes.string,
methodData: PropTypes.object,
showRetry: PropTypes.bool,
nonceAndDate: PropTypes.string,
retryTransaction: PropTypes.func,
setSelectedToken: PropTypes.func,
nonceAndDate: PropTypes.string,
showCancelModal: PropTypes.func,
showCancel: PropTypes.bool,
showRetry: PropTypes.bool,
showTransactionDetailsModal: PropTypes.func,
token: PropTypes.object,
assetImages: PropTypes.object,
tokenData: PropTypes.object,
transaction: PropTypes.object,
value: PropTypes.string,
}
state = {
@ -30,16 +35,39 @@ export default class TransactionListItem extends PureComponent {
}
handleClick = () => {
const { transaction, history } = this.props
const {
transaction,
history,
showTransactionDetailsModal,
methodData,
showCancel,
showRetry,
} = this.props
const { id, status } = transaction
const { showTransactionDetails } = this.state
const windowType = window.METAMASK_UI_TYPE
if (status === UNAPPROVED_STATUS) {
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
return
}
this.setState({ showTransactionDetails: !showTransactionDetails })
if (windowType === ENVIRONMENT_TYPE_FULLSCREEN) {
this.setState({ showTransactionDetails: !showTransactionDetails })
} else {
showTransactionDetailsModal({
transaction,
onRetry: this.handleRetry,
showRetry: showRetry && methodData.done,
onCancel: this.handleCancel,
showCancel,
})
}
}
handleCancel = () => {
const { transaction: { id, txParams: { gasPrice } } = {}, showCancelModal } = this.props
showCancelModal(id, gasPrice)
}
handleRetry = () => {
@ -100,12 +128,13 @@ export default class TransactionListItem extends PureComponent {
render () {
const {
transaction,
methodData,
showRetry,
nonceAndDate,
assetImages,
methodData,
nonceAndDate,
showCancel,
showRetry,
tokenData,
transaction,
} = this.props
const { txParams = {} } = transaction
const { showTransactionDetails } = this.state
@ -148,17 +177,23 @@ export default class TransactionListItem extends PureComponent {
{ this.renderPrimaryCurrency() }
{ this.renderSecondaryCurrency() }
</div>
{
showTransactionDetails && (
<div className="transaction-list-item__details-container">
<TransactionListItemDetails
transaction={transaction}
showRetry={showRetry && methodData.done}
onRetry={this.handleRetry}
/>
</div>
)
}
<div className={classnames('transaction-list-item__expander', {
'transaction-list-item__expander--show': showTransactionDetails,
})}>
{
showTransactionDetails && (
<div className="transaction-list-item__details-container">
<TransactionListItemDetails
transaction={transaction}
onRetry={this.handleRetry}
showRetry={showRetry && methodData.done}
onCancel={this.handleCancel}
showCancel={showCancel}
/>
</div>
)
}
</div>
</div>
)
}

View File

@ -3,7 +3,7 @@ import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import withMethodData from '../../higher-order-components/with-method-data'
import TransactionListItem from './transaction-list-item.component'
import { setSelectedToken, retryTransaction } from '../../actions'
import { setSelectedToken, retryTransaction, showModal } from '../../actions'
import { hexToDecimal } from '../../helpers/conversions.util'
import { getTokenData } from '../../helpers/transactions.util'
import { formatDate } from '../../util'
@ -25,6 +25,19 @@ const mapDispatchToProps = dispatch => {
return {
setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
retryTransaction: transactionId => dispatch(retryTransaction(transactionId)),
showCancelModal: (transactionId, originalGasPrice) => {
return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
},
showTransactionDetailsModal: ({ transaction, onRetry, showRetry, onCancel, showCancel }) => {
return dispatch(showModal({
name: 'TRANSACTION_DETAILS',
transaction,
onRetry,
showRetry,
onCancel,
showCancel,
}))
},
}
}

View File

@ -3,6 +3,8 @@
flex-direction: column;
flex: 1;
overflow-y: hidden;
margin-top: 8px;
border-top: 1px solid $geyser;
&__completed-transactions {
display: flex;
@ -15,7 +17,7 @@
font-size: .875rem;
color: $dusty-gray;
border-bottom: 1px solid $geyser;
padding: 16px 0 8px 20px;
padding: 8px 0 8px 20px;
@media screen and (max-width: $break-small) {
padding: 8px 0 8px 16px;

View File

@ -56,7 +56,7 @@ export default class TransactionList extends PureComponent {
</div>
{
pendingTransactions.map((transaction, index) => (
this.renderTransaction(transaction, index)
this.renderTransaction(transaction, index, true)
))
}
</div>
@ -78,7 +78,7 @@ export default class TransactionList extends PureComponent {
)
}
renderTransaction (transaction, index) {
renderTransaction (transaction, index, showCancel) {
const { selectedToken, assetImages } = this.props
return transaction.key === TRANSACTION_TYPE_SHAPESHIFT
@ -92,6 +92,7 @@ export default class TransactionList extends PureComponent {
transaction={transaction}
key={transaction.id}
showRetry={this.shouldShowRetry(transaction)}
showCancel={showCancel}
token={selectedToken}
assetImages={assetImages}
/>

View File

@ -104,7 +104,7 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
if (prevTx && prevTx.status === 'dropped') {
this.props.dispatch(actions.showModal({
name: 'TRANSACTION_CONFIRMED',
onHide: () => history.push(DEFAULT_ROUTE),
onSubmit: () => history.push(DEFAULT_ROUTE),
}))
return

View File

@ -18,5 +18,6 @@ export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
export const UNKNOWN_FUNCTION_KEY = 'unknownFunction'
export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'

View File

@ -1,6 +1,11 @@
import ethUtil from 'ethereumjs-util'
import { conversionUtil } from '../conversion-util'
import { ETH, GWEI, WEI } from '../constants/common'
export function bnToHex (inputBn) {
return ethUtil.addHexPrefix(inputBn.toString(16))
}
export function hexToDecimal (hexValue) {
return conversionUtil(hexValue, {
fromNumericBase: 'hex',
@ -8,6 +13,13 @@ export function hexToDecimal (hexValue) {
})
}
export function decimalToHex (decimal) {
return conversionUtil(decimal, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
})
}
export function getEthConversionFromWeiHex ({ value, conversionRate, numberOfDecimals = 6 }) {
const denominations = [ETH, GWEI, WEI]

View File

@ -14,6 +14,7 @@ import {
TRANSFER_FROM_ACTION_KEY,
SIGNATURE_REQUEST_KEY,
UNKNOWN_FUNCTION_KEY,
CANCEL_ATTEMPT_ACTION_KEY,
} from '../constants/transactions'
import { addCurrencies } from '../conversion-util'
@ -44,7 +45,11 @@ export function isConfirmDeployContract (txData = {}) {
}
export async function getTransactionActionKey (transaction, methodData) {
const { txParams: { data, to } = {}, msgParams } = transaction
const { txParams: { data, to } = {}, msgParams, type } = transaction
if (type === 'cancel') {
return CANCEL_ATTEMPT_ACTION_KEY
}
if (msgParams) {
return SIGNATURE_REQUEST_KEY

View File

@ -0,0 +1 @@
export { default } from './with-modal-props'

View File

@ -0,0 +1,43 @@
import assert from 'assert'
import configureMockStore from 'redux-mock-store'
import { mount } from 'enzyme'
import React from 'react'
import withModalProps from '../with-modal-props'
const mockState = {
appState: {
modal: {
modalState: {
props: {
prop1: 'prop1',
prop2: 2,
prop3: true,
},
},
},
},
}
describe('withModalProps', () => {
it('should return a component wrapped with modal state props', () => {
const TestComponent = props => (
<div className="test">Testing</div>
)
const WrappedComponent = withModalProps(TestComponent)
const store = configureMockStore()(mockState)
const wrapper = mount(
<WrappedComponent store={store} />
)
assert.ok(wrapper)
const testComponent = wrapper.find(TestComponent).at(0)
assert.equal(testComponent.length, 1)
assert.equal(testComponent.find('.test').text(), 'Testing')
const testComponentProps = testComponent.props()
assert.equal(testComponentProps.prop1, 'prop1')
assert.equal(testComponentProps.prop2, 2)
assert.equal(testComponentProps.prop3, true)
assert.equal(typeof testComponentProps.hideModal, 'function')
})
})

View File

@ -0,0 +1,21 @@
import { connect } from 'react-redux'
import { hideModal } from '../../actions'
const mapStateToProps = state => {
const { appState } = state
const { props: modalProps } = appState.modal.modalState
return {
...modalProps,
}
}
const mapDispatchToProps = dispatch => {
return {
hideModal: () => dispatch(hideModal()),
}
}
export default function withModalProps (Component) {
return connect(mapStateToProps, mapDispatchToProps)(Component)
}