Merge pull request #9644 from MetaMask/Version-v8.1.2

Version v8.1.2 RC
feature/default_network_editable
Mark Stacey 4 years ago committed by GitHub
commit 074ed8d303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      CHANGELOG.md
  2. 20
      app/_locales/en/messages.json
  3. 2
      app/manifest/_base.json
  4. 27
      app/scripts/lib/account-tracker.js
  5. 2
      app/scripts/metamask-controller.js
  6. 15
      ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
  7. 3
      ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js
  8. 3
      ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  9. 5
      ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
  10. 8
      ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  11. 2
      ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
  12. 18
      ui/app/components/app/modals/qr-scanner/qr-scanner.component.js
  13. 3
      ui/app/components/app/transaction-list-item/transaction-list-item.component.js
  14. 19
      ui/app/components/ui/info-tooltip/index.scss
  15. 12
      ui/app/components/ui/info-tooltip/info-tooltip.js
  16. 1
      ui/app/hooks/tests/useTransactionDisplayData.test.js
  17. 3
      ui/app/hooks/useTransactionDisplayData.js
  18. 18
      ui/app/hooks/useTransactionTimeRemaining.js
  19. 13
      ui/app/pages/swaps/awaiting-swap/awaiting-swap.js
  20. 30
      ui/app/pages/swaps/fee-card/fee-card.js
  21. 51
      ui/app/pages/swaps/fee-card/fee-card.stories.js
  22. 26
      ui/app/pages/swaps/fee-card/index.scss
  23. 5
      ui/app/pages/swaps/main-quote-summary/index.scss
  24. 12
      ui/app/pages/swaps/main-quote-summary/quote-backdrop.js
  25. 4
      ui/app/pages/swaps/swaps.util.js
  26. 27
      ui/app/pages/swaps/view-quote/index.scss
  27. 18
      ui/app/pages/swaps/view-quote/view-quote.js
  28. 2
      ui/app/pages/unlock-page/unlock-page.component.js
  29. 31
      ui/app/selectors/custom-gas.js
  30. 7
      ui/app/selectors/tests/custom-gas.test.js

@ -2,6 +2,14 @@
## Current Develop Branch ## Current Develop Branch
## 8.1.2 Mon Oct 19 2020
- [#9608](https://github.com/MetaMask/metamask-extension/pull/9608): Ensure QR code scanner works
- [#9624](https://github.com/MetaMask/metamask-extension/pull/9624): Help users avoid insufficient gas prices in swaps
- [#9614](https://github.com/MetaMask/metamask-extension/pull/9614): Update swaps network fee tooltip
- [#9623](https://github.com/MetaMask/metamask-extension/pull/9623): Prevent reducing the gas limit for swaps
- [#9630](https://github.com/MetaMask/metamask-extension/pull/9630): Fix UI crash when trying to render estimated time remaining of non-submitted transaction
- [#9633](https://github.com/MetaMask/metamask-extension/pull/9633): Update View Quote page to better represent the MetaMask fee
## 8.1.1 Tue Oct 13 2020 ## 8.1.1 Tue Oct 13 2020
- [#9586](https://github.com/MetaMask/metamask-extension/pull/9586): Prevent build quote crash when swapping from non-tracked token with balance (#9586) - [#9586](https://github.com/MetaMask/metamask-extension/pull/9586): Prevent build quote crash when swapping from non-tracked token with balance (#9586)
- [#9592](https://github.com/MetaMask/metamask-extension/pull/9592): Remove commitment to maintain a public metrics dashboard (#9592) - [#9592](https://github.com/MetaMask/metamask-extension/pull/9592): Remove commitment to maintain a public metrics dashboard (#9592)

@ -708,6 +708,10 @@
"gasLimitTooLow": { "gasLimitTooLow": {
"message": "Gas limit must be at least 21000" "message": "Gas limit must be at least 21000"
}, },
"gasLimitTooLowWithDynamicFee": {
"message": "Gas limit must be at least $1",
"description": "$1 is the custom gas limit, in decimal."
},
"gasPrice": { "gasPrice": {
"message": "Gas Price (GWEI)" "message": "Gas Price (GWEI)"
}, },
@ -1643,6 +1647,10 @@
"swapEstimatedNetworkFee": { "swapEstimatedNetworkFee": {
"message": "Estimated network fee" "message": "Estimated network fee"
}, },
"swapEstimatedNetworkFeeSummary": {
"message": "The “$1” is what we expect the actual fee to be. The exact amount depends on network conditions.",
"description": "$1 will be the translation of swapEstimatedNetworkFee, with the font bolded"
},
"swapEstimatedNetworkFees": { "swapEstimatedNetworkFees": {
"message": "Estimated network fees" "message": "Estimated network fees"
}, },
@ -1677,6 +1685,9 @@
"swapFinalizing": { "swapFinalizing": {
"message": "Finalizing..." "message": "Finalizing..."
}, },
"swapGasFeeSummary": {
"message": "The gas fee covers the cost of processing your swap and storing it on the Ethereum network. MetaMask does not profit from this fee."
},
"swapGetQuotes": { "swapGetQuotes": {
"message": "Get quotes" "message": "Get quotes"
}, },
@ -1705,7 +1716,8 @@
"message": "Transaction may fail, max slippage too low." "message": "Transaction may fail, max slippage too low."
}, },
"swapMaxNetworkFeeInfo": { "swapMaxNetworkFeeInfo": {
"message": "The Max network fee is the most you’ll pay to complete your transaction. The max fee helps ensure your Swap has the best chance of succeeding. MetaMask does not profit from network fees." "message": "“$1” is the most you’ll spend. When the network is volatile this can be a large amount.",
"description": "$1 will be the translation of swapMaxNetworkFees, with the font bolded"
}, },
"swapMaxNetworkFees": { "swapMaxNetworkFees": {
"message": "Max network fee" "message": "Max network fee"
@ -1717,7 +1729,7 @@
"message": "MetaMask fee" "message": "MetaMask fee"
}, },
"swapMetaMaskFeeDescription": { "swapMetaMaskFeeDescription": {
"message": "A service fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.", "message": "We find the best price from the top liquidity sources, every time. A fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.",
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
}, },
"swapNQuotesAvailable": { "swapNQuotesAvailable": {
@ -1741,6 +1753,10 @@
"swapQuoteDetailsSlippageInfo": { "swapQuoteDetailsSlippageInfo": {
"message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"max slippage\" setting." "message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"max slippage\" setting."
}, },
"swapQuoteIncludesRate": {
"message": "Quote includes a $1% MetaMask fee",
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
},
"swapQuoteNofN": { "swapQuoteNofN": {
"message": "Quote $1 of $2", "message": "Quote $1 of $2",
"description": "A count of loaded quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of quotes to load." "description": "A count of loaded quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of quotes to load."

@ -68,6 +68,6 @@
"notifications" "notifications"
], ],
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "8.1.1", "version": "8.1.2",
"web_accessible_resources": ["inpage.js", "phishing.html"] "web_accessible_resources": ["inpage.js", "phishing.html"]
} }

@ -14,7 +14,7 @@ import log from 'loglevel'
import pify from 'pify' import pify from 'pify'
import Web3 from 'web3' import Web3 from 'web3'
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi' import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'
import { MAINNET_NETWORK_ID, RINKEBY_NETWORK_ID, ROPSTEN_NETWORK_ID, KOVAN_NETWORK_ID } from '../controllers/network/enums' import { MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, KOVAN_CHAIN_ID } from '../controllers/network/enums'
import { import {
SINGLE_CALL_BALANCES_ADDRESS, SINGLE_CALL_BALANCES_ADDRESS,
@ -24,8 +24,6 @@ import {
} from '../controllers/network/contract-addresses' } from '../controllers/network/contract-addresses'
import { bnToHex } from './util' import { bnToHex } from './util'
export default class AccountTracker {
/** /**
* This module is responsible for tracking any number of accounts and caching their current balances & transaction * This module is responsible for tracking any number of accounts and caching their current balances & transaction
* counts. * counts.
@ -33,7 +31,6 @@ export default class AccountTracker {
* It also tracks transaction hashes, and checks their inclusion status on each new block. * It also tracks transaction hashes, and checks their inclusion status on each new block.
* *
* @typedef {Object} AccountTracker * @typedef {Object} AccountTracker
* @param {Object} opts - Initialize various properties of the class.
* @property {Object} store The stored object containing all accounts to track, as well as the current block's gas limit. * @property {Object} store The stored object containing all accounts to track, as well as the current block's gas limit.
* @property {Object} store.accounts The accounts currently stored in this AccountTracker * @property {Object} store.accounts The accounts currently stored in this AccountTracker
* @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block * @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block
@ -44,6 +41,14 @@ export default class AccountTracker {
* @property {Object} _currentBlockNumber Reference to a property on the _blockTracker: the number (i.e. an id) of the the current block * @property {Object} _currentBlockNumber Reference to a property on the _blockTracker: the number (i.e. an id) of the the current block
* *
*/ */
export default class AccountTracker {
/**
* @param {Object} opts - Options for initializing the controller
* @param {Object} opts.provider - An EIP-1193 provider instance that uses the current global network
* @param {Object} opts.blockTracker - A block tracker, which emits events for each new block
* @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network
*/
constructor (opts = {}) { constructor (opts = {}) {
const initState = { const initState = {
accounts: {}, accounts: {},
@ -61,7 +66,7 @@ export default class AccountTracker {
}) })
// bind function for easier listener syntax // bind function for easier listener syntax
this._updateForBlock = this._updateForBlock.bind(this) this._updateForBlock = this._updateForBlock.bind(this)
this.network = opts.network this.getCurrentChainId = opts.getCurrentChainId
this.web3 = new Web3(this._provider) this.web3 = new Web3(this._provider)
} }
@ -196,22 +201,22 @@ export default class AccountTracker {
async _updateAccounts () { async _updateAccounts () {
const { accounts } = this.store.getState() const { accounts } = this.store.getState()
const addresses = Object.keys(accounts) const addresses = Object.keys(accounts)
const currentNetwork = this.network.getNetworkState() const chainId = this.getCurrentChainId()
switch (currentNetwork) { switch (chainId) {
case MAINNET_NETWORK_ID.toString(): case MAINNET_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS) await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS)
break break
case RINKEBY_NETWORK_ID.toString(): case RINKEBY_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY) await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY)
break break
case ROPSTEN_NETWORK_ID.toString(): case ROPSTEN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN) await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN)
break break
case KOVAN_NETWORK_ID.toString(): case KOVAN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN) await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN)
break break

@ -168,7 +168,7 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker = new AccountTracker({ this.accountTracker = new AccountTracker({
provider: this.provider, provider: this.provider,
blockTracker: this.blockTracker, blockTracker: this.blockTracker,
network: this.networkController, getCurrentChainId: this.networkController.getCurrentChainId.bind(this.networkController),
}) })
// start and stop polling for balances based on activeControllerConnections // start and stop polling for balances based on activeControllerConnections

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import Tooltip from '../../../ui/tooltip' import Tooltip from '../../../ui/tooltip'
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'
export default class AdvancedGasInputs extends Component { export default class AdvancedGasInputs extends Component {
static contextTypes = { static contextTypes = {
@ -18,6 +19,11 @@ export default class AdvancedGasInputs extends Component {
customPriceIsSafe: PropTypes.bool, customPriceIsSafe: PropTypes.bool,
isSpeedUp: PropTypes.bool, isSpeedUp: PropTypes.bool,
customGasLimitMessage: PropTypes.string, customGasLimitMessage: PropTypes.string,
minimumGasLimit: PropTypes.number,
}
static defaultProps = {
minimumGasLimit: Number(MIN_GAS_LIMIT_DEC),
} }
constructor (props) { constructor (props) {
@ -84,7 +90,7 @@ export default class AdvancedGasInputs extends Component {
return {} return {}
} }
gasLimitError ({ insufficientBalance, gasLimit }) { gasLimitError ({ insufficientBalance, gasLimit, minimumGasLimit }) {
const { t } = this.context const { t } = this.context
if (insufficientBalance) { if (insufficientBalance) {
@ -92,9 +98,9 @@ export default class AdvancedGasInputs extends Component {
errorText: t('insufficientBalance'), errorText: t('insufficientBalance'),
errorType: 'error', errorType: 'error',
} }
} else if (gasLimit < 21000) { } else if (gasLimit < minimumGasLimit) {
return { return {
errorText: t('gasLimitTooLow'), errorText: t('gasLimitTooLowWithDynamicFee', [minimumGasLimit]),
errorType: 'error', errorType: 'error',
} }
} }
@ -153,6 +159,7 @@ export default class AdvancedGasInputs extends Component {
customPriceIsSafe, customPriceIsSafe,
isSpeedUp, isSpeedUp,
customGasLimitMessage, customGasLimitMessage,
minimumGasLimit,
} = this.props } = this.props
const { const {
gasPrice, gasPrice,
@ -172,7 +179,7 @@ export default class AdvancedGasInputs extends Component {
const { const {
errorText: gasLimitErrorText, errorText: gasLimitErrorText,
errorType: gasLimitErrorType, errorType: gasLimitErrorType,
} = this.gasLimitError({ insufficientBalance, gasLimit }) } = this.gasLimitError({ insufficientBalance, gasLimit, minimumGasLimit })
const gasLimitErrorComponent = gasLimitErrorType ? ( const gasLimitErrorComponent = gasLimitErrorType ? (
<div className={`advanced-gas-inputs__gas-edit-row__${gasLimitErrorType}-text`}> <div className={`advanced-gas-inputs__gas-edit-row__${gasLimitErrorType}-text`}>
{ gasLimitErrorText } { gasLimitErrorText }

@ -17,6 +17,7 @@ describe('Advanced Gas Inputs', function () {
insufficientBalance: false, insufficientBalance: false,
customPriceIsSafe: true, customPriceIsSafe: true,
isSpeedUp: false, isSpeedUp: false,
minimumGasLimit: 21000,
} }
beforeEach(function () { beforeEach(function () {
@ -91,7 +92,7 @@ describe('Advanced Gas Inputs', function () {
assert.equal(renderError.length, 2) assert.equal(renderError.length, 2)
assert.equal(renderError.at(0).text(), 'zeroGasPriceOnSpeedUpError') assert.equal(renderError.at(0).text(), 'zeroGasPriceOnSpeedUpError')
assert.equal(renderError.at(1).text(), 'gasLimitTooLow') assert.equal(renderError.at(1).text(), 'gasLimitTooLowWithDynamicFee')
}) })
it('warns when custom gas price is too low', function () { it('warns when custom gas price is too low', function () {

@ -27,6 +27,7 @@ export default class AdvancedTabContent extends Component {
isSpeedUp: PropTypes.bool, isSpeedUp: PropTypes.bool,
isEthereumNetwork: PropTypes.bool, isEthereumNetwork: PropTypes.bool,
customGasLimitMessage: PropTypes.string, customGasLimitMessage: PropTypes.string,
minimumGasLimit: PropTypes.number.isRequired,
} }
renderDataSummary (transactionFee, timeRemaining) { renderDataSummary (transactionFee, timeRemaining) {
@ -67,6 +68,7 @@ export default class AdvancedTabContent extends Component {
transactionFee, transactionFee,
isEthereumNetwork, isEthereumNetwork,
customGasLimitMessage, customGasLimitMessage,
minimumGasLimit,
} = this.props } = this.props
return ( return (
@ -83,6 +85,7 @@ export default class AdvancedTabContent extends Component {
customPriceIsSafe={customPriceIsSafe} customPriceIsSafe={customPriceIsSafe}
isSpeedUp={isSpeedUp} isSpeedUp={isSpeedUp}
customGasLimitMessage={customGasLimitMessage} customGasLimitMessage={customGasLimitMessage}
minimumGasLimit={minimumGasLimit}
/> />
</div> </div>
{ isEthereumNetwork { isEthereumNetwork

@ -53,7 +53,8 @@ export default class GasModalPageContainer extends Component {
customTotalSupplement: PropTypes.string, customTotalSupplement: PropTypes.string,
isSwap: PropTypes.bool, isSwap: PropTypes.bool,
value: PropTypes.string, value: PropTypes.string,
conversionRate: PropTypes.number, conversionRate: PropTypes.string,
minimumGasLimit: PropTypes.number.isRequired,
} }
state = { state = {
@ -98,6 +99,7 @@ export default class GasModalPageContainer extends Component {
}, },
isEthereumNetwork, isEthereumNetwork,
customGasLimitMessage, customGasLimitMessage,
minimumGasLimit,
} = this.props } = this.props
return ( return (
@ -116,6 +118,7 @@ export default class GasModalPageContainer extends Component {
isSpeedUp={isSpeedUp} isSpeedUp={isSpeedUp}
isRetry={isRetry} isRetry={isRetry}
isEthereumNetwork={isEthereumNetwork} isEthereumNetwork={isEthereumNetwork}
minimumGasLimit={minimumGasLimit}
/> />
) )
} }

@ -62,6 +62,7 @@ import {
calcGasTotal, calcGasTotal,
isBalanceSufficient, isBalanceSufficient,
} from '../../../../pages/send/send.utils' } from '../../../../pages/send/send.utils'
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'
import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils' import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils'
import GasModalPageContainer from './gas-modal-page-container.component' import GasModalPageContainer from './gas-modal-page-container.component'
@ -75,6 +76,7 @@ const mapStateToProps = (state, ownProps) => {
customTotalSupplement = '', customTotalSupplement = '',
extraInfoRow = null, extraInfoRow = null,
useFastestButtons = false, useFastestButtons = false,
minimumGasLimit = Number(MIN_GAS_LIMIT_DEC),
} = modalProps || {} } = modalProps || {}
const { transaction = {} } = ownProps const { transaction = {} } = ownProps
const selectedTransaction = isSwap const selectedTransaction = isSwap
@ -158,7 +160,7 @@ const mapStateToProps = (state, ownProps) => {
newTotalFiat, newTotalFiat,
currentTimeEstimate, currentTimeEstimate,
blockTime: getBasicGasEstimateBlockTime(state), blockTime: getBasicGasEstimateBlockTime(state),
customPriceIsSafe: isCustomPriceSafe(state), customPriceIsSafe: isCustomPriceSafe(state, isSwap),
maxModeOn, maxModeOn,
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
buttonDataLoading, buttonDataLoading,
@ -202,6 +204,7 @@ const mapStateToProps = (state, ownProps) => {
conversionRate, conversionRate,
value, value,
customTotalSupplement, customTotalSupplement,
minimumGasLimit,
} }
} }
@ -264,6 +267,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
tokenBalance, tokenBalance,
customGasLimit, customGasLimit,
transaction, transaction,
minimumGasLimit,
} = stateProps } = stateProps
const { const {
hideGasButtonGroup: dispatchHideGasButtonGroup, hideGasButtonGroup: dispatchHideGasButtonGroup,
@ -333,7 +337,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
disableSave: ( disableSave: (
insufficientBalance || insufficientBalance ||
(isSpeedUp && customGasPrice === 0) || (isSpeedUp && customGasPrice === 0) ||
customGasLimit < 21000 customGasLimit < minimumGasLimit
), ),
} }
} }

@ -63,6 +63,7 @@ describe('gas-modal-page-container container', function () {
id: 34, id: 34,
}, },
extraInfoRow: { label: 'mockLabel', value: 'mockValue' }, extraInfoRow: { label: 'mockLabel', value: 'mockValue' },
minimumGasLimit: 21000,
}, },
}, },
}, },
@ -170,6 +171,7 @@ describe('gas-modal-page-container container', function () {
id: 34, id: 34,
}, },
value: '0x640000000000000', value: '0x640000000000000',
minimumGasLimit: 21000,
} }
const baseMockOwnProps = { transaction: { id: 34 } } const baseMockOwnProps = { transaction: { id: 34 } }
const tests = [ const tests = [

@ -104,15 +104,29 @@ export default class QrScanner extends Component {
componentWillUnmount () { componentWillUnmount () {
this.mounted = false this.mounted = false
clearTimeout(this.permissionChecker) clearTimeout(this.permissionChecker)
this.teardownCodeReader()
}
teardownCodeReader () {
if (this.codeReader) { if (this.codeReader) {
this.codeReader.reset() this.codeReader.reset()
this.codeReader.stop()
this.codeReader = null
} }
} }
initCamera = async () => { initCamera = async () => {
// The `decodeFromInputVideoDevice` call prompts the browser to show
// the user the camera permission request. We must then call it again
// once we receive permission so that the video displays.
// It's important to prevent this codeReader from being created twice;
// Firefox otherwise starts 2 video streams, one of which cannot be stopped
if (!this.codeReader) {
this.codeReader = new BrowserQRCodeReader() this.codeReader = new BrowserQRCodeReader()
}
try { try {
await this.codeReader.getVideoInputDevices() await this.codeReader.getVideoInputDevices()
this.checkPermissions()
const content = await this.codeReader.decodeFromInputVideoDevice(undefined, 'video') const content = await this.codeReader.decodeFromInputVideoDevice(undefined, 'video')
const result = this.parseContent(content.text) const result = this.parseContent(content.text)
if (!this.mounted) { if (!this.mounted) {
@ -162,7 +176,7 @@ export default class QrScanner extends Component {
stopAndClose = () => { stopAndClose = () => {
if (this.codeReader) { if (this.codeReader) {
this.codeReader.reset() this.teardownCodeReader()
} }
this.props.hideModal() this.props.hideModal()
} }
@ -170,7 +184,7 @@ export default class QrScanner extends Component {
tryAgain = () => { tryAgain = () => {
clearTimeout(this.permissionChecker) clearTimeout(this.permissionChecker)
if (this.codeReader) { if (this.codeReader) {
this.codeReader.reset() this.teardownCodeReader()
} }
this.setState(this.getInitialState(), () => { this.setState(this.getInitialState(), () => {
this.checkEnvironment() this.checkEnvironment()

@ -50,9 +50,10 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
displayedStatusKey, displayedStatusKey,
isPending, isPending,
senderAddress, senderAddress,
isSubmitted,
} = useTransactionDisplayData(transactionGroup) } = useTransactionDisplayData(transactionGroup)
const timeRemaining = useTransactionTimeRemaining(isPending, isEarliestNonce, submittedTime, gasPrice) const timeRemaining = useTransactionTimeRemaining(isSubmitted, isEarliestNonce, submittedTime, gasPrice)
const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST
const isApproval = category === TRANSACTION_CATEGORY_APPROVAL const isApproval = category === TRANSACTION_CATEGORY_APPROVAL

@ -5,24 +5,29 @@
} }
} }
.tippy-popper[x-placement^=top] .tippy-tooltip-info-theme [x-arrow] { .tippy-popper[x-placement^=top] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=top] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-top-color: $white; border-top-color: $white;
} }
.tippy-popper[x-placement^=right] .tippy-tooltip-info-theme [x-arrow] { .tippy-popper[x-placement^=right] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=right] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-right-color: $white; border-right-color: $white;
} }
.tippy-popper[x-placement^=left] .tippy-tooltip-info-theme [x-arrow] { .tippy-popper[x-placement^=left] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=left] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-left-color: $white; border-left-color: $white;
} }
.tippy-popper[x-placement^=bottom] .tippy-tooltip-info-theme [x-arrow] { .tippy-popper[x-placement^=bottom] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=bottom] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-bottom-color: $white; border-bottom-color: $white;
} }
.tippy-tooltip { .tippy-tooltip {
&#{&}-info-theme { &#{&}-info-theme,
&#{&}-wideInfo-theme {
background: white; background: white;
color: black; color: black;
box-shadow: 0 0 14px rgba(0, 0, 0, 0.18); box-shadow: 0 0 14px rgba(0, 0, 0, 0.18);
@ -38,4 +43,8 @@
color: $Grey-500; color: $Grey-500;
} }
} }
&#{&}-wideInfo-theme {
max-width: 260px;
}
} }

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames'
import Tooltip from '../tooltip' import Tooltip from '../tooltip'
const positionArrowClassMap = { const positionArrowClassMap = {
@ -12,17 +13,21 @@ const positionArrowClassMap = {
export default function InfoTooltip ({ export default function InfoTooltip ({
contentText = '', contentText = '',
position = '', position = '',
containerClassName,
wrapperClassName,
wide,
}) { }) {
return ( return (
<div className="info-tooltip"> <div className="info-tooltip">
<Tooltip <Tooltip
interactive interactive
position={position} position={position}
containerClassName="info-tooltip__tooltip-container" containerClassName={classnames('info-tooltip__tooltip-container', containerClassName)}
wrapperClassName={wrapperClassName}
tooltipInnerClassName="info-tooltip__tooltip-content" tooltipInnerClassName="info-tooltip__tooltip-content"
tooltipArrowClassName={positionArrowClassMap[position]} tooltipArrowClassName={positionArrowClassMap[position]}
html={contentText} html={contentText}
theme="tippy-tooltip-info" theme={wide ? 'tippy-tooltip-wideInfo' : 'tippy-tooltip-info'}
> >
<img src="images/mm-info-icon.svg" /> <img src="images/mm-info-icon.svg" />
</Tooltip> </Tooltip>
@ -33,4 +38,7 @@ export default function InfoTooltip ({
InfoTooltip.propTypes = { InfoTooltip.propTypes = {
contentText: PropTypes.string, contentText: PropTypes.string,
position: PropTypes.oneOf(['top', 'left', 'bottom', 'right']), position: PropTypes.oneOf(['top', 'left', 'bottom', 'right']),
wide: PropTypes.bool,
containerClassName: PropTypes.string,
wrapperClassName: PropTypes.string,
} }

@ -27,6 +27,7 @@ const expectedResults = [
secondaryCurrency: '-1 ETH', secondaryCurrency: '-1 ETH',
isPending: false, isPending: false,
displayedStatusKey: 'confirmed', displayedStatusKey: 'confirmed',
isSubmitted: false,
}, },
{ {
title: 'Send ETH', title: 'Send ETH',

@ -23,6 +23,7 @@ import {
TOKEN_CATEGORY_HASH, TOKEN_CATEGORY_HASH,
SWAP, SWAP,
SWAP_APPROVAL, SWAP_APPROVAL,
SUBMITTED_STATUS,
} from '../helpers/constants/transactions' } from '../helpers/constants/transactions'
import { getTokens } from '../ducks/metamask/metamask' import { getTokens } from '../ducks/metamask/metamask'
import { useI18nContext } from './useI18nContext' import { useI18nContext } from './useI18nContext'
@ -74,6 +75,7 @@ export function useTransactionDisplayData (transactionGroup) {
const displayedStatusKey = getStatusKey(primaryTransaction) const displayedStatusKey = getStatusKey(primaryTransaction)
const isPending = displayedStatusKey in PENDING_STATUS_HASH const isPending = displayedStatusKey in PENDING_STATUS_HASH
const isSubmitted = displayedStatusKey === SUBMITTED_STATUS
const primaryValue = primaryTransaction.txParams?.value const primaryValue = primaryTransaction.txParams?.value
let prefix = '-' let prefix = '-'
@ -213,5 +215,6 @@ export function useTransactionDisplayData (transactionGroup) {
) ? undefined : secondaryCurrency, ) ? undefined : secondaryCurrency,
displayedStatusKey, displayedStatusKey,
isPending, isPending,
isSubmitted,
} }
} }

@ -26,7 +26,7 @@ function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
* returns a string representing the number of minutes predicted for the transaction to be * returns a string representing the number of minutes predicted for the transaction to be
* completed. Only returns this prediction if the transaction is the earliest pending * completed. Only returns this prediction if the transaction is the earliest pending
* transaction, and the feature flag for showing timing is enabled. * transaction, and the feature flag for showing timing is enabled.
* @param {bool} isPending - is the transaction currently pending * @param {bool} isSubmitted - is the transaction currently in the 'submitted' state
* @param {bool} isEarliestNonce - is this transaction the earliest nonce in list * @param {bool} isEarliestNonce - is this transaction the earliest nonce in list
* @param {number} submittedTime - the timestamp for when the transaction was submitted * @param {number} submittedTime - the timestamp for when the transaction was submitted
* @param {number} currentGasPrice - gas price to use for calculation of time * @param {number} currentGasPrice - gas price to use for calculation of time
@ -34,7 +34,7 @@ function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
* @returns {string | undefined} i18n formatted string if applicable * @returns {string | undefined} i18n formatted string if applicable
*/ */
export function useTransactionTimeRemaining ( export function useTransactionTimeRemaining (
isPending, isSubmitted,
isEarliestNonce, isEarliestNonce,
submittedTime, submittedTime,
currentGasPrice, currentGasPrice,
@ -73,7 +73,7 @@ export function useTransactionTimeRemaining (
if ( if (
(isMainNet && (isMainNet &&
(transactionTimeFeatureActive || forceAllow)) && (transactionTimeFeatureActive || forceAllow)) &&
isPending && isSubmitted &&
isEarliestNonce && isEarliestNonce &&
!isNaN(initialTimeEstimate) !isNaN(initialTimeEstimate)
) { ) {
@ -93,10 +93,10 @@ export function useTransactionTimeRemaining (
isMainNet, isMainNet,
transactionTimeFeatureActive, transactionTimeFeatureActive,
isEarliestNonce, isEarliestNonce,
isPending,
submittedTime, submittedTime,
initialTimeEstimate, initialTimeEstimate,
forceAllow, forceAllow,
isSubmitted,
]) ])
// there are numerous checks to determine if time should be displayed. // there are numerous checks to determine if time should be displayed.
@ -104,8 +104,10 @@ export function useTransactionTimeRemaining (
// User is currently not on the mainnet // User is currently not on the mainnet
// User does not have the transactionTime feature flag enabled // User does not have the transactionTime feature flag enabled
// The transaction is not pending, or isn't the earliest nonce // The transaction is not pending, or isn't the earliest nonce
const usedFormat = dontFormat if (timeRemaining && dontFormat) {
? timeRemaining return timeRemaining
: rtf.format(timeRemaining, 'minute') } else if (timeRemaining) {
return timeRemaining ? usedFormat : undefined return rtf.format(timeRemaining, 'minute')
}
return undefined
} }

@ -19,7 +19,7 @@ import { useTransactionTimeRemaining } from '../../../hooks/useTransactionTimeRe
import { usePrevious } from '../../../hooks/usePrevious' import { usePrevious } from '../../../hooks/usePrevious'
import Mascot from '../../../components/ui/mascot' import Mascot from '../../../components/ui/mascot'
import PulseLoader from '../../../components/ui/pulse-loader' import PulseLoader from '../../../components/ui/pulse-loader'
import { getBlockExplorerUrlForTx } from '../../../helpers/utils/transactions.util' import { getBlockExplorerUrlForTx, getStatusKey } from '../../../helpers/utils/transactions.util'
import CountdownTimer from '../countdown-timer' import CountdownTimer from '../countdown-timer'
import { import {
QUOTES_EXPIRED_ERROR, QUOTES_EXPIRED_ERROR,
@ -28,6 +28,7 @@ import {
QUOTES_NOT_AVAILABLE_ERROR, QUOTES_NOT_AVAILABLE_ERROR,
OFFLINE_FOR_MAINTENANCE, OFFLINE_FOR_MAINTENANCE,
} from '../../../helpers/constants/swaps' } from '../../../helpers/constants/swaps'
import { SUBMITTED_STATUS } from '../../../helpers/constants/transactions'
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes' import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'
import { getRenderableGasFeesForQuote } from '../swaps.util' import { getRenderableGasFeesForQuote } from '../swaps.util'
@ -96,7 +97,15 @@ export default function AwaitingSwap ({
rpcPrefs, rpcPrefs,
) )
const timeRemaining = useTransactionTimeRemaining(true, true, tradeTxData?.submittedTime, usedGasPrice, true, true) const statusKey = tradeTxData && getStatusKey(tradeTxData)
const timeRemaining = useTransactionTimeRemaining(
statusKey === SUBMITTED_STATUS,
true,
tradeTxData?.submittedTime,
usedGasPrice,
true,
true,
)
const previousTimeRemaining = usePrevious(timeRemaining) const previousTimeRemaining = usePrevious(timeRemaining)
const timeRemainingIsNumber = typeof timeRemaining === 'number' && !isNaN(timeRemaining) const timeRemainingIsNumber = typeof timeRemaining === 'number' && !isNaN(timeRemaining)
const previousTimeRemainingIsNumber = typeof previousTimeRemaining === 'number' && !isNaN(previousTimeRemaining) const previousTimeRemainingIsNumber = typeof previousTimeRemaining === 'number' && !isNaN(previousTimeRemaining)

@ -22,6 +22,30 @@ export default function FeeCard ({
<div className="fee-card__row-header-text--bold"> <div className="fee-card__row-header-text--bold">
{t('swapEstimatedNetworkFee')} {t('swapEstimatedNetworkFee')}
</div> </div>
<InfoTooltip
position="top"
contentText={(
<>
<p className="fee-card__info-tooltip-paragraph">{ t('swapGasFeeSummary') }</p>
<p className="fee-card__info-tooltip-paragraph">{ t('swapEstimatedNetworkFeeSummary', [
<span className="fee-card__bold" key="fee-card-bold-1">
{ t('swapEstimatedNetworkFee') }
</span>,
]) }
</p>
<p className="fee-card__info-tooltip-paragraph">{ t('swapMaxNetworkFeeInfo', [
<span className="fee-card__bold" key="fee-card-bold-2">
{ t('swapMaxNetworkFees') }
</span>,
]) }
</p>
</>
)
}
containerClassName="fee-card__info-tooltip-content-container"
wrapperClassName="fee-card__row-label fee-card__info-tooltip-container"
wide
/>
</div> </div>
<div> <div>
<div className="fee-card__row-header-secondary--bold"> <div className="fee-card__row-header-secondary--bold">
@ -42,12 +66,6 @@ export default function FeeCard ({
<div className="fee-card__link"> <div className="fee-card__link">
{t('edit')} {t('edit')}
</div> </div>
<div className="fee-card__row-label">
<InfoTooltip
position="top"
contentText={t('swapMaxNetworkFeeInfo')}
/>
</div>
</div> </div>
<div> <div>
<div className="fee-card__row-header-secondary"> <div className="fee-card__row-header-secondary">

@ -3,6 +3,15 @@ import { action } from '@storybook/addon-actions'
import { text } from '@storybook/addon-knobs/react' import { text } from '@storybook/addon-knobs/react'
import FeeCard from './fee-card' import FeeCard from './fee-card'
const tokenApprovalTextComponent = (
<span
key="swaps-view-quote-approve-symbol-1"
className="view-quote__bold"
>
ABC
</span>
)
const containerStyle = { const containerStyle = {
width: '300px', width: '300px',
} }
@ -24,19 +33,11 @@ export const WithAllProps = () => {
fee: text('secondaryFee', '100 USD'), fee: text('secondaryFee', '100 USD'),
maxFee: text('secondaryMaxFee', '200 USD'), maxFee: text('secondaryMaxFee', '200 USD'),
})} })}
maxFeeRow={({ onFeeCardMaxRowClick={action('Clicked max fee row link')}
text: text('maxFeeText', 'Max Fee'), tokenApprovalTextComponent={tokenApprovalTextComponent}
linkText: text('maxFeeLinkText', 'Edit'), tokenApprovalSourceTokenSymbol="ABC"
tooltipText: text('maxFeeTooltipText', 'Click here to edit.'), onTokenApprovalClick={action('Clicked third row link')}
onClick: action('Clicked max fee row link'), hideTokenApprovalRow={false}
})}
thirdRow={({
text: text('thirdRowText', 'Extra Option'),
linkText: text('thirdRowLinkText', 'Click Me'),
tooltipText: text('thirdRowTooltipText', 'Something happens if you click this'),
onClick: action('Clicked third row link'),
hide: false,
})}
/> />
</div> </div>
) )
@ -55,19 +56,8 @@ export const WithoutThirdRow = () => {
fee: text('secondaryFee', '100 USD'), fee: text('secondaryFee', '100 USD'),
maxFee: text('secondaryMaxFee', '200 USD'), maxFee: text('secondaryMaxFee', '200 USD'),
})} })}
maxFeeRow={({ onFeeCardMaxRowClick={action('Clicked max fee row link')}
text: text('maxFeeText', 'Max Fee'), hideTokenApprovalRow
linkText: text('maxFeeLinkText', 'Edit'),
tooltipText: text('maxFeeTooltipText', 'Click here to edit.'),
onClick: action('Clicked max fee row link'),
})}
thirdRow={({
text: text('thirdRowText', 'Extra Option'),
linkText: text('thirdRowLinkText', 'Click Me'),
tooltipText: text('thirdRowTooltipText', 'Something happens if you click this'),
onClick: action('Clicked third row link'),
hide: true,
})}
/> />
</div> </div>
) )
@ -77,17 +67,12 @@ export const WithOnlyRequiredProps = () => {
return ( return (
<div style={containerStyle}> <div style={containerStyle}>
<FeeCard <FeeCard
feeRowText={text('feeRowText', 'Network fees')}
primaryFee={({ primaryFee={({
fee: text('primaryFee', '1 ETH'), fee: text('primaryFee', '1 ETH'),
maxFee: text('primaryMaxFee', '2 ETH'), maxFee: text('primaryMaxFee', '2 ETH'),
})} })}
maxFeeRow={({ onFeeCardMaxRowClick={action('Clicked max fee row link')}
text: text('maxFeeText', 'Max Fee'), hideTokenApprovalRow
linkText: text('maxFeeLinkText', 'Edit'),
tooltipText: text('maxFeeTooltipText', 'Click here to edit.'),
onClick: action('Clicked max fee row link'),
})}
/> />
</div> </div>
) )

@ -27,7 +27,7 @@
&__row-header-text, &__row-header-text,
&__row-header-text--bold { &__row-header-text--bold {
margin-right: 6px; margin-right: 4px;
cursor: pointer; cursor: pointer;
} }
@ -56,6 +56,21 @@
} }
} }
&__info-tooltip-container {
height: 10px;
width: 10px;
justify-content: center;
margin-top: 2px;
}
&__info-tooltip-paragraph {
margin-bottom: 8px;
}
&__info-tooltip-paragraph:last-of-type {
margin-bottom: 0;
}
&__row-fee { &__row-fee {
margin-right: 4px; margin-right: 4px;
} }
@ -91,11 +106,8 @@
color: $Grey-500; color: $Grey-500;
} }
&__row-header-secondary,
&__row-header-secondary--bold { &__row-header-secondary--bold {
margin-right: 16px;
}
&__row-header-secondary {
margin-right: 12px; margin-right: 12px;
} }
@ -109,6 +121,10 @@
&__row-header-primary--bold { &__row-header-primary--bold {
font-weight: bold; font-weight: bold;
} }
&__bold {
font-weight: bold;
}
} }
.info-tooltip { .info-tooltip {

@ -22,7 +22,7 @@
&__quote-backdrop { &__quote-backdrop {
width: 310px; width: 310px;
height: 179.15px; height: 164px;
} }
&__details { &__details {
@ -49,13 +49,14 @@
} }
&__quote-details-top { &__quote-details-top {
height: 137px; height: 94px;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 12px; padding: 12px;
padding-top: 2px;
margin-top: 4px; margin-top: 4px;
} }

@ -5,13 +5,13 @@ export default function QuotesBackdrop ({
withTopTab, withTopTab,
}) { }) {
return ( return (
<svg width="311" height="199" viewBox="25.5 29.335899353027344 311 199"fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="311" height="164" viewBox="25.5 29.335899353027344 311 164" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)"> <g filter="url(#filter0_d)">
<path d="M25.5 57.5046C25.5 53.0864 29.0817 49.5046 33.5 49.5046H328.5C332.918 49.5046 336.5 53.0864 336.5 57.5046V221.005C336.5 225.423 332.918 229.005 328.5 229.005H33.5C29.0817 229.005 25.5 225.423 25.5 221.005V57.5046Z" fill="url(#paint0_linear)" /> <path d="M25.4749 54C25.4749 49.5817 29.0566 46 33.4749 46H328.475C332.893 46 336.475 49.5817 336.475 54V185.5C336.475 189.918 332.893 193.5 328.475 193.5H33.4749C29.0566 193.5 25.4749 189.918 25.4749 185.5V54Z" fill="url(#paint0_linear)" />
{withTopTab && <path d="M121.705 34.8352C122.929 31.816 125.861 29.8406 129.119 29.8406H230.883C234.141 29.8406 237.073 31.816 238.297 34.8352L251.468 62.9263C253.601 68.1861 249.73 73.9317 244.054 73.9317H115.948C110.272 73.9317 106.401 68.1861 108.534 62.9263L121.705 34.8352Z" fill="url(#paint1_linear)" />} {withTopTab && <path d="M132.68 34.3305C133.903 31.3114 136.836 29.3359 140.094 29.3359H219.858C223.116 29.3359 226.048 31.3114 227.272 34.3305L237.443 59.4217C239.575 64.6815 235.705 70.4271 230.029 70.4271H129.922C124.247 70.4271 120.376 64.6814 122.508 59.4217L132.68 34.3305Z" fill="url(#paint1_linear)" />}
</g> </g>
<defs> <defs>
<filter id="filter0_d" x="-13.5" y="0.840576" width="389" height="277.164" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB"> <filter id="filter0_d" x="-13.5251" y="0.335938" width="389" height="242.164" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" /> <feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dy="10" /> <feOffset dy="10" />
@ -20,11 +20,11 @@ export default function QuotesBackdrop ({
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
</filter> </filter>
<linearGradient id="paint0_linear" x1="25.5" y1="94.1976" x2="342.259" y2="94.1976" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear" x1="25.4749" y1="90.693" x2="342.234" y2="90.693" gradientUnits="userSpaceOnUse">
<stop stopColor="#037DD6" /> <stop stopColor="#037DD6" />
<stop offset="0.994792" stopColor="#1098FC" /> <stop offset="0.994792" stopColor="#1098FC" />
</linearGradient> </linearGradient>
<linearGradient id="paint1_linear" x1="25.5" y1="94.1976" x2="342.259" y2="94.1976" gradientUnits="userSpaceOnUse"> <linearGradient id="paint1_linear" x1="25.4749" y1="90.693" x2="342.234" y2="90.693" gradientUnits="userSpaceOnUse">
<stop stopColor="#037DD6" /> <stop stopColor="#037DD6" />
<stop offset="0.994792" stopColor="#1098FC" /> <stop offset="0.994792" stopColor="#1098FC" />
</linearGradient> </linearGradient>

@ -275,7 +275,7 @@ export function getRenderableGasFeesForQuote (tradeGas, approveGas, gasPrice, cu
const ethFee = getValueFromWeiHex({ const ethFee = getValueFromWeiHex({
value: gasTotalInWeiHex, value: gasTotalInWeiHex,
toDenomination: 'ETH', toDenomination: 'ETH',
numberOfDecimals: 6, numberOfDecimals: 5,
}) })
const rawNetworkFees = getValueFromWeiHex({ const rawNetworkFees = getValueFromWeiHex({
value: gasTotalInWeiHex, value: gasTotalInWeiHex,
@ -315,7 +315,6 @@ export function quotesToRenderableData (quotes, gasPrice, conversionRate, curren
conversionRate, conversionRate,
) )
const metaMaskFee = `0.875%`
const slippageMultiplier = (new BigNumber(100 - slippage)).div(100) const slippageMultiplier = (new BigNumber(100 - slippage)).div(100)
const minimumAmountReceived = (new BigNumber(destinationValue)).times(slippageMultiplier).toFixed(6) const minimumAmountReceived = (new BigNumber(destinationValue)).times(slippageMultiplier).toFixed(6)
@ -348,7 +347,6 @@ export function quotesToRenderableData (quotes, gasPrice, conversionRate, curren
destinationTokenValue: formatSwapsValueForDisplay(destinationValue), destinationTokenValue: formatSwapsValueForDisplay(destinationValue),
isBestQuote: quote.isBestQuote, isBestQuote: quote.isBestQuote,
liquiditySourceKey, liquiditySourceKey,
metaMaskFee,
feeInEth, feeInEth,
detailedNetworkFees: `${feeInEth} (${feeInFiat})`, detailedNetworkFees: `${feeInEth} (${feeInFiat})`,
networkFees: feeInFiat, networkFees: feeInFiat,

@ -13,6 +13,7 @@
height: 100%; height: 100%;
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
justify-content: space-between;
@media screen and (max-width: 576px) { @media screen and (max-width: 576px) {
overflow-y: auto; overflow-y: auto;
@ -38,16 +39,11 @@
&__view-other-button-container { &__view-other-button-container {
border-radius: 28px; border-radius: 28px;
margin-top: 38px;
width: 100%; width: 100%;
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@media screen and (min-width: 576px) {
margin-top: auto;
}
} }
&__view-other-button, &__view-other-button,
@ -131,13 +127,9 @@
&__fee-card-container { &__fee-card-container {
width: 100%; width: 100%;
margin-top: auto; margin-top: 8px;
margin-bottom: 8px; margin-bottom: 8px;
@media screen and (max-width: 576px) {
margin-top: 16px;
}
@media screen and (min-width: 576px) { @media screen and (min-width: 576px) {
margin-bottom: 0; margin-bottom: 0;
@ -158,4 +150,19 @@
margin-top: 8px; margin-top: 8px;
} }
} }
&__metamask-rate {
display: flex;
margin-top: 8%;
}
&__metamask-rate-text {
@include H7;
color: $Grey-500;
}
&__metamask-rate-info-icon {
margin-left: 4px;
}
} }

@ -22,6 +22,7 @@ import {
getBalanceError, getBalanceError,
getCustomSwapsGas, getCustomSwapsGas,
getDestinationTokenInfo, getDestinationTokenInfo,
getMetaMaskFeeAmount,
getSwapsTradeTxParams, getSwapsTradeTxParams,
getTopQuote, getTopQuote,
navigateBackToBuildQuote, navigateBackToBuildQuote,
@ -72,6 +73,7 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker'
import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps' import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps'
import CountdownTimer from '../countdown-timer' import CountdownTimer from '../countdown-timer'
import SwapsFooter from '../swaps-footer' import SwapsFooter from '../swaps-footer'
import InfoTooltip from '../../../components/ui/info-tooltip'
export default function ViewQuote () { export default function ViewQuote () {
const history = useHistory() const history = useHistory()
@ -131,12 +133,11 @@ export default function ViewQuote () {
.round(0) .round(0)
.toString(16) .toString(16)
const maxGasLimit = (customMaxGas || const nonCustomMaxGasLimit = hexMax(
hexMax(
(`0x${decimalToHex(usedQuote?.maxGas || 0)}`), (`0x${decimalToHex(usedQuote?.maxGas || 0)}`),
usedGasLimitWithMultiplier, usedGasLimitWithMultiplier,
) )
) const maxGasLimit = customMaxGas || nonCustomMaxGasLimit
const gasTotalInWeiHex = calcGasTotal(maxGasLimit, gasPrice) const gasTotalInWeiHex = calcGasTotal(maxGasLimit, gasPrice)
@ -340,6 +341,8 @@ export default function ViewQuote () {
} }
}, [sourceTokenSymbol, sourceTokenValue, destinationTokenSymbol, destinationTokenValue, fetchParams, topQuote, numberOfQuotes, feeInFiat, bestQuoteReviewedEvent, anonymousBestQuoteReviewedEvent]) }, [sourceTokenSymbol, sourceTokenValue, destinationTokenSymbol, destinationTokenValue, fetchParams, topQuote, numberOfQuotes, feeInFiat, bestQuoteReviewedEvent, anonymousBestQuoteReviewedEvent])
const metaMaskFee = useSelector(getMetaMaskFeeAmount)
const onFeeCardTokenApprovalClick = () => { const onFeeCardTokenApprovalClick = () => {
anonymousEditSpendLimitOpened() anonymousEditSpendLimitOpened()
editSpendLimitOpened() editSpendLimitOpened()
@ -394,6 +397,7 @@ export default function ViewQuote () {
: null : null
), ),
useFastestButtons: true, useFastestButtons: true,
minimumGasLimit: Number(hexToDecimal(nonCustomMaxGasLimit)),
})) }))
const tokenApprovalTextComponent = ( const tokenApprovalTextComponent = (
@ -494,6 +498,14 @@ export default function ViewQuote () {
<i className="fa fa-arrow-right" /> <i className="fa fa-arrow-right" />
</div> </div>
</div> </div>
<div className="view-quote__metamask-rate">
<p className="view-quote__metamask-rate-text">{ t('swapQuoteIncludesRate', [metaMaskFee]) }</p>
<InfoTooltip
position="top"
contentText={t('swapMetaMaskFeeDescription', [metaMaskFee])}
wrapperClassName="view-quote__metamask-rate-info-icon"
/>
</div>
<div <div
className={classnames('view-quote__fee-card-container', { className={classnames('view-quote__fee-card-container', {
'view-quote__fee-card-container--thin': showWarning, 'view-quote__fee-card-container--thin': showWarning,

@ -122,7 +122,7 @@ export default class UnlockPage extends Component {
style={style} style={style}
disabled={!this.state.password} disabled={!this.state.password}
fullWidth fullWidth
variant="raised" variant="contained"
size="large" size="large"
onClick={this.handleSubmit} onClick={this.handleSubmit}
disableRipple disableRipple

@ -88,16 +88,31 @@ export function getSafeLowEstimate (state) {
return safeLow return safeLow
} }
export function isCustomPriceSafe (state) { export function getAverageEstimate (state) {
const {
gas: {
basicEstimates: {
average,
},
},
} = state
return average
}
export function isCustomPriceSafe (state, averageIsSafe) {
const safeLow = getSafeLowEstimate(state) const safeLow = getSafeLowEstimate(state)
const average = getAverageEstimate(state)
const safeMinimumPrice = averageIsSafe ? average : safeLow
const customGasPrice = getCustomGasPrice(state) const customGasPrice = getCustomGasPrice(state)
if (!customGasPrice) { if (!customGasPrice) {
return true return true
} }
if (safeLow === null) { if (safeMinimumPrice === null) {
return null return false
} }
const customPriceSafe = conversionGreaterThan( const customPriceSafe = conversionGreaterThan(
@ -107,7 +122,7 @@ export function isCustomPriceSafe (state) {
fromDenomination: 'WEI', fromDenomination: 'WEI',
toDenomination: 'GWEI', toDenomination: 'GWEI',
}, },
{ value: safeLow, fromNumericBase: 'dec' }, { value: safeMinimumPrice, fromNumericBase: 'dec' },
) )
return customPriceSafe return customPriceSafe
@ -216,7 +231,7 @@ export function getRenderableBasicEstimateData (state, gasLimit, useFastestButto
}, },
} = state } = state
const slowEstimatData = { const slowEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW, gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit), feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit),
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
@ -234,7 +249,7 @@ export function getRenderableBasicEstimateData (state, gasLimit, useFastestButto
timeEstimate: avgWait && getRenderableTimeEstimate(avgWait), timeEstimate: avgWait && getRenderableTimeEstimate(avgWait),
priceInHexWei: getGasPriceInHexWei(average), priceInHexWei: getGasPriceInHexWei(average),
} }
const fastEstimatData = { const fastEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.FAST, gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit), feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit),
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
@ -254,8 +269,8 @@ export function getRenderableBasicEstimateData (state, gasLimit, useFastestButto
} }
return useFastestButtons return useFastestButtons
? [averageEstimateData, fastEstimatData, fastestEstimateData] ? [fastEstimateData, fastestEstimateData]
: [slowEstimatData, averageEstimateData, fastEstimatData] : [slowEstimateData, averageEstimateData, fastEstimateData]
} }
export function getRenderableEstimateDataForSmallButtonsFromGWEI (state) { export function getRenderableEstimateDataForSmallButtonsFromGWEI (state) {

@ -346,13 +346,6 @@ describe('custom-gas selectors', function () {
}, },
{ {
expectedResult: [ expectedResult: [
{
gasEstimateType: 'AVERAGE',
feeInPrimaryCurrency: '0.000147 ETH',
feeInSecondaryCurrency: '$0.38',
priceInHexWei: '0x1a13b8600',
timeEstimate: '~10 min 6 sec',
},
{ {
gasEstimateType: 'FAST', gasEstimateType: 'FAST',
feeInSecondaryCurrency: '$0.54', feeInSecondaryCurrency: '$0.54',

Loading…
Cancel
Save