Adds speed up slide-in gas customization sidebar

feature/default_network_editable
Dan Miller 6 years ago
parent 9b9a2cc2e0
commit e3f015c88f
  1. 27
      ui/app/actions.js
  2. 22
      ui/app/app.js
  3. 25
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  4. 12
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss
  5. 18
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js
  6. 4
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
  7. 57
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  8. 1
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
  9. 66
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
  10. 22
      ui/app/components/page-container/page-container.component.js
  11. 2
      ui/app/components/sidebars/index.scss
  12. 40
      ui/app/components/sidebars/sidebar-content.scss
  13. 21
      ui/app/components/sidebars/sidebar.component.js
  14. 9
      ui/app/components/sidebars/tests/sidebars-component.test.js
  15. 1
      ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
  16. 11
      ui/app/components/transaction-list-item/transaction-list-item.component.js
  17. 20
      ui/app/components/transaction-list-item/transaction-list-item.container.js
  18. 1
      ui/app/reducers/app.js
  19. 5
      ui/app/selectors.js

@ -325,6 +325,8 @@ var actions = {
clearPendingTokens, clearPendingTokens,
createCancelTransaction, createCancelTransaction,
createSpeedUpTransaction,
approveProviderRequest, approveProviderRequest,
rejectProviderRequest, rejectProviderRequest,
clearApprovedOrigins, clearApprovedOrigins,
@ -1837,6 +1839,28 @@ function createCancelTransaction (txId, customGasPrice) {
} }
} }
function createSpeedUpTransaction (txId, customGasPrice) {
log.debug('background.createSpeedUpTransaction')
let newTx
return dispatch => {
return new Promise((resolve, reject) => {
background.createSpeedUpTransaction(txId, customGasPrice, (err, newState) => {
if (err) {
dispatch(actions.displayWarning(err.message))
reject(err)
}
const { selectedAddressTxList } = newState
newTx = selectedAddressTxList[selectedAddressTxList.length - 1]
resolve(newState)
})
})
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => newTx)
}
}
// //
// config // config
// //
@ -1937,12 +1961,13 @@ function hideModal (payload) {
} }
} }
function showSidebar ({ transitionName, type }) { function showSidebar ({ transitionName, type, props }) {
return { return {
type: actions.SIDEBAR_OPEN, type: actions.SIDEBAR_OPEN,
value: { value: {
transitionName, transitionName,
type, type,
props,
}, },
} }
} }

