From 38b91f63a21d1563cf88307e280f52836df005db Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 5 Feb 2019 20:54:28 -0330 Subject: [PATCH] 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 --- app/_locales/en/messages.json | 12 ++ .../confirm-detail-row/index.scss | 4 + .../confirm-page-container-content/index.scss | 4 + .../advanced-gas-inputs.component.js | 146 ++++++++++++++++++ .../advanced-gas-inputs.container.js | 12 ++ .../advanced-gas-inputs/index.js | 1 + .../advanced-gas-inputs/index.scss | 133 ++++++++++++++++ .../components/gas-customization/index.scss | 2 + ui/app/components/modals/modal.js | 34 ++++ .../confirm-transaction-base.component.js | 23 +++ .../confirm-transaction-base.container.js | 39 ++++- .../settings-tab/settings-tab.component.js | 29 ++++ .../settings-tab/settings-tab.container.js | 3 + .../send-gas-row/send-gas-row.component.js | 85 +++++++--- .../send-gas-row/send-gas-row.container.js | 51 +++++- .../tests/send-gas-row-container.test.js | 47 +++++- ui/app/css/itcss/components/send.scss | 4 +- ui/app/ducks/confirm-transaction.duck.js | 3 + ui/app/helpers/conversions.util.js | 8 + ui/app/selectors.js | 5 + 20 files changed, 609 insertions(+), 36 deletions(-) create mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js create mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js create mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/index.js create mode 100644 ui/app/components/gas-customization/advanced-gas-inputs/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c1692ce5e..2ace2c2a8 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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" }, diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss index 580a41fde..1672ef8c6 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss @@ -43,4 +43,8 @@ font-size: .625rem; } } + + .advanced-gas-inputs__gas-edit-rows { + margin-bottom: 16px; + } } diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss index 698e624f4..78639a435 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss @@ -52,6 +52,10 @@ &__gas-fee { border-bottom: 1px solid $geyser; + + .advanced-gas-inputs__gas-edit-rows { + margin-bottom: 16px; + } } &__function-type { diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js new file mode 100644 index 000000000..f0abff478 --- /dev/null +++ b/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -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 ( +
+ onChange(Number(event.target.value))} + /> +
+
onChange(value + 1)}>
+
onChange(value - 1)}>
+
+ { isInError + ?
+ { errorText } +
+ : null } +
+ ) + } + + infoButton (onClick) { + return + } + + renderGasEditRow (gasInputArgs) { + return ( +
+
+ { this.context.t(gasInputArgs.labelKey) } + { this.infoButton(() => gasInputArgs.infoOnClick()) } +
+ { this.gasInput(gasInputArgs) } +
+ ) + } + + render () { + const { + customGasPrice, + updateCustomGasPrice, + customGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + showGasPriceInfoModal, + showGasLimitInfoModal, + } = this.props + + return ( +
+ { 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, + }) } +
+ ) + } +} diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js new file mode 100644 index 000000000..883d11c6d --- /dev/null +++ b/ui/app/components/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js @@ -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) diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/index.js b/ui/app/components/gas-customization/advanced-gas-inputs/index.js new file mode 100644 index 000000000..bd8abaa3e --- /dev/null +++ b/ui/app/components/gas-customization/advanced-gas-inputs/index.js @@ -0,0 +1 @@ +export { default } from './advanced-gas-inputs.container' diff --git a/ui/app/components/gas-customization/advanced-gas-inputs/index.scss b/ui/app/components/gas-customization/advanced-gas-inputs/index.scss new file mode 100644 index 000000000..50953cbe5 --- /dev/null +++ b/ui/app/components/gas-customization/advanced-gas-inputs/index.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/ui/app/components/gas-customization/index.scss b/ui/app/components/gas-customization/index.scss index e99d4e57f..b06c1d044 100644 --- a/ui/app/components/gas-customization/index.scss +++ b/ui/app/components/gas-customization/index.scss @@ -3,3 +3,5 @@ @import './gas-modal-page-container/index'; @import './gas-price-chart/index'; + +@import './advanced-gas-inputs/index'; diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 32c860a7b..08bf205ef 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -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: { diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index 6bc415781..8d404aaca 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -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 + ? convertThenUpdateGasAndCalculate({ gasPrice: newGasPrice, gasLimit })} + updateCustomGasLimit={newGasLimit => convertThenUpdateGasAndCalculate({ gasLimit: newGasLimit, gasPrice })} + customGasPrice={gasPrice} + customGasLimit={gasLimit} + insufficientBalance={insufficientBalance} + customPriceIsSafe={true} + isSpeedUp={false} + /> + : null + }
{ 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 })) }, diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js index ce1f72407..1c02b2507 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js @@ -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 ( +
+
+ { t('showAdvancedGasInline') } +
+ { t('showAdvancedGasInlineDescription') } +
+
+
+
+ setAdvancedInlineGasFeatureFlag(!value)} + activeLabel="" + inactiveLabel="" + /> +
+
+
+ ) + } + 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() }
) diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js index 92f645438..49da0db12 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js @@ -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 => { diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js index 8d305dd4f..50337e0bf 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js @@ -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
showCustomizeGasModal()}> + { this.context.t('advancedOptions') } +
+ } + + renderContent () { const { conversionRate, convertedCurrency, gasLoadingError, gasTotal, - gasFeeError, showCustomizeGasModal, gasPriceButtonGroupProps, gasButtonGroupShown, + advancedInlineGasShown, resetGasButtons, + setGasPrice, + setGasLimit, + gasPrice, + gasLimit, + insufficientBalance, } = this.props + const gasPriceButtonGroup =
+ + { this.renderAdvancedOptionsButton() } +
+ const gasFeeDisplay = showCustomizeGasModal()} + /> + const advancedGasInputs =
+ setGasPrice(newGasPrice, gasLimit)} + updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)} + customGasPrice={gasPrice} + customGasLimit={gasLimit} + insufficientBalance={insufficientBalance} + customPriceIsSafe={true} + isSpeedUp={false} + /> + { this.renderAdvancedOptionsButton() } +
+ + if (advancedInlineGasShown) { + return advancedGasInputs + } else if (gasButtonGroupShown) { + return gasPriceButtonGroup + } else { + return gasFeeDisplay + } + } + + render () { + const { gasFeeError } = this.props + return ( - {gasButtonGroupShown - ?
- -
showCustomizeGasModal()}> - { this.context.t('advancedOptions') } -
-
- : showCustomizeGasModal()} - />} - + { this.renderContent() }
) } diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js index 977f8ab3c..b32928b75 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js @@ -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, } } diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js index f0c82e4f7..439f2ef6a 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -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') }) }) diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 4372f275c..07ab04613 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -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 { diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js index e228d2d39..c6e2c1be3 100644 --- a/ui/app/ducks/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction.duck.js @@ -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 = { diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js index 065d67e8e..d2aaeca33 100644 --- a/ui/app/helpers/conversions.util.js +++ b/ui/app/helpers/conversions.util.js @@ -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) +} diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 6e9bf6470..976342455 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -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) +}