Merge pull request #5329 from whymarrh/confirm-features

Add batch reject to confirm screen
feature/default_network_editable
Whymarrh Whitby 6 years ago committed by GitHub
commit b11a8ca2f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      app/_locales/en/messages.json
  2. 42
      ui/app/actions.js
  3. 13
      ui/app/components/confirm-page-container/confirm-page-container.component.js
  4. 14
      ui/app/components/modals/modal.js
  5. 1
      ui/app/components/modals/reject-transactions/index.js
  6. 6
      ui/app/components/modals/reject-transactions/index.scss
  7. 45
      ui/app/components/modals/reject-transactions/reject-transactions.component.js
  8. 17
      ui/app/components/modals/reject-transactions/reject-transactions.container.js
  9. 27
      ui/app/components/page-container/index.scss
  10. 46
      ui/app/components/page-container/page-container-footer/page-container-footer.component.js
  11. 12
      ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js
  12. 25
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
  13. 22
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js

@ -128,6 +128,9 @@
"cancellationGasFee": { "cancellationGasFee": {
"message": "Cancellation Gas Fee" "message": "Cancellation Gas Fee"
}, },
"cancelN": {
"message": "Cancel all $1 transactions"
},
"classicInterface": { "classicInterface": {
"message": "Use classic interface" "message": "Use classic interface"
}, },
@ -781,6 +784,18 @@
"refundAddress": { "refundAddress": {
"message": "Your Refund Address" "message": "Your Refund Address"
}, },
"reject": {
"message": "Reject"
},
"rejectAll": {
"message": "Reject All"
},
"rejectTxsN": {
"message": "Reject $1 transactions"
},
"rejectTxsDescription": {
"message": "You are about to batch reject $1 transactions."
},
"rejected": { "rejected": {
"message": "Rejected" "message": "Rejected"
}, },