@ -43,6 +43,10 @@ const Alert = require('./components/alert')
import AppHeader from './components/app-header' import AppHeader from './components/app-header'
import UnlockPage from './components/pages/unlock-page' import UnlockPage from './components/pages/unlock-page'
import {
submittedPendingTransactionsSelector,
} from './selectors/transactions'
// Routes // Routes
const { const {
DEFAULT_ROUTE, DEFAULT_ROUTE,
@ -106,12 +110,20 @@ class App extends Component {
currentView, currentView,
setMouseUserState, setMouseUserState,
sidebar, sidebar,
submittedPendingTransactions,
} = this.props } = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config' const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ? const loadMessage = loadingMessage || isLoadingNetwork ?
this.getConnectingLabel(loadingMessage) : null this.getConnectingLabel(loadingMessage) : null
log.debug('Main ui render function') log.debug('Main ui render function')
const {
isOpen: sidebarIsOpen,
transitionName: sidebarTransitionName,
type: sidebarType,
props: { transaction: sidebarTransaction },
} = sidebar
return ( return (
h('.flex-column.full-height', { h('.flex-column.full-height', {
className: classnames({ 'mouse-user-styles': isMouseUser }), className: classnames({ 'mouse-user-styles': isMouseUser }),
@ -139,10 +151,12 @@ class App extends Component {
// sidebar // sidebar
h(Sidebar, { h(Sidebar, {
sidebarOpen: sidebar.isOpen, sidebarOpen: sidebarIsOpen,
sidebarShouldClose: sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id),
hideSidebar: this.props.hideSidebar, hideSidebar: this.props.hideSidebar,
transitionName: sidebar.transitionName, transitionName: sidebarTransitionName,
type: sidebar.type, type: sidebarType,
sidebarProps: sidebar.props,
}), }),
// network dropdown // network dropdown
@ -254,6 +268,7 @@ App.propTypes = {
activeAddress: PropTypes.string, activeAddress: PropTypes.string,
unapprovedTxs: PropTypes.object, unapprovedTxs: PropTypes.object,
seedWords: PropTypes.string, seedWords: PropTypes.string,
submittedPendingTransactions: PropTypes.array,
unapprovedMsgCount: PropTypes.number, unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number, unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number, unapprovedTypedMessagesCount: PropTypes.number,
@ -313,6 +328,7 @@ function mapStateToProps (state) {
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized), isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
isPopup: state.metamask.isPopup, isPopup: state.metamask.isPopup,
seedWords: state.metamask.seedWords, seedWords: state.metamask.seedWords,
submittedPendingTransactions: submittedPendingTransactionsSelector(state),
unapprovedTxs, unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs, unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedMsgCount, unapprovedMsgCount,

@ -1,5 +1,6 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames'
import GasPriceChart from '../../gas-price-chart' import GasPriceChart from '../../gas-price-chart'
export default class AdvancedTabContent extends Component { export default class AdvancedTabContent extends Component {
@ -16,23 +17,31 @@ export default class AdvancedTabContent extends Component {
totalFee: PropTypes.string, totalFee: PropTypes.string,
timeRemaining: PropTypes.string, timeRemaining: PropTypes.string,
gasChartProps: PropTypes.object, gasChartProps: PropTypes.object,
insufficientBalance: PropTypes.bool,
} }
gasInput (value, onChange, min, precision, showGWEI) { gasInput (value, onChange, min, insufficientBalance, precision, showGWEI) {
return ( return (
<div className="advanced-tab__gas-edit-row__input-wrapper"> <div className="advanced-tab__gas-edit-row__input-wrapper">
<input <input
className="advanced-tab__gas-edit-row__input" className={classnames('advanced-tab__gas-edit-row__input', {
'advanced-tab__gas-edit-row__input--error': insufficientBalance,
})}
type="number" type="number"
value={value} value={value}
min={min} min={min}
precision={precision} precision={precision}
onChange={event => onChange(Number(event.target.value))} onChange={event => onChange(Number(event.target.value))}
/> />
<div className="advanced-tab__gas-edit-row__input-arrows"> <div className={classnames('advanced-tab__gas-edit-row__input-arrows', {
'advanced-tab__gas-edit-row__input-arrows--error': insufficientBalance,
})}>
<div className="advanced-tab__gas-edit-row__input-arrows__i-wrap"><i className="fa fa-sm fa-angle-up" onClick={() => onChange(value + 1)} /></div> <div className="advanced-tab__gas-edit-row__input-arrows__i-wrap"><i className="fa fa-sm fa-angle-up" onClick={() => onChange(value + 1)} /></div>
<div className="advanced-tab__gas-edit-row__input-arrows__i-wrap"><i className="fa fa-sm fa-angle-down" onClick={() => onChange(value - 1)} /></div> <div className="advanced-tab__gas-edit-row__input-arrows__i-wrap"><i className="fa fa-sm fa-angle-down" onClick={() => onChange(value - 1)} /></div>
</div> </div>
{insufficientBalance && <div className="advanced-tab__gas-edit-row__insufficient-balance">
Insufficient Balance
</div>}
</div> </div>
) )
} }
@ -70,11 +79,11 @@ export default class AdvancedTabContent extends Component {
) )
} }
renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit) { renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit, insufficientBalance) {
return ( return (
<div className="advanced-tab__gas-edit-rows"> <div className="advanced-tab__gas-edit-rows">
{ this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, 9, true) } { this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, insufficientBalance, 9, true) }
{ this.renderGasEditRow('gasLimit', customGasLimit, updateCustomGasLimit, customGasLimit, 0) } { this.renderGasEditRow('gasLimit', customGasLimit, updateCustomGasLimit, customGasLimit, insufficientBalance, 0) }
</div> </div>
) )
} }
@ -86,6 +95,7 @@ export default class AdvancedTabContent extends Component {
timeRemaining, timeRemaining,
customGasPrice, customGasPrice,
customGasLimit, customGasLimit,
insufficientBalance,
totalFee, totalFee,
gasChartProps, gasChartProps,
} = this.props } = this.props
@ -98,7 +108,8 @@ export default class AdvancedTabContent extends Component {
customGasPrice, customGasPrice,
updateCustomGasPrice, updateCustomGasPrice,
customGasLimit, customGasLimit,
updateCustomGasLimit updateCustomGasLimit,
insufficientBalance
) } ) }
<div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div> <div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div>
<GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} /> <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} />

