Add togglable advanced gas controls on send and confirm screens (#6112)

* Extract advanced gas input controls to their own component

* Add advanced inline gas toggle to settings

* Add optional advanced inline gas to send send screen

* Adds optional advanced gas inputs to the confirm screen

* Add info modals for advanced gas inputs.

* Fix translation of advance gas toggle description.

* Lint and unit test fixes for inline-advanced-gas-inputs

* Increase margin above advanced options button on send screen

* Move methods from constructor to property syntax in advanced-gas-inputs.component
feature/default_network_editable
Dan J Miller 6 years ago committed by Dan Finlay
parent c28fa31250
commit 38b91f63a2
  1. 12
      app/_locales/en/messages.json
  2. 4
      ui/app/components/confirm-page-container/confirm-detail-row/index.scss
  3. 4
      ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
  4. 146
      ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
  5. 12
      ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js
  6. 1
      ui/app/components/gas-customization/advanced-gas-inputs/index.js
  7. 133
      ui/app/components/gas-customization/advanced-gas-inputs/index.scss
  8. 2
      ui/app/components/gas-customization/index.scss
  9. 34
      ui/app/components/modals/modal.js
  10. 23
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
  11. 39
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
  12. 29
      ui/app/components/pages/settings/settings-tab/settings-tab.component.js
  13. 3
      ui/app/components/pages/settings/settings-tab/settings-tab.container.js
  14. 85
      ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
  15. 51
      ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
  16. 47
      ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
  17. 4
      ui/app/css/itcss/components/send.scss
  18. 3
      ui/app/ducks/confirm-transaction.duck.js
  19. 8
      ui/app/helpers/conversions.util.js
  20. 5
      ui/app/selectors.js

@ -529,6 +529,9 @@
"gasLimitCalculation": {
"message": "We calculate the suggested gas limit based on network success rates."
},
"gasLimitInfoModalContent": {
"message": "Gas limit is the maximum amount of units of gas you are willing to spend."
},
"gasLimitRequired": {
"message": "Gas Limit Required"
},
@ -547,6 +550,9 @@
"gasPriceExtremelyLow": {
"message": "Gas Price Extremely Low"
},
"gasPriceInfoModalContent": {
"message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas."
},
"gasPriceNoDenom": {
"message": "Gas Price"
},
@ -1210,6 +1216,12 @@
"shapeshiftBuy": {
"message": "Buy with Shapeshift"
},
"showAdvancedGasInline": {
"message": "Advanced gas controls"
},
"showAdvancedGasInlineDescription": {
"message": "Select this to show gas price and limit controls directly on the send and confirm screens."
},
"showPrivateKeys": {
"message": "Show Private Keys"
},

@ -43,4 +43,8 @@
font-size: .625rem;
}
}
.advanced-gas-inputs__gas-edit-rows {
margin-bottom: 16px;
}
}

@ -52,6 +52,10 @@
&__gas-fee {
border-bottom: 1px solid $geyser;
.advanced-gas-inputs__gas-edit-rows {
margin-bottom: 16px;
}
}
&__function-type {

@ -0,0 +1,146 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import debounce from 'lodash.debounce'
export default class AdvancedTabContent extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
customGasPrice: PropTypes.number,
customGasLimit: PropTypes.number,
insufficientBalance: PropTypes.bool,
customPriceIsSafe: PropTypes.bool,
isSpeedUp: PropTypes.bool,
showGasPriceInfoModal: PropTypes.func,
showGasLimitInfoModal: PropTypes.func,
}
debouncedGasLimitReset = debounce((dVal) => {
if (dVal < 21000) {
this.props.updateCustomGasLimit(21000)
}
}, 1000, { trailing: true })
onChangeGasLimit = (val) => {
this.props.updateCustomGasLimit(val)
this.debouncedGasLimitReset(val)
}
gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) {
const { t } = this.context
let errorText
let errorType
let isInError = true
if (insufficientBalance) {
errorText = t('insufficientBalance')
errorType = 'error'
} else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) {
errorText = t('zeroGasPriceOnSpeedUpError')
errorType = 'error'
} else if (labelKey === 'gasPrice' && !customPriceIsSafe) {
errorText = t('gasPriceExtremelyLow')
errorType = 'warning'
} else {
isInError = false
}
return {
isInError,
errorText,
errorType,
}
}
gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) {
const {
isInError,
errorText,
errorType,
} = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value })
return (
<div className="advanced-gas-inputs__gas-edit-row__input-wrapper">
<input
className={classnames('advanced-gas-inputs__gas-edit-row__input', {
'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error',
'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning',
})}
type="number"
value={value}
onChange={event => onChange(Number(event.target.value))}
/>
<div className={classnames('advanced-gas-inputs__gas-edit-row__input-arrows', {
'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error',
'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning',
})}>
<div className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value + 1)}><i className="fa fa-sm fa-angle-up" /></div>
<div className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value - 1)}><i className="fa fa-sm fa-angle-down" /></div>
</div>
{ isInError
? <div className={`advanced-gas-inputs__gas-edit-row__${errorType}-text`}>
{ errorText }
</div>
: null }
</div>
)
}
infoButton (onClick) {
return <i className="fa fa-info-circle" onClick={onClick} />
}
renderGasEditRow (gasInputArgs) {
return (
<div className="advanced-gas-inputs__gas-edit-row">
<div className="advanced-gas-inputs__gas-edit-row__label">
{ this.context.t(gasInputArgs.labelKey) }
{ this.infoButton(() => gasInputArgs.infoOnClick()) }
</div>
{ this.gasInput(gasInputArgs) }
</div>
)
}
render () {
const {
customGasPrice,
updateCustomGasPrice,
customGasLimit,
insufficientBalance,
customPriceIsSafe,
isSpeedUp,
showGasPriceInfoModal,
showGasLimitInfoModal,
} = this.props
return (
<div className="advanced-gas-inputs__gas-edit-rows">
{ this.renderGasEditRow({
labelKey: 'gasPrice',
value: customGasPrice,
onChange: updateCustomGasPrice,
insufficientBalance,
customPriceIsSafe,
showGWEI: true,
isSpeedUp,
infoOnClick: showGasPriceInfoModal,
}) }
{ this.renderGasEditRow({
labelKey: 'gasLimit',
value: customGasLimit,
onChange: this.onChangeGasLimit,
insufficientBalance,
customPriceIsSafe,
infoOnClick: showGasLimitInfoModal,
}) }
</div>
)
}
}

@ -0,0 +1,12 @@
import { connect } from 'react-redux'
import { showModal } from '../../../actions'
import AdvancedGasInputs from './advanced-gas-inputs.component'
const mapDispatchToProps = dispatch => {
return {
showGasPriceInfoModal: modalName => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })),
showGasLimitInfoModal: modalName => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })),
}
}
export default connect(null, mapDispatchToProps)(AdvancedGasInputs)

@ -0,0 +1 @@
export { default } from './advanced-gas-inputs.container'

@ -0,0 +1,133 @@
.advanced-gas-inputs {
&__gas-edit-rows {
display: flex;
flex-flow: row;
justify-content: space-between;
}
&__gas-edit-row {
display: flex;
flex-flow: column;
width: 47.5%;
&__label {
color: #313B5E;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
@media screen and (max-width: 576px) {
font-size: 10px;
}
.fa-info-circle {
color: $silver;
margin-left: 10px;
cursor: pointer;
}
.fa-info-circle:hover {
color: $mid-gray;
}
}
&__error-text {
font-size: 12px;
color: red;
}
&__warning-text {
font-size: 12px;
color: orange;
}
&__input-wrapper {
position: relative;
}
&__input {
border: 1px solid $dusty-gray;
border-radius: 4px;
color: $mid-gray;
font-size: 16px;
height: 24px;
width: 100%;
padding-left: 8px;
padding-top: 2px;
margin-top: 7px;
}
&__input--error {
border: 1px solid $red;
}
&__input--warning {
border: 1px solid $orange;
}
&__input-arrows {
position: absolute;
top: 7px;
right: 0px;
width: 17px;
height: 24px;
border: 1px solid #dadada;
border-top-right-radius: 4px;
display: flex;
flex-direction: column;
color: #9b9b9b;
font-size: .8em;
border-bottom-right-radius: 4px;
cursor: pointer;
&__i-wrap {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
cursor: pointer;
}
&__i-wrap:hover {
background: #4EADE7;
color: $white;
}
i:hover {
background: #4EADE7;
}
i {
font-size: 10px;
}
}
&__input-arrows--error {
border: 1px solid $red;
}
&__input-arrows--warning {
border: 1px solid $orange;
}
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
&__gwei-symbol {
position: absolute;
top: 8px;
right: 10px;
color: $dusty-gray;
}
}
}

@ -3,3 +3,5 @@
@import './gas-modal-page-container/index';
@import './gas-price-chart/index';
@import './advanced-gas-inputs/index';

@ -230,6 +230,40 @@ const MODALS = {
},
},
GAS_PRICE_INFO_MODAL: {
contents: [
h(NotifcationModal, {
header: 'gasPriceNoDenom',
message: 'gasPriceInfoModalContent',
}),
],
mobileModalStyle: {
width: '95%',
top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
},
laptopModalStyle: {
width: '449px',
top: 'calc(33% + 45px)',
},
},
GAS_LIMIT_INFO_MODAL: {
contents: [
h(NotifcationModal, {
header: 'gasLimit',
message: 'gasLimitInfoModalContent',
}),
],
mobileModalStyle: {
width: '95%',
top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh',
},
laptopModalStyle: {
width: '449px',
top: 'calc(33% + 45px)',
},
},
CONFIRM_RESET_ACCOUNT: {
contents: h(ConfirmResetAccount),
mobileModalStyle: {

@ -11,6 +11,7 @@ import {
import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions'
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../../constants/common'
import AdvancedGasInputs from '../../gas-customization/advanced-gas-inputs'
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
@ -81,6 +82,11 @@ export default class ConfirmTransactionBase extends Component {
titleComponent: PropTypes.node,
valid: PropTypes.bool,
warning: PropTypes.string,
advancedInlineGasShown: PropTypes.bool,
gasPrice: PropTypes.number,
gasLimit: PropTypes.number,
insufficientBalance: PropTypes.bool,
convertThenUpdateGasAndCalculate: PropTypes.func,
}
state = {
@ -165,6 +171,11 @@ export default class ConfirmTransactionBase extends Component {
hexTransactionFee,
hexTransactionTotal,
hideDetails,
advancedInlineGasShown,
gasPrice,
gasLimit,
insufficientBalance,
convertThenUpdateGasAndCalculate,
} = this.props
if (hideDetails) {
@ -182,6 +193,18 @@ export default class ConfirmTransactionBase extends Component {
headerTextClassName="confirm-detail-row__header-text--edit"
onHeaderClick={() => this.handleEditGas()}
/>
{advancedInlineGasShown
? <AdvancedGasInputs
updateCustomGasPrice={newGasPrice => convertThenUpdateGasAndCalculate({ gasPrice: newGasPrice, gasLimit })}
updateCustomGasLimit={newGasLimit => convertThenUpdateGasAndCalculate({ gasLimit: newGasLimit, gasPrice })}
customGasPrice={gasPrice}
customGasLimit={gasLimit}
insufficientBalance={insufficientBalance}
customPriceIsSafe={true}
isSpeedUp={false}
/>
: null
}
</div>
<div>
<ConfirmDetailRow

@ -14,11 +14,17 @@ import {
GAS_LIMIT_TOO_LOW_ERROR_KEY,
} from '../../../constants/error-keys'
import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
import { isBalanceSufficient } from '../../send/send.utils'
import {
convertGasPriceForInputs,
convertGasLimitForInputs,
decimalToHex,
decGWEIToHexWEI,
} from '../../../helpers/conversions.util'
import { isBalanceSufficient, calcGasTotal } from '../../send/send.utils'
import { conversionGreaterThan } from '../../../conversion-util'
import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
import { addressSlicer, valuesFor } from '../../../util'
import { getMetaMaskAccounts } from '../../../selectors'
import { getMetaMaskAccounts, getAdvancedInlineGasShown } from '../../../selectors'
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return {
@ -47,7 +53,13 @@ const mapStateToProps = (state, props) => {
nonce,
} = confirmTransaction
const { txParams = {}, lastGasPrice, id: transactionId } = txData
const { from: fromAddress, to: txParamsToAddress } = txParams
const {
from: fromAddress,
to: txParamsToAddress,
gasPrice,
gas: gasLimit,
value: amount,
} = txParams
const accounts = getMetaMaskAccounts(state)
const {
conversionRate,
@ -84,6 +96,13 @@ const mapStateToProps = (state, props) => {
)
const unapprovedTxCount = valuesFor(currentNetworkUnapprovedTxs).length
const insufficientBalance = !isBalanceSufficient({
amount,
gasTotal: calcGasTotal(gasLimit, gasPrice),
balance,
conversionRate,
})
return {
balance,
fromAddress,
@ -113,9 +132,13 @@ const mapStateToProps = (state, props) => {
unapprovedTxCount,
currentNetworkUnapprovedTxs,
customGas: {
gasLimit: customGasLimit || txData.gasPrice,
gasPrice: customGasPrice || txData.gasLimit,
gasLimit: customGasLimit || gasPrice,
gasPrice: customGasPrice || gasLimit,
},
advancedInlineGasShown: getAdvancedInlineGasShown(state),
gasPrice: convertGasPriceForInputs(gasPrice),
gasLimit: convertGasLimitForInputs(gasLimit),
insufficientBalance,
}
}
@ -132,6 +155,12 @@ const mapDispatchToProps = dispatch => {
updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
},
convertThenUpdateGasAndCalculate: ({ gasLimit, gasPrice }) => {
return dispatch(updateGasAndCalculate({
gasLimit: decimalToHex(gasLimit),
gasPrice: decGWEIToHexWEI(gasPrice),
}))
},
showRejectTransactionsConfirmationModal: ({ onSubmit, unapprovedTxCount }) => {
return dispatch(showModal({ name: 'REJECT_TRANSACTIONS', onSubmit, unapprovedTxCount }))
},

@ -59,6 +59,8 @@ export default class SettingsTab extends PureComponent {
nativeCurrency: PropTypes.string,
useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func,
setAdvancedInlineGasFeatureFlag: PropTypes.func,
advancedInlineGas: PropTypes.bool,
}
state = {
@ -412,6 +414,32 @@ export default class SettingsTab extends PureComponent {
)
}
renderAdvancedGasInputInline () {
const { t } = this.context
const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props
return (
<div className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{ t('showAdvancedGasInline') }</span>
<div className="settings-page__content-description">
{ t('showAdvancedGasInlineDescription') }
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<ToggleButton
value={advancedInlineGas}
onToggle={value => setAdvancedInlineGasFeatureFlag(!value)}
activeLabel=""
inactiveLabel=""
/>
</div>
</div>
</div>
)
}
renderUsePrimaryCurrencyOptions () {
const { t } = this.context
const {
@ -508,6 +536,7 @@ export default class SettingsTab extends PureComponent {
{ this.renderClearApproval() }
{ this.renderPrivacyOptIn() }
{ this.renderHexDataOptIn() }
{ this.renderAdvancedGasInputInline() }
{ this.renderBlockieOptIn() }
</div>
)

@ -25,6 +25,7 @@ const mapStateToProps = state => {
featureFlags: {
sendHexData,
privacyMode,
advancedInlineGas,
} = {},
provider = {},
currentLocale,
@ -39,6 +40,7 @@ const mapStateToProps = state => {
nativeCurrency,
useBlockie,
sendHexData,
advancedInlineGas,
privacyMode,
provider,
useNativeCurrencyAsPrimaryCurrency,
@ -54,6 +56,7 @@ const mapDispatchToProps = dispatch => {
setUseBlockie: value => dispatch(setUseBlockie(value)),
updateCurrentLocale: key => dispatch(updateCurrentLocale(key)),
setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)),
setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)),
showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
setUseNativeCurrencyAsPrimaryCurrencyPreference: value => {

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/'
import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group'
import AdvancedGasInputs from '../../../gas-customization/advanced-gas-inputs'
export default class SendGasRow extends Component {
@ -13,54 +14,94 @@ export default class SendGasRow extends Component {
gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string,
showCustomizeGasModal: PropTypes.func,
setGasPrice: PropTypes.func,
setGasLimit: PropTypes.func,
gasPriceButtonGroupProps: PropTypes.object,
gasButtonGroupShown: PropTypes.bool,
advancedInlineGasShown: PropTypes.bool,
resetGasButtons: PropTypes.func,
gasPrice: PropTypes.number,
gasLimit: PropTypes.number,
insufficientBalance: PropTypes.bool,
}
static contextTypes = {
t: PropTypes.func,
}
render () {
renderAdvancedOptionsButton () {
const { showCustomizeGasModal } = this.props
return <div className="advanced-gas-options-btn" onClick={() => showCustomizeGasModal()}>
{ this.context.t('advancedOptions') }
</div>
}
renderContent () {
const {
conversionRate,
convertedCurrency,
gasLoadingError,
gasTotal,
gasFeeError,
showCustomizeGasModal,
gasPriceButtonGroupProps,
gasButtonGroupShown,
advancedInlineGasShown,
resetGasButtons,
setGasPrice,
setGasLimit,
gasPrice,
gasLimit,
insufficientBalance,
} = this.props
const gasPriceButtonGroup = <div>
<GasPriceButtonGroup
className="gas-price-button-group--small"
showCheck={false}
{...gasPriceButtonGroupProps}
/>
{ this.renderAdvancedOptionsButton() }
</div>
const gasFeeDisplay = <GasFeeDisplay
conversionRate={conversionRate}
convertedCurrency={convertedCurrency}
gasLoadingError={gasLoadingError}
gasTotal={gasTotal}
onReset={resetGasButtons}
onClick={() => showCustomizeGasModal()}
/>
const advancedGasInputs = <div>
<AdvancedGasInputs
updateCustomGasPrice={newGasPrice => setGasPrice(newGasPrice, gasLimit)}
updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)}
customGasPrice={gasPrice}
customGasLimit={gasLimit}
insufficientBalance={insufficientBalance}
customPriceIsSafe={true}
isSpeedUp={false}
/>
{ this.renderAdvancedOptionsButton() }
</div>
if (advancedInlineGasShown) {
return advancedGasInputs
} else if (gasButtonGroupShown) {
return gasPriceButtonGroup
} else {
return gasFeeDisplay
}
}
render () {
const { gasFeeError } = this.props
return (
<SendRowWrapper
label={`${this.context.t('transactionFee')}:`}
showError={gasFeeError}
errorType={'gasFee'}
>
{gasButtonGroupShown
? <div>
<GasPriceButtonGroup
className="gas-price-button-group--small"
showCheck={false}
{...gasPriceButtonGroupProps}
/>
<div className="advanced-gas-options-btn" onClick={() => showCustomizeGasModal()}>
{ this.context.t('advancedOptions') }
</div>
</div>
: <GasFeeDisplay
conversionRate={conversionRate}
convertedCurrency={convertedCurrency}
gasLoadingError={gasLoadingError}
gasTotal={gasTotal}
onReset={resetGasButtons}
onClick={() => showCustomizeGasModal()}
/>}
{ this.renderContent() }
</SendRowWrapper>
)
}

@ -4,12 +4,24 @@ import {
getCurrentCurrency,
getGasTotal,
getGasPrice,
getGasLimit,
getSendAmount,
} from '../../send.selectors.js'
import {
isBalanceSufficient,
calcGasTotal,
} from '../../send.utils.js'
import {
getBasicGasEstimateLoadingStatus,
getRenderableEstimateDataForSmallButtonsFromGWEI,
getDefaultActiveButtonIndex,
} from '../../../../selectors/custom-gas'
import {
decGWEIToHexWEI,
decimalToHex,
convertGasPriceForInputs,
convertGasLimitForInputs,
} from '../../../../helpers/conversions.util'
import {
showGasButtonGroup,
} from '../../../../ducks/send.duck'
@ -17,19 +29,34 @@ import {
resetCustomData,
} from '../../../../ducks/gas.duck'
import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js'
import { showModal, setGasPrice } from '../../../../actions'
import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../actions'
import { getAdvancedInlineGasShown, getCurrentEthBalance } from '../../../../selectors'
import SendGasRow from './send-gas-row.component'
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow)
function mapStateToProps (state) {
const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state)
const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, getGasPrice(state))
const gasPrice = getGasPrice(state)
const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice)
const renderableGasPrice = convertGasPriceForInputs(gasPrice)
const renderableGasLimit = convertGasLimitForInputs(getGasLimit(state))
const gasTotal = getGasTotal(state)
const conversionRate = getConversionRate(state)
const balance = getCurrentEthBalance(state)
const insufficientBalance = !isBalanceSufficient({
amount: getSendAmount(state),
gasTotal,
balance,
conversionRate,
})
return {
conversionRate: getConversionRate(state),
conversionRate,
convertedCurrency: getCurrentCurrency(state),
gasTotal: getGasTotal(state),
gasTotal,
gasFeeError: gasFeeIsInError(state),
gasLoadingError: getGasLoadingError(state),
gasPriceButtonGroupProps: {
@ -39,13 +66,26 @@ function mapStateToProps (state) {
gasButtonInfo,
},
gasButtonGroupShown: getGasButtonGroupShown(state),
advancedInlineGasShown: getAdvancedInlineGasShown(state),
gasPrice: renderableGasPrice,
gasLimit: renderableGasLimit,
insufficientBalance,
}
}
function mapDispatchToProps (dispatch) {
return {
showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })),
setGasPrice: newPrice => dispatch(setGasPrice(newPrice)),
setGasPrice: (newPrice, gasLimit) => {
newPrice = decGWEIToHexWEI(newPrice)
dispatch(setGasPrice(newPrice))
dispatch(setGasTotal(calcGasTotal(gasLimit, newPrice)))
},
setGasLimit: (newLimit, gasPrice) => {
newLimit = decimalToHex(newLimit)
dispatch(setGasLimit(newLimit))
dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice)))
},
showGasButtonGroup: () => dispatch(showGasButtonGroup()),
resetCustomData: () => dispatch(resetCustomData()),
}
@ -74,5 +114,6 @@ function mergeProps (stateProps, dispatchProps, ownProps) {
dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei)
dispatchShowGasButtonGroup()
},
setGasPrice: dispatchSetGasPrice,
}
}

@ -9,6 +9,8 @@ let mergeProps
const actionSpies = {
showModal: sinon.spy(),
setGasPrice: sinon.spy(),
setGasTotal: sinon.spy(),
setGasLimit: sinon.spy(),
}
const sendDuckSpies = {
@ -28,11 +30,26 @@ proxyquire('../send-gas-row.container.js', {
return () => ({})
},
},
'../../../../selectors': {
getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`,
getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`,
},
'../../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`,
getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`,
getGasPrice: (s) => `mockGasPrice:${s}`,
getGasLimit: (s) => `mockGasLimit:${s}`,
getSendAmount: (s) => `mockSendAmount:${s}`,
},
'../../send.utils.js': {
isBalanceSufficient: ({
amount,
gasTotal,
balance,
conversionRate,
}) => `${amount}:${gasTotal}:${balance}:${conversionRate}`,
calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
},
'./send-gas-row.selectors.js': {
getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
@ -47,6 +64,12 @@ proxyquire('../send-gas-row.container.js', {
},
'../../../../ducks/send.duck': sendDuckSpies,
'../../../../ducks/gas.duck': gasDuckSpies,
'../../../../helpers/conversions.util': {
convertGasPriceForInputs: str => str + '*',
convertGasLimitForInputs: str => str + '**',
decGWEIToHexWEI: str => '0x' + str + '000',
decimalToHex: str => '0x' + str,
},
})
describe('send-gas-row container', () => {
@ -67,6 +90,10 @@ describe('send-gas-row container', () => {
gasButtonInfo: `mockGasButtonInfo:mockState`,
},
gasButtonGroupShown: `mockGetGasButtonGroupShown:mockState`,
advancedInlineGasShown: 'mockAdvancedInlineGasShown:mockState',
gasLimit: 'mockGasLimit:mockState**',
gasPrice: 'mockGasPrice:mockState*',
insufficientBalance: false,
})
})
@ -79,6 +106,7 @@ describe('send-gas-row container', () => {
beforeEach(() => {
dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
actionSpies.setGasTotal.resetHistory()
})
describe('showCustomizeGasModal()', () => {
@ -94,10 +122,23 @@ describe('send-gas-row container', () => {
describe('setGasPrice()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.setGasPrice('mockNewPrice')
assert(dispatchSpy.calledOnce)
mapDispatchToPropsObject.setGasPrice('mockNewPrice', 'mockLimit')
assert(dispatchSpy.calledTwice)
assert(actionSpies.setGasPrice.calledOnce)
assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'mockNewPrice')
assert.equal(actionSpies.setGasPrice.getCall(0).args[0], '0xmockNewPrice000')
assert(actionSpies.setGasTotal.calledOnce)
assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockLimit0xmockNewPrice000')
})
})
describe('setGasLimit()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.setGasLimit('mockNewLimit', 'mockPrice')
assert(dispatchSpy.calledTwice)
assert(actionSpies.setGasLimit.calledOnce)
assert.equal(actionSpies.setGasLimit.getCall(0).args[0], '0xmockNewLimit')
assert(actionSpies.setGasTotal.calledOnce)
assert.equal(actionSpies.setGasTotal.getCall(0).args[0], '0xmockNewLimitmockPrice')
})
})

@ -560,6 +560,7 @@
&__form-field {
flex: 1 1 auto;
min-width: 0;
max-width: 277px;
.currency-display {
color: $tundora;
@ -586,7 +587,7 @@
font-family: Roboto;
font-size: 16px;
line-height: 22px;
width: 88px;
width: 95px;
font-weight: 400;
flex: 0 0 auto;
}
@ -934,6 +935,7 @@
font-size: 14px;
color: #2f9ae0;
cursor: pointer;
margin-top: 16px;
}
.sliders-icon-container {

@ -24,6 +24,7 @@ import {
import { getSymbolAndDecimals } from '../token-util'
import { conversionUtil } from '../conversion-util'
import { addHexPrefix } from 'ethereumjs-util'
// Actions
const createActionType = action => `metamask/confirm-transaction/${action}`
@ -256,6 +257,8 @@ export function setFetchingData (isFetching) {
}
export function updateGasAndCalculate ({ gasLimit, gasPrice }) {
gasLimit = addHexPrefix(gasLimit)
gasPrice = addHexPrefix(gasPrice)
return (dispatch, getState) => {
const { confirmTransaction: { txData } } = getState()
const newTxData = {

@ -120,3 +120,11 @@ export function hexWEIToDecGWEI (decGWEI) {
toDenomination: 'GWEI',
})
}
export function convertGasPriceForInputs (gasPriceInHexWEI) {
return Number(hexWEIToDecGWEI(gasPriceInHexWEI))
}
export function convertGasLimitForInputs (gasLimitInHexWEI) {
return parseInt(gasLimitInHexWEI, 16)
}

@ -36,6 +36,7 @@ const selectors = {
getCurrentEthBalance,
getNetworkIdentifier,
isBalanceCached,
getAdvancedInlineGasShown,
}
module.exports = selectors
@ -230,3 +231,7 @@ function getTotalUnapprovedCount ({ metamask }) {
function preferencesSelector ({ metamask }) {
return metamask.preferences
}
function getAdvancedInlineGasShown (state) {
return Boolean(state.metamask.featureFlags.advancedInlineGas)
}

Loading…
Cancel
Save