@ -167,6 +167,7 @@ var actions = {
updateTransaction, updateTransaction,
updateAndApproveTx, updateAndApproveTx,
cancelTx: cancelTx, cancelTx: cancelTx,
cancelTxs,
completedTx: completedTx, completedTx: completedTx,
txError: txError, txError: txError,
nextTx: nextTx, nextTx: nextTx,
@ -1302,6 +1303,47 @@ function cancelTx (txData) {
} }
} }
/**
* Cancels all of the given transactions
* @param {Array<object>} txDataList a list of tx data objects
* @return {function(*): Promise<void>}
*/
function cancelTxs (txDataList) {
return async (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
const txIds = txDataList.map(({id}) => id)
const cancellations = txIds.map((id) => new Promise((resolve, reject) => {
background.cancelTransaction(id, (err) => {
if (err) {
return reject(err)
}
resolve()
})
}))
await Promise.all(cancellations)
const newState = await updateMetamaskStateFromBackground()
dispatch(actions.updateMetamaskState(newState))
dispatch(actions.clearSend())
txIds.forEach((id) => {
dispatch(actions.completedTx(id))
})
dispatch(actions.hideLoadingIndication())
if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
return global.platform.closeCurrentWindow()
}
}
}
/**
* @deprecated
* @param {Array<object>} txsData
* @return {Function}
*/
function cancelAllTx (txsData) { function cancelAllTx (txsData) {
return (dispatch) => { return (dispatch) => {
txsData.forEach((txData, i) => { txsData.forEach((txData, i) => {

@ -41,7 +41,9 @@ export default class ConfirmPageContainer extends Component {
assetImage: PropTypes.string, assetImage: PropTypes.string,
summaryComponent: PropTypes.node, summaryComponent: PropTypes.node,
warning: PropTypes.string, warning: PropTypes.string,
unapprovedTxCount: PropTypes.number,
// Footer // Footer
onCancelAll: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
@ -67,10 +69,12 @@ export default class ConfirmPageContainer extends Component {
summaryComponent, summaryComponent,
detailsComponent, detailsComponent,
dataComponent, dataComponent,
onCancelAll,
onCancel, onCancel,
onSubmit, onSubmit,
identiconAddress, identiconAddress,
nonce, nonce,
unapprovedTxCount,
assetImage, assetImage,
warning, warning,
} = this.props } = this.props
@ -112,11 +116,18 @@ export default class ConfirmPageContainer extends Component {
} }
<PageContainerFooter <PageContainerFooter
onCancel={() => onCancel()} onCancel={() => onCancel()}
cancelText={this.context.t('reject')}
onSubmit={() => onSubmit()} onSubmit={() => onSubmit()}
submitText={this.context.t('confirm')} submitText={this.context.t('confirm')}
submitButtonType="confirm" submitButtonType="confirm"
disabled={disabled} disabled={disabled}
/> >
{unapprovedTxCount > 1 && (
<a onClick={() => onCancelAll()}>
{this.context.t('rejectTxsN', [unapprovedTxCount])}
</a>
)}
</PageContainerFooter>
</div> </div>
) )
} }

@ -28,6 +28,7 @@ import ConfirmCustomizeGasModal from './customize-gas'
import CancelTransaction from './cancel-transaction' import CancelTransaction from './cancel-transaction'
import WelcomeBeta from './welcome-beta' import WelcomeBeta from './welcome-beta'
import TransactionDetails from './transaction-details' import TransactionDetails from './transaction-details'
import RejectTransactions from './reject-transactions'
const modalContainerBaseStyle = { const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)', transform: 'translate3d(-50%, 0, 0px)',
@ -378,6 +379,19 @@ const MODALS = {
}, },
}, },
REJECT_TRANSACTIONS: {
contents: h(RejectTransactions),
mobileModalStyle: {
...modalContainerMobileStyle,
},
laptopModalStyle: {
...modalContainerLaptopStyle,
},
contentStyle: {
borderRadius: '8px',
},
},
DEFAULT: { DEFAULT: {
contents: [], contents: [],
mobileModalStyle: {}, mobileModalStyle: {},

@ -0,0 +1 @@
export { default } from './reject-transactions.container'

@ -0,0 +1,6 @@
.reject-transactions {
&__description {
text-align: center;
font-size: .875rem;
}
}

@ -0,0 +1,45 @@
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import Modal from '../../modal'
export default class RejectTransactionsModal extends PureComponent {
static contextTypes = {
t: PropTypes.func.isRequired,
}
static propTypes = {
onSubmit: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
unapprovedTxCount: PropTypes.number.isRequired,
}
onSubmit = async () => {
const { onSubmit, hideModal } = this.props
await onSubmit()
hideModal()
}
render () {
const { t } = this.context
const { hideModal, unapprovedTxCount } = this.props
return (
<Modal
headerText={t('rejectTxsN', [unapprovedTxCount])}
onClose={hideModal}
onSubmit={this.onSubmit}
onCancel={hideModal}
submitText={t('rejectAll')}
cancelText={t('cancel')}
submitType="secondary"
>
<div>
<div className="reject-transactions__description">
{ t('rejectTxsDescription', [unapprovedTxCount]) }
</div>
</div>
</Modal>
)
}
}

@ -0,0 +1,17 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import RejectTransactionsModal from './reject-transactions.component'
import withModalProps from '../../../higher-order-components/with-modal-props'
const mapStateToProps = (state, ownProps) => {
const { unapprovedTxCount } = ownProps
return {
unapprovedTxCount,
}
}
export default compose(
withModalProps,
connect(mapStateToProps),
)(RejectTransactionsModal)

@ -43,16 +43,39 @@
&__footer { &__footer {
display: flex; display: flex;
flex-flow: row; flex-flow: column;
justify-content: center; justify-content: center;
border-top: 1px solid $geyser; border-top: 1px solid $geyser;
padding: 16px;
flex: 0 0 auto; flex: 0 0 auto;
.btn-default, .btn-default,
.btn-confirm { .btn-confirm {
font-size: 1rem; font-size: 1rem;
} }
header {
display: flex;
flex-flow: row;
justify-content: center;
padding: 16px;
flex: 0 0 auto;
}
footer {
display: flex;
flex-flow: row;
justify-content: space-around;
padding: 0 16px 16px;
flex: 0 0 auto;
a, a:hover {
text-decoration: none;
cursor: pointer;
font-size: 0.75rem;
text-transform: uppercase;
color: #2f9ae0;
}
}
} }
&__footer-button { &__footer-button {

@ -5,6 +5,7 @@ import Button from '../../button'
export default class PageContainerFooter extends Component { export default class PageContainerFooter extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node,
onCancel: PropTypes.func, onCancel: PropTypes.func,
cancelText: PropTypes.string, cancelText: PropTypes.string,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
@ -19,6 +20,7 @@ export default class PageContainerFooter extends Component {
render () { render () {
const { const {
children,
onCancel, onCancel,
cancelText, cancelText,
onSubmit, onSubmit,
@ -30,24 +32,32 @@ export default class PageContainerFooter extends Component {
return ( return (
<div className="page-container__footer"> <div className="page-container__footer">
<Button <header>
type="default" <Button
large type="default"
className="page-container__footer-button" large
onClick={e => onCancel(e)} className="page-container__footer-button"
> onClick={e => onCancel(e)}
{ cancelText || this.context.t('cancel') } >
</Button> { cancelText || this.context.t('cancel') }
</Button>
<Button
type={submitButtonType || 'primary'} <Button
large type={submitButtonType || 'primary'}
className="page-container__footer-button" large
disabled={disabled} className="page-container__footer-button"
onClick={e => onSubmit(e)} disabled={disabled}
> onClick={e => onSubmit(e)}
{ submitText || this.context.t('next') } >
</Button> { submitText || this.context.t('next') }
</Button>
</header>
{children && (
<footer>
{children}
</footer>
)}
</div> </div>
) )

@ -25,6 +25,17 @@ describe('Page Footer', () => {
assert.equal(wrapper.find('.page-container__footer').length, 1) assert.equal(wrapper.find('.page-container__footer').length, 1)
}) })
it('should render a footer inside page-container__footer when given children', () => {
const wrapper = shallow(
<PageFooter>
<div>Works</div>
</PageFooter>,
{ context: { t: sinon.spy((k) => `[${k}]`) } }
)
assert.equal(wrapper.find('.page-container__footer footer').length, 1)
})
it('renders two button components', () => { it('renders two button components', () => {
assert.equal(wrapper.find(Button).length, 2) assert.equal(wrapper.find(Button).length, 2)
}) })
@ -65,5 +76,4 @@ describe('Page Footer', () => {
assert.equal(onSubmit.callCount, 1) assert.equal(onSubmit.callCount, 1)
}) })
}) })
}) })