@ -102,6 +102,11 @@
} }
} }
&__insufficient-balance {
font-size: 12px;
color: red;
}
&__input-wrapper { &__input-wrapper {
position: relative; position: relative;
@ -119,6 +124,10 @@
margin-top: 7px; margin-top: 7px;
} }
&__input--error {
border: 1px solid $red;
}
&__input-arrows { &__input-arrows {
position: absolute; position: absolute;
top: 7px; top: 7px;
@ -155,6 +164,9 @@
} }
} }
&__input-arrows--error {
border: 1px solid $red;
}
input[type="number"]::-webkit-inner-spin-button { input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;

@ -27,6 +27,7 @@ describe('AdvancedTabContent Component', function () {
customGasLimit={23456} customGasLimit={23456}
timeRemaining={21500} timeRemaining={21500}
totalFee={'$0.25'} totalFee={'$0.25'}
insufficientBalance={false}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
}) })
@ -63,11 +64,13 @@ describe('AdvancedTabContent Component', function () {
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args
assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500]) assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500])
})
it('should call renderGasEditRows with the expected params', () => {
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args
assert.deepEqual(renderGasEditRowArgs, [ assert.deepEqual(renderGasEditRowArgs, [
11, propsMethodSpies.updateCustomGasPrice, 23456, propsMethodSpies.updateCustomGasLimit, 11, propsMethodSpies.updateCustomGasPrice, 23456, propsMethodSpies.updateCustomGasLimit, false,
]) ])
}) })
}) })
@ -142,7 +145,8 @@ describe('AdvancedTabContent Component', function () {
'mockGasPrice', 'mockGasPrice',
() => 'mockUpdateCustomGasPriceReturn', () => 'mockUpdateCustomGasPriceReturn',
'mockGasLimit', 'mockGasLimit',
() => 'mockUpdateCustomGasLimitReturn' () => 'mockUpdateCustomGasLimitReturn',
false
)) ))
}) })
@ -161,10 +165,10 @@ describe('AdvancedTabContent Component', function () {
const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args
assert.equal(renderGasEditRowSpyArgs.length, 2) assert.equal(renderGasEditRowSpyArgs.length, 2)
assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [ assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [
'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', 9, true, 'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', false, 9, true,
].map(String)) ].map(String))
assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [
'gasLimit', 'mockGasLimit', () => 'mockUpdateCustomGasLimitReturn', 'mockGasLimit', 0, 'gasLimit', 'mockGasLimit', () => 'mockUpdateCustomGasLimitReturn', 'mockGasLimit', false, 0,
].map(String)) ].map(String))
}) })
}) })
@ -195,8 +199,8 @@ describe('AdvancedTabContent Component', function () {
321, 321,
value => value + 7, value => value + 7,
0, 0,
8, false,
false 8
)) ))
}) })
@ -204,7 +208,7 @@ describe('AdvancedTabContent Component', function () {
assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper')) assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper'))
}) })
it('should render an input, but not a GWEI symbol', () => { it('should render two children, including an input', () => {
assert.equal(gasInput.children().length, 2) assert.equal(gasInput.children().length, 2)
assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input'))
}) })

@ -48,6 +48,7 @@ export default class GasModalPageContainer extends Component {
newTotalFiat, newTotalFiat,
gasChartProps, gasChartProps,
currentTimeEstimate, currentTimeEstimate,
insufficientBalance,
}) { }) {
const { transactionFee } = this.props const { transactionFee } = this.props
return ( return (
@ -60,6 +61,7 @@ export default class GasModalPageContainer extends Component {
transactionFee={transactionFee} transactionFee={transactionFee}
totalFee={newTotalFiat} totalFee={newTotalFiat}
gasChartProps={gasChartProps} gasChartProps={gasChartProps}
insufficientBalance={insufficientBalance}
/> />
) )
} }
@ -139,7 +141,7 @@ export default class GasModalPageContainer extends Component {
title={this.context.t('customGas')} title={this.context.t('customGas')}
subtitle={this.context.t('customGasSubTitle')} subtitle={this.context.t('customGasSubTitle')}
tabsComponent={this.renderTabs(infoRowProps, tabProps)} tabsComponent={this.renderTabs(infoRowProps, tabProps)}
disabled={false} disabled={tabProps.insufficientBalance}
onCancel={() => cancelAndClose()} onCancel={() => cancelAndClose()}
onClose={() => cancelAndClose()} onClose={() => cancelAndClose()}
onSubmit={() => { onSubmit={() => {

@ -5,6 +5,8 @@ import {
hideModal, hideModal,
setGasLimit, setGasLimit,
setGasPrice, setGasPrice,
createSpeedUpTransaction,
hideSidebar,
} from '../../../actions' } from '../../../actions'
import { import {
setCustomGasPrice, setCustomGasPrice,
@ -22,6 +24,7 @@ import {
getCurrentCurrency, getCurrentCurrency,
conversionRateSelector as getConversionRate, conversionRateSelector as getConversionRate,
getSelectedToken, getSelectedToken,
getCurrentEthBalance,
} from '../../../selectors.js' } from '../../../selectors.js'
import { import {
formatTimeEstimate, formatTimeEstimate,
@ -34,6 +37,9 @@ import {
getEstimatedGasTimes, getEstimatedGasTimes,
getRenderableBasicEstimateData, getRenderableBasicEstimateData,
} from '../../../selectors/custom-gas' } from '../../../selectors/custom-gas'
import {
submittedPendingTransactionsSelector,
} from '../../../selectors/transactions'
import { import {
formatCurrency, formatCurrency,
} from '../../../helpers/confirm-transaction/util' } from '../../../helpers/confirm-transaction/util'
@ -48,17 +54,19 @@ import {
} from '../../../helpers/formatters' } from '../../../helpers/formatters'
import { import {
calcGasTotal, calcGasTotal,
isBalanceSufficient,
} from '../../send/send.utils' } from '../../send/send.utils'
import { addHexPrefix } from 'ethereumjs-util' import { addHexPrefix } from 'ethereumjs-util'
import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils' import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils'
const mapStateToProps = state => { const mapStateToProps = (state, ownProps) => {
const { transaction = {} } = ownProps
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) const buttonDataLoading = getBasicGasEstimateLoadingStatus(state)
const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state) const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id)
const gasTotal = calcGasTotal(currentGasLimit, currentGasPrice)
const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice
const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit
const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
const gasButtonInfo = getRenderableBasicEstimateData(state) const gasButtonInfo = getRenderableBasicEstimateData(state)
@ -74,6 +82,14 @@ const mapStateToProps = state => {
const gasPrices = getEstimatedGasPrices(state) const gasPrices = getEstimatedGasPrices(state)
const estimatedTimes = getEstimatedGasTimes(state) const estimatedTimes = getEstimatedGasTimes(state)
const balance = getCurrentEthBalance(state)
const insufficientBalance = !isBalanceSufficient({
amount: value,
gasTotal,
balance,
conversionRate,
})
return { return {
hideBasic, hideBasic,
@ -104,6 +120,9 @@ const mapStateToProps = state => {
transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal), transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal),
sendAmount: addHexWEIsToRenderableEth(value, '0x0'), sendAmount: addHexWEIsToRenderableEth(value, '0x0'),
}, },
isSpeedUp: transaction.status === 'submitted',
txId: transaction.id,
insufficientBalance,
} }
} }
@ -125,18 +144,24 @@ const mapDispatchToProps = dispatch => {
updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => { updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => {
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
}, },
createSpeedUpTransaction: (txId, gasPrice) => {
return dispatch(createSpeedUpTransaction(txId, gasPrice))
},
hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)), setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)),
hideSidebar: () => dispatch(hideSidebar()),
} }
} }
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { gasPriceButtonGroupProps, isConfirm } = stateProps const { gasPriceButtonGroupProps, isConfirm, isSpeedUp, txId } = stateProps
const { const {
updateCustomGasPrice: dispatchUpdateCustomGasPrice, updateCustomGasPrice: dispatchUpdateCustomGasPrice,
hideGasButtonGroup: dispatchHideGasButtonGroup, hideGasButtonGroup: dispatchHideGasButtonGroup,
setGasData: dispatchSetGasData, setGasData: dispatchSetGasData,
updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate,
createSpeedUpTransaction: dispatchCreateSpeedUpTransaction,
hideSidebar: dispatchHideSidebar,
...otherDispatchProps ...otherDispatchProps
} = dispatchProps } = dispatchProps
@ -144,12 +169,17 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
...stateProps, ...stateProps,
...otherDispatchProps, ...otherDispatchProps,
...ownProps, ...ownProps,
onSubmit: isConfirm onSubmit: (gasLimit, gasPrice) => {
? dispatchUpdateConfirmTxGasAndCalculate if (isConfirm) {
: (newLimit, newPrice) => { dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice)
dispatchSetGasData(newLimit, newPrice) } else if (isSpeedUp) {
dispatchCreateSpeedUpTransaction(txId, gasPrice)
dispatchHideSidebar()
} else {
dispatchSetGasData(gasLimit, gasPrice)
dispatchHideGasButtonGroup() dispatchHideGasButtonGroup()
}, }
},
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps, ...gasPriceButtonGroupProps,
handleGasPriceSelection: dispatchUpdateCustomGasPrice, handleGasPriceSelection: dispatchUpdateCustomGasPrice,
@ -171,9 +201,12 @@ function calcCustomGasLimit (customGasLimitInHex) {
return parseInt(customGasLimitInHex, 16) return parseInt(customGasLimitInHex, 16)
} }
function getTxParams (state) { function getTxParams (state, transactionId) {
const { confirmTransaction: { txData }, metamask: { send } } = state const { confirmTransaction: { txData }, metamask: { send } } = state
return txData.txParams || { const pendingTransactions = submittedPendingTransactionsSelector(state)
const pendingTransaction = pendingTransactions.find(({ id }) => id === transactionId)
const { txParams: pendingTxParams } = pendingTransaction || {}
return txData.txParams || pendingTxParams || {
from: send.from, from: send.from,
gas: send.gasLimit, gas: send.gasLimit,
gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state), gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state),

@ -68,6 +68,7 @@ describe('GasModalPageContainer Component', function () {
currentTimeEstimate={'1 min 31 sec'} currentTimeEstimate={'1 min 31 sec'}
customGasPriceInHex={'mockCustomGasPriceInHex'} customGasPriceInHex={'mockCustomGasPriceInHex'}
customGasLimitInHex={'mockCustomGasLimitInHex'} customGasLimitInHex={'mockCustomGasLimitInHex'}
insufficientBalance={false}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
}) })

@ -44,14 +44,16 @@ proxyquire('../gas-modal-page-container.container.js', {
'../../../ducks/gas.duck': gasActionSpies, '../../../ducks/gas.duck': gasActionSpies,
'../../../ducks/confirm-transaction.duck': confirmTransactionActionSpies, '../../../ducks/confirm-transaction.duck': confirmTransactionActionSpies,
'../../../ducks/send.duck': sendActionSpies, '../../../ducks/send.duck': sendActionSpies,
'../../../selectors.js': {
getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
},
}) })
describe('gas-modal-page-container container', () => { describe('gas-modal-page-container container', () => {
describe('mapStateToProps()', () => { describe('mapStateToProps()', () => {
it('should map the correct properties to props', () => { it('should map the correct properties to props', () => {
const mockState2 = { const baseMockState = {
appState: { appState: {
modal: { modal: {
modalState: { modalState: {
@ -92,9 +94,7 @@ describe('gas-modal-page-container container', () => {
}, },
}, },
} }
const result2 = mapStateToProps(mockState2) const baseExpectedResult = {
assert.deepEqual(result2, {
isConfirm: true, isConfirm: true,
customGasPrice: 4.294967295, customGasPrice: 4.294967295,
customGasLimit: 2863311530, customGasLimit: 2863311530,
@ -116,13 +116,40 @@ describe('gas-modal-page-container container', () => {
}, },
hideBasic: true, hideBasic: true,
infoRowProps: { infoRowProps: {
originalTotalFiat: '22.58', originalTotalFiat: '637.41',
originalTotalEth: '0.451569 ETH', originalTotalEth: '12.748189 ETH',
newTotalFiat: '637.41', newTotalFiat: '637.41',
newTotalEth: '12.748189 ETH', newTotalEth: '12.748189 ETH',
sendAmount: '0.45036 ETH', sendAmount: '0.45036 ETH',
transactionFee: '12.297829 ETH', transactionFee: '12.297829 ETH',
}, },
insufficientBalance: true,
isSpeedUp: false,
txId: 34,
}
const baseMockOwnProps = { transaction: { id: 34 } }
const tests = [
{ mockState: baseMockState, expectedResult: baseExpectedResult, mockOwnProps: baseMockOwnProps },
{
mockState: Object.assign({}, baseMockState, {
metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' },
}),
expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }),
mockOwnProps: baseMockOwnProps,
},
{
mockState: baseMockState,
mockOwnProps: Object.assign({}, baseMockOwnProps, {
transaction: { id: 34, status: 'submitted' },
}),
expectedResult: Object.assign({}, baseExpectedResult, { isSpeedUp: true }),
},
]
let result
tests.forEach(({ mockState, mockOwnProps, expectedResult}) => {
result = mapStateToProps(mockState, mockOwnProps)
assert.deepEqual(result, expectedResult)
}) })
}) })
@ -230,9 +257,21 @@ describe('gas-modal-page-container container', () => {
setGasData: sinon.spy(), setGasData: sinon.spy(),
updateConfirmTxGasAndCalculate: sinon.spy(), updateConfirmTxGasAndCalculate: sinon.spy(),
someOtherDispatchProp: sinon.spy(), someOtherDispatchProp: sinon.spy(),
createSpeedUpTransaction: sinon.spy(),
hideSidebar: sinon.spy(),
} }
ownProps = { someOwnProp: 123 } ownProps = { someOwnProp: 123 }
}) })
afterEach(() => {
dispatchProps.updateCustomGasPrice.resetHistory()
dispatchProps.hideGasButtonGroup.resetHistory()
dispatchProps.setGasData.resetHistory()
dispatchProps.updateConfirmTxGasAndCalculate.resetHistory()
dispatchProps.someOtherDispatchProp.resetHistory()
dispatchProps.createSpeedUpTransaction.resetHistory()
dispatchProps.hideSidebar.resetHistory()
})
it('should return the expected props when isConfirm is true', () => { it('should return the expected props when isConfirm is true', () => {
const result = mergeProps(stateProps, dispatchProps, ownProps) const result = mergeProps(stateProps, dispatchProps, ownProps)
@ -289,6 +328,19 @@ describe('gas-modal-page-container container', () => {
result.someOtherDispatchProp() result.someOtherDispatchProp()
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
}) })
it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => {
const result = mergeProps(Object.assign({}, stateProps, { isSpeedUp: true, isConfirm: false }), dispatchProps, ownProps)
result.onSubmit()
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
assert.equal(dispatchProps.setGasData.callCount, 0)
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
assert.equal(dispatchProps.createSpeedUpTransaction.callCount, 1)
assert.equal(dispatchProps.hideSidebar.callCount, 1)
})
}) })
}) })

@ -112,17 +112,19 @@ export default class PageContainer extends PureComponent {
tabs={this.renderTabs()} tabs={this.renderTabs()}
headerCloseText={headerCloseText} headerCloseText={headerCloseText}
/> />
<div className="page-container__content"> <div className="page-container__bottom">
{ this.renderContent() } <div className="page-container__content">
{ this.renderContent() }
</div>
<PageContainerFooter
onCancel={onCancel}
cancelText={cancelText}
hideCancel={hideCancel}
onSubmit={onSubmit}
submitText={submitText}
disabled={disabled}
/>
</div> </div>
<PageContainerFooter
onCancel={onCancel}
cancelText={cancelText}
hideCancel={hideCancel}
onSubmit={onSubmit}
submitText={submitText}
disabled={disabled}
/>
</div> </div>
) )
} }

@ -1,3 +1,5 @@
@import './sidebar-content';
.sidebar-right-enter { .sidebar-right-enter {
transition: transform 300ms ease-in-out; transition: transform 300ms ease-in-out;
transform: translateX(-100%); transform: translateX(-100%);

@ -0,0 +1,40 @@
.sidebar-left {
.gas-modal-page-container {
.page-container {
max-width: 100%;
}
.gas-price-chart {
margin-left: 10px;
}
.page-container__bottom {
display: flex;
flex-direction: column;
flex-flow: space-between;
height: 100%;
}
.page-container__content {
overflow-y: inherit;
}
.basic-tab-content {
height: 377px;
margin-bottom: 0px;
border-bottom: 1px solid #d2d8dd;
}
.advanced-tab__fee-chart {
height: 320px;
}
.advanced-tab__fee-chart__speed-buttons {
bottom: 77px;
}
.gas-modal-content__info-row {
height: 170px;
}
}
}

@ -3,14 +3,17 @@ import PropTypes from 'prop-types'
import ReactCSSTransitionGroup from 'react-addons-css-transition-group' import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
import WalletView from '../wallet-view' import WalletView from '../wallet-view'
import { WALLET_VIEW_SIDEBAR } from './sidebar.constants' import { WALLET_VIEW_SIDEBAR } from './sidebar.constants'
import CustomizeGas from '../gas-customization/gas-modal-page-container/'
export default class Sidebar extends Component { export default class Sidebar extends Component {
static propTypes = { static propTypes = {
sidebarOpen: PropTypes.bool, sidebarOpen: PropTypes.bool,
hideSidebar: PropTypes.func, hideSidebar: PropTypes.func,
sidebarShouldClose: PropTypes.bool,
transitionName: PropTypes.string, transitionName: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
sidebarProps: PropTypes.object,
}; };
renderOverlay () { renderOverlay () {
@ -18,19 +21,27 @@ export default class Sidebar extends Component {
} }
renderSidebarContent () { renderSidebarContent () {
const { type } = this.props const { type, sidebarProps = {} } = this.props
const { transaction = {} } = sidebarProps
switch (type) { switch (type) {
case WALLET_VIEW_SIDEBAR: case WALLET_VIEW_SIDEBAR:
return <WalletView responsiveDisplayClassname={'sidebar-right' } /> return <WalletView responsiveDisplayClassname={'sidebar-right' } />
case 'customize-gas':
return <div className={'sidebar-left'}><CustomizeGas transaction={transaction} /></div>
default: default:
return null return null
} }
} }
componentDidUpdate (prevProps) {
if (!prevProps.sidebarShouldClose && this.props.sidebarShouldClose) {
this.props.hideSidebar()
}
}
render () { render () {
const { transitionName, sidebarOpen } = this.props const { transitionName, sidebarOpen, sidebarShouldClose } = this.props
return ( return (
<div> <div>
@ -39,9 +50,9 @@ export default class Sidebar extends Component {
transitionEnterTimeout={300} transitionEnterTimeout={300}
transitionLeaveTimeout={200} transitionLeaveTimeout={200}
> >
{ sidebarOpen ? this.renderSidebarContent() : null } { sidebarOpen && !sidebarShouldClose ? this.renderSidebarContent() : null }
</ReactCSSTransitionGroup> </ReactCSSTransitionGroup>
{ sidebarOpen ? this.renderOverlay() : null } { sidebarOpen && !sidebarShouldClose ? this.renderOverlay() : null }
</div> </div>
) )
} }

@ -6,6 +6,7 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
import Sidebar from '../sidebar.component.js' import Sidebar from '../sidebar.component.js'
import WalletView from '../../wallet-view' import WalletView from '../../wallet-view'
import CustomizeGas from '../../gas-customization/gas-modal-page-container/'
const propsMethodSpies = { const propsMethodSpies = {
hideSidebar: sinon.spy(), hideSidebar: sinon.spy(),
@ -59,6 +60,14 @@ describe('Sidebar Component', function () {
assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right') assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right')
}) })
it('should render sidebar content with the correct props', () => {
wrapper.setProps({ type: 'customize-gas' })
renderSidebarContent = wrapper.instance().renderSidebarContent()
const renderedSidebarContent = shallow(renderSidebarContent)
assert(renderedSidebarContent.hasClass('sidebar-left'))
assert(renderedSidebarContent.childAt(0).is(CustomizeGas))
})
it('should not render with an unrecognized type', () => { it('should not render with an unrecognized type', () => {
wrapper.setProps({ type: 'foobar' }) wrapper.setProps({ type: 'foobar' })
renderSidebarContent = wrapper.instance().renderSidebarContent() renderSidebarContent = wrapper.instance().renderSidebarContent()

@ -26,6 +26,7 @@ export default class TransactionListItemDetails extends PureComponent {
const prefix = prefixForNetwork(metamaskNetworkId) const prefix = prefixForNetwork(metamaskNetworkId)
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
global.platform.openWindow({ url: etherscanUrl }) global.platform.openWindow({ url: etherscanUrl })
this.setState({ showTransactionDetails: true }) this.setState({ showTransactionDetails: true })
} }

@ -27,6 +27,8 @@ export default class TransactionListItem extends PureComponent {
tokenData: PropTypes.object, tokenData: PropTypes.object,
transaction: PropTypes.object, transaction: PropTypes.object,
value: PropTypes.string, value: PropTypes.string,
fetchBasicGasEstimates: PropTypes.func,
fetchGasEstimates: PropTypes.func,
} }
state = { state = {
@ -69,9 +71,12 @@ export default class TransactionListItem extends PureComponent {
} }
resubmit () { resubmit () {
const { transaction: { id }, retryTransaction, history } = this.props const { transaction, retryTransaction, fetchBasicGasEstimates, fetchGasEstimates } = this.props
return retryTransaction(id) fetchBasicGasEstimates().then(basicEstimates => {
.then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)) fetchGasEstimates(basicEstimates.blockTime)
}).then(() => {
retryTransaction(transaction)
})
} }
renderPrimaryCurrency () { renderPrimaryCurrency () {

@ -3,10 +3,16 @@ import { withRouter } from 'react-router-dom'
import { compose } from 'recompose' import { compose } from 'recompose'
import withMethodData from '../../higher-order-components/with-method-data' import withMethodData from '../../higher-order-components/with-method-data'
import TransactionListItem from './transaction-list-item.component' import TransactionListItem from './transaction-list-item.component'
import { setSelectedToken, retryTransaction, showModal } from '../../actions' import { setSelectedToken, showModal, showSidebar } from '../../actions'
import { hexToDecimal } from '../../helpers/conversions.util' import { hexToDecimal } from '../../helpers/conversions.util'
import { getTokenData } from '../../helpers/transactions.util' import { getTokenData } from '../../helpers/transactions.util'
import { formatDate } from '../../util' import { formatDate } from '../../util'
import {
fetchBasicGasEstimates,
fetchGasEstimates,
setCustomGasPrice,
setCustomGasLimit,
} from '../../ducks/gas.duck'
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps
@ -23,8 +29,18 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
retryTransaction: transactionId => dispatch(retryTransaction(transactionId)), retryTransaction: (transaction) => {
dispatch(setCustomGasPrice(transaction.txParams.gasPrice))
dispatch(setCustomGasLimit(transaction.txParams.gas))
dispatch(showSidebar({
transitionName: 'sidebar-left',
type: 'customize-gas',
props: { transaction },
}))
},
showCancelModal: (transactionId, originalGasPrice) => { showCancelModal: (transactionId, originalGasPrice) => {
return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice })) return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
}, },

@ -52,6 +52,7 @@ function reduceApp (state, action) {
isOpen: false, isOpen: false,
transitionName: '', transitionName: '',
type: '', type: '',
props: {},
}, },
alertOpen: false, alertOpen: false,
alertMessage: null, alertMessage: null,

@ -35,6 +35,7 @@ const selectors = {
getTotalUnapprovedCount, getTotalUnapprovedCount,
preferencesSelector, preferencesSelector,
getMetaMaskAccounts, getMetaMaskAccounts,
getCurrentEthBalance,
} }
module.exports = selectors module.exports = selectors
@ -137,6 +138,10 @@ function getCurrentAccountWithSendEtherInfo (state) {
return accounts.find(({ address }) => address === currentAddress) return accounts.find(({ address }) => address === currentAddress)
} }
function getCurrentEthBalance (state) {
return getCurrentAccountWithSendEtherInfo(state).balance
}
function getGasIsLoading (state) { function getGasIsLoading (state) {
return state.appState.gasIsLoading return state.appState.gasIsLoading
} }

Loading…
Cancel
Save