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

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
This commit is contained in:
Dan J Miller 2019-02-05 20:54:28 -03:30 committed by Dan Finlay
parent c28fa31250
commit 38b91f63a2
20 changed files with 609 additions and 36 deletions

View File

@ -529,6 +529,9 @@
"gasLimitCalculation": { "gasLimitCalculation": {
"message": "We calculate the suggested gas limit based on network success rates." "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": { "gasLimitRequired": {
"message": "Gas Limit Required" "message": "Gas Limit Required"
}, },
@ -547,6 +550,9 @@
"gasPriceExtremelyLow": { "gasPriceExtremelyLow": {
"message": "Gas Price Extremely Low" "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": { "gasPriceNoDenom": {
"message": "Gas Price" "message": "Gas Price"
}, },
@ -1210,6 +1216,12 @@
"shapeshiftBuy": { "shapeshiftBuy": {
"message": "Buy with Shapeshift" "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": { "showPrivateKeys": {
"message": "Show Private Keys" "message": "Show Private Keys"
}, },

View File

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

View File

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

View File

@ -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>
)
}
}

View File

@ -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)

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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: { CONFIRM_RESET_ACCOUNT: {
contents: h(ConfirmResetAccount), contents: h(ConfirmResetAccount),
mobileModalStyle: { mobileModalStyle: {

View File

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

View File

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

View File

@ -59,6 +59,8 @@ export default class SettingsTab extends PureComponent {
nativeCurrency: PropTypes.string, nativeCurrency: PropTypes.string,
useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func,
setAdvancedInlineGasFeatureFlag: PropTypes.func,
advancedInlineGas: PropTypes.bool,
} }
state = { 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 () { renderUsePrimaryCurrencyOptions () {
const { t } = this.context const { t } = this.context
const { const {
@ -508,6 +536,7 @@ export default class SettingsTab extends PureComponent {
{ this.renderClearApproval() } { this.renderClearApproval() }
{ this.renderPrivacyOptIn() } { this.renderPrivacyOptIn() }
{ this.renderHexDataOptIn() } { this.renderHexDataOptIn() }
{ this.renderAdvancedGasInputInline() }
{ this.renderBlockieOptIn() } { this.renderBlockieOptIn() }
</div> </div>
) )

View File

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

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/' import SendRowWrapper from '../send-row-wrapper/'
import GasFeeDisplay from './gas-fee-display/gas-fee-display.component' import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group' import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group'
import AdvancedGasInputs from '../../../gas-customization/advanced-gas-inputs'
export default class SendGasRow extends Component { export default class SendGasRow extends Component {
@ -13,54 +14,94 @@ export default class SendGasRow extends Component {
gasLoadingError: PropTypes.bool, gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string, gasTotal: PropTypes.string,
showCustomizeGasModal: PropTypes.func, showCustomizeGasModal: PropTypes.func,
setGasPrice: PropTypes.func,
setGasLimit: PropTypes.func,
gasPriceButtonGroupProps: PropTypes.object, gasPriceButtonGroupProps: PropTypes.object,
gasButtonGroupShown: PropTypes.bool, gasButtonGroupShown: PropTypes.bool,
advancedInlineGasShown: PropTypes.bool,
resetGasButtons: PropTypes.func, resetGasButtons: PropTypes.func,
gasPrice: PropTypes.number,
gasLimit: PropTypes.number,
insufficientBalance: PropTypes.bool,
} }
static contextTypes = { static contextTypes = {
t: PropTypes.func, 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 { const {
conversionRate, conversionRate,
convertedCurrency, convertedCurrency,
gasLoadingError, gasLoadingError,
gasTotal, gasTotal,
gasFeeError,
showCustomizeGasModal, showCustomizeGasModal,
gasPriceButtonGroupProps, gasPriceButtonGroupProps,
gasButtonGroupShown, gasButtonGroupShown,
advancedInlineGasShown,
resetGasButtons, resetGasButtons,
setGasPrice,
setGasLimit,
gasPrice,
gasLimit,
insufficientBalance,
} = this.props } = 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 ( return (
<SendRowWrapper <SendRowWrapper
label={`${this.context.t('transactionFee')}:`} label={`${this.context.t('transactionFee')}:`}
showError={gasFeeError} showError={gasFeeError}
errorType={'gasFee'} errorType={'gasFee'}
> >
{gasButtonGroupShown { this.renderContent() }
? <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()}
/>}
</SendRowWrapper> </SendRowWrapper>
) )
} }

View File

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

View File

@ -9,6 +9,8 @@ let mergeProps
const actionSpies = { const actionSpies = {
showModal: sinon.spy(), showModal: sinon.spy(),
setGasPrice: sinon.spy(), setGasPrice: sinon.spy(),
setGasTotal: sinon.spy(),
setGasLimit: sinon.spy(),
} }
const sendDuckSpies = { const sendDuckSpies = {
@ -28,11 +30,26 @@ proxyquire('../send-gas-row.container.js', {
return () => ({}) return () => ({})
}, },
}, },
'../../../../selectors': {
getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`,
getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`,
},
'../../send.selectors.js': { '../../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`, getConversionRate: (s) => `mockConversionRate:${s}`,
getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`,
getGasPrice: (s) => `mockGasPrice:${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': { './send-gas-row.selectors.js': {
getGasLoadingError: (s) => `mockGasLoadingError:${s}`, getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
@ -47,6 +64,12 @@ proxyquire('../send-gas-row.container.js', {
}, },
'../../../../ducks/send.duck': sendDuckSpies, '../../../../ducks/send.duck': sendDuckSpies,
'../../../../ducks/gas.duck': gasDuckSpies, '../../../../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', () => { describe('send-gas-row container', () => {
@ -67,6 +90,10 @@ describe('send-gas-row container', () => {
gasButtonInfo: `mockGasButtonInfo:mockState`, gasButtonInfo: `mockGasButtonInfo:mockState`,
}, },
gasButtonGroupShown: `mockGetGasButtonGroupShown: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(() => { beforeEach(() => {
dispatchSpy = sinon.spy() dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
actionSpies.setGasTotal.resetHistory()
}) })
describe('showCustomizeGasModal()', () => { describe('showCustomizeGasModal()', () => {
@ -94,10 +122,23 @@ describe('send-gas-row container', () => {
describe('setGasPrice()', () => { describe('setGasPrice()', () => {
it('should dispatch an action', () => { it('should dispatch an action', () => {
mapDispatchToPropsObject.setGasPrice('mockNewPrice') mapDispatchToPropsObject.setGasPrice('mockNewPrice', 'mockLimit')
assert(dispatchSpy.calledOnce) assert(dispatchSpy.calledTwice)
assert(actionSpies.setGasPrice.calledOnce) 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')
}) })
}) })

View File

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

View File

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

View File

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

View File

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