@ -22,6 +22,7 @@ export default class ConfirmTransactionBase extends Component {
// Redux props // Redux props
balance: PropTypes.string, balance: PropTypes.string,
cancelTransaction: PropTypes.func, cancelTransaction: PropTypes.func,
cancelAllTransactions: PropTypes.func,
clearConfirmTransaction: PropTypes.func, clearConfirmTransaction: PropTypes.func,
clearSend: PropTypes.func, clearSend: PropTypes.func,
conversionRate: PropTypes.number, conversionRate: PropTypes.number,
@ -43,12 +44,14 @@ export default class ConfirmTransactionBase extends Component {
sendTransaction: PropTypes.func, sendTransaction: PropTypes.func,
showCustomizeGasModal: PropTypes.func, showCustomizeGasModal: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func, showTransactionConfirmedModal: PropTypes.func,
showRejectTransactionsConfirmationModal: PropTypes.func,
toAddress: PropTypes.string, toAddress: PropTypes.string,
tokenData: PropTypes.object, tokenData: PropTypes.object,
tokenProps: PropTypes.object, tokenProps: PropTypes.object,
toName: PropTypes.string, toName: PropTypes.string,
transactionStatus: PropTypes.string, transactionStatus: PropTypes.string,
txData: PropTypes.object, txData: PropTypes.object,
unapprovedTxCount: PropTypes.number,
// Component props // Component props
action: PropTypes.string, action: PropTypes.string,
contentComponent: PropTypes.node, contentComponent: PropTypes.node,
@ -249,6 +252,25 @@ export default class ConfirmTransactionBase extends Component {
onEdit({ txData, tokenData, tokenProps }) onEdit({ txData, tokenData, tokenProps })
} }
handleCancelAll () {
const {
cancelAllTransactions,
clearConfirmTransaction,
history,
showRejectTransactionsConfirmationModal,
unapprovedTxCount,
} = this.props
showRejectTransactionsConfirmationModal({
unapprovedTxCount,
async onSubmit () {
await cancelAllTransactions()
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
},
})
}
handleCancel () { handleCancel () {
const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props
@ -314,6 +336,7 @@ export default class ConfirmTransactionBase extends Component {
nonce, nonce,
assetImage, assetImage,
warning, warning,
unapprovedTxCount,
} = this.props } = this.props
const { submitting, submitError } = this.state const { submitting, submitError } = this.state
@ -337,6 +360,7 @@ export default class ConfirmTransactionBase extends Component {
dataComponent={this.renderData()} dataComponent={this.renderData()}
contentComponent={contentComponent} contentComponent={contentComponent}
nonce={nonce} nonce={nonce}
unapprovedTxCount={unapprovedTxCount}
assetImage={assetImage} assetImage={assetImage}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
errorMessage={errorMessage || submitError} errorMessage={errorMessage || submitError}
@ -344,6 +368,7 @@ export default class ConfirmTransactionBase extends Component {
warning={warning} warning={warning}
disabled={!propsValid || !valid || submitting} disabled={!propsValid || !valid || submitting}
onEdit={() => this.handleEdit()} onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()} onCancel={() => this.handleCancel()}
onSubmit={() => this.handleSubmit()} onSubmit={() => this.handleSubmit()}
/> />

@ -8,7 +8,7 @@ import {
clearConfirmTransaction, clearConfirmTransaction,
updateGasAndCalculate, updateGasAndCalculate,
} from '../../../ducks/confirm-transaction.duck' } from '../../../ducks/confirm-transaction.duck'
import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions' import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal } from '../../../actions'
import { import {
INSUFFICIENT_FUNDS_ERROR_KEY, INSUFFICIENT_FUNDS_ERROR_KEY,
GAS_LIMIT_TOO_LOW_ERROR_KEY, GAS_LIMIT_TOO_LOW_ERROR_KEY,
@ -17,7 +17,7 @@ import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
import { isBalanceSufficient } from '../../send/send.utils' import { isBalanceSufficient } from '../../send/send.utils'
import { conversionGreaterThan } from '../../../conversion-util' import { conversionGreaterThan } from '../../../conversion-util'
import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants' import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
import { addressSlicer } from '../../../util' import { addressSlicer, valuesFor } from '../../../util'
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return { return {
@ -53,6 +53,8 @@ const mapStateToProps = (state, props) => {
selectedAddress, selectedAddress,
selectedAddressTxList, selectedAddressTxList,
assetImages, assetImages,
network,
unapprovedTxs,
} = metamask } = metamask
const assetImage = assetImages[txParamsToAddress] const assetImage = assetImages[txParamsToAddress]
const { balance } = accounts[selectedAddress] const { balance } = accounts[selectedAddress]
@ -67,6 +69,12 @@ const mapStateToProps = (state, props) => {
const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList) const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
const transactionStatus = transaction ? transaction.status : '' const transactionStatus = transaction ? transaction.status : ''
const currentNetworkUnapprovedTxs = R.filter(
({ metamaskNetworkId }) => metamaskNetworkId === network,
valuesFor(unapprovedTxs),
)
const unapprovedTxCount = currentNetworkUnapprovedTxs.length
return { return {
balance, balance,
fromAddress, fromAddress,
@ -90,6 +98,8 @@ const mapStateToProps = (state, props) => {
transactionStatus, transactionStatus,
nonce, nonce,
assetImage, assetImage,
unapprovedTxs,
unapprovedTxCount,
} }
} }
@ -106,7 +116,11 @@ const mapDispatchToProps = dispatch => {
updateGasAndCalculate: ({ gasLimit, gasPrice }) => { updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
}, },
showRejectTransactionsConfirmationModal: ({ onSubmit, unapprovedTxCount }) => {
return dispatch(showModal({ name: 'REJECT_TRANSACTIONS', onSubmit, unapprovedTxCount }))
},
cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)),
sendTransaction: txData => dispatch(updateAndApproveTx(txData)), sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
} }
} }
@ -156,8 +170,9 @@ const getValidateEditGas = ({ balance, conversionRate, txData }) => {
} }
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { balance, conversionRate, txData } = stateProps const { balance, conversionRate, txData, unapprovedTxs } = stateProps
const { const {
cancelAllTransactions: dispatchCancelAllTransactions,
showCustomizeGasModal: dispatchShowCustomizeGasModal, showCustomizeGasModal: dispatchShowCustomizeGasModal,
updateGasAndCalculate: dispatchUpdateGasAndCalculate, updateGasAndCalculate: dispatchUpdateGasAndCalculate,
...otherDispatchProps ...otherDispatchProps
@ -174,6 +189,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
onSubmit: txData => dispatchUpdateGasAndCalculate(txData), onSubmit: txData => dispatchUpdateGasAndCalculate(txData),
validate: validateEditGas, validate: validateEditGas,
}), }),
cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),
} }
} }

Loading…
Cancel
Save