diff --git a/CHANGELOG.md b/CHANGELOG.md index a42139678..c2d1ff61c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## 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 - [#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) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 3188d7894..a4710ccbf 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -708,6 +708,10 @@ "gasLimitTooLow": { "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": { "message": "Gas Price (GWEI)" }, @@ -1643,6 +1647,10 @@ "swapEstimatedNetworkFee": { "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": { "message": "Estimated network fees" }, @@ -1677,6 +1685,9 @@ "swapFinalizing": { "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": { "message": "Get quotes" }, @@ -1705,7 +1716,8 @@ "message": "Transaction may fail, max slippage too low." }, "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": { "message": "Max network fee" @@ -1717,7 +1729,7 @@ "message": "MetaMask fee" }, "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." }, "swapNQuotesAvailable": { @@ -1741,6 +1753,10 @@ "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." }, + "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": { "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." diff --git a/app/manifest/_base.json b/app/manifest/_base.json index 09bb58e6d..f32d4e9bb 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -68,6 +68,6 @@ "notifications" ], "short_name": "__MSG_appName__", - "version": "8.1.1", + "version": "8.1.2", "web_accessible_resources": ["inpage.js", "phishing.html"] } diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 2d97a1382..bb5563606 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -14,7 +14,7 @@ import log from 'loglevel' import pify from 'pify' import Web3 from 'web3' 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 { SINGLE_CALL_BALANCES_ADDRESS, @@ -24,25 +24,30 @@ import { } from '../controllers/network/contract-addresses' import { bnToHex } from './util' +/** + * This module is responsible for tracking any number of accounts and caching their current balances & transaction + * counts. + * + * It also tracks transaction hashes, and checks their inclusion status on each new block. + * + * @typedef {Object} AccountTracker + * @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 {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block + * @property {Object} _provider A provider needed to create the EthQuery instance used within this AccountTracker. + * @property {EthQuery} _query An EthQuery instance used to access account information from the blockchain + * @property {BlockTracker} _blockTracker A BlockTracker instance. Needed to ensure that accounts and their info updates + * when a new block is created. + * @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 { /** - * This module is responsible for tracking any number of accounts and caching their current balances & transaction - * counts. - * - * It also tracks transaction hashes, and checks their inclusion status on each new block. - * - * @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.accounts The accounts currently stored in this AccountTracker - * @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block - * @property {Object} _provider A provider needed to create the EthQuery instance used within this AccountTracker. - * @property {EthQuery} _query An EthQuery instance used to access account information from the blockchain - * @property {BlockTracker} _blockTracker A BlockTracker instance. Needed to ensure that accounts and their info updates - * when a new block is created. - * @property {Object} _currentBlockNumber Reference to a property on the _blockTracker: the number (i.e. an id) of the the current block - * + * @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 = {}) { const initState = { @@ -61,7 +66,7 @@ export default class AccountTracker { }) // bind function for easier listener syntax this._updateForBlock = this._updateForBlock.bind(this) - this.network = opts.network + this.getCurrentChainId = opts.getCurrentChainId this.web3 = new Web3(this._provider) } @@ -196,22 +201,22 @@ export default class AccountTracker { async _updateAccounts () { const { accounts } = this.store.getState() const addresses = Object.keys(accounts) - const currentNetwork = this.network.getNetworkState() + const chainId = this.getCurrentChainId() - switch (currentNetwork) { - case MAINNET_NETWORK_ID.toString(): + switch (chainId) { + case MAINNET_CHAIN_ID: await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS) break - case RINKEBY_NETWORK_ID.toString(): + case RINKEBY_CHAIN_ID: await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY) break - case ROPSTEN_NETWORK_ID.toString(): + case ROPSTEN_CHAIN_ID: await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN) break - case KOVAN_NETWORK_ID.toString(): + case KOVAN_CHAIN_ID: await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN) break diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b4166bde5..f66091d80 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -168,7 +168,7 @@ export default class MetamaskController extends EventEmitter { this.accountTracker = new AccountTracker({ provider: this.provider, blockTracker: this.blockTracker, - network: this.networkController, + getCurrentChainId: this.networkController.getCurrentChainId.bind(this.networkController), }) // start and stop polling for balances based on activeControllerConnections diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js index 75319090d..5886f75d9 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import classnames from 'classnames' import { debounce } from 'lodash' import Tooltip from '../../../ui/tooltip' +import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants' export default class AdvancedGasInputs extends Component { static contextTypes = { @@ -18,6 +19,11 @@ export default class AdvancedGasInputs extends Component { customPriceIsSafe: PropTypes.bool, isSpeedUp: PropTypes.bool, customGasLimitMessage: PropTypes.string, + minimumGasLimit: PropTypes.number, + } + + static defaultProps = { + minimumGasLimit: Number(MIN_GAS_LIMIT_DEC), } constructor (props) { @@ -84,7 +90,7 @@ export default class AdvancedGasInputs extends Component { return {} } - gasLimitError ({ insufficientBalance, gasLimit }) { + gasLimitError ({ insufficientBalance, gasLimit, minimumGasLimit }) { const { t } = this.context if (insufficientBalance) { @@ -92,9 +98,9 @@ export default class AdvancedGasInputs extends Component { errorText: t('insufficientBalance'), errorType: 'error', } - } else if (gasLimit < 21000) { + } else if (gasLimit < minimumGasLimit) { return { - errorText: t('gasLimitTooLow'), + errorText: t('gasLimitTooLowWithDynamicFee', [minimumGasLimit]), errorType: 'error', } } @@ -153,6 +159,7 @@ export default class AdvancedGasInputs extends Component { customPriceIsSafe, isSpeedUp, customGasLimitMessage, + minimumGasLimit, } = this.props const { gasPrice, @@ -172,7 +179,7 @@ export default class AdvancedGasInputs extends Component { const { errorText: gasLimitErrorText, errorType: gasLimitErrorType, - } = this.gasLimitError({ insufficientBalance, gasLimit }) + } = this.gasLimitError({ insufficientBalance, gasLimit, minimumGasLimit }) const gasLimitErrorComponent = gasLimitErrorType ? (
{ gasLimitErrorText } diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js index a18e0e114..b47a224a6 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js @@ -17,6 +17,7 @@ describe('Advanced Gas Inputs', function () { insufficientBalance: false, customPriceIsSafe: true, isSpeedUp: false, + minimumGasLimit: 21000, } beforeEach(function () { @@ -91,7 +92,7 @@ describe('Advanced Gas Inputs', function () { assert.equal(renderError.length, 2) 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 () { diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 255012392..176fd9b12 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -27,6 +27,7 @@ export default class AdvancedTabContent extends Component { isSpeedUp: PropTypes.bool, isEthereumNetwork: PropTypes.bool, customGasLimitMessage: PropTypes.string, + minimumGasLimit: PropTypes.number.isRequired, } renderDataSummary (transactionFee, timeRemaining) { @@ -67,6 +68,7 @@ export default class AdvancedTabContent extends Component { transactionFee, isEthereumNetwork, customGasLimitMessage, + minimumGasLimit, } = this.props return ( @@ -83,6 +85,7 @@ export default class AdvancedTabContent extends Component { customPriceIsSafe={customPriceIsSafe} isSpeedUp={isSpeedUp} customGasLimitMessage={customGasLimitMessage} + minimumGasLimit={minimumGasLimit} />
{ isEthereumNetwork diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 061f43a95..249b29ca7 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -53,7 +53,8 @@ export default class GasModalPageContainer extends Component { customTotalSupplement: PropTypes.string, isSwap: PropTypes.bool, value: PropTypes.string, - conversionRate: PropTypes.number, + conversionRate: PropTypes.string, + minimumGasLimit: PropTypes.number.isRequired, } state = { @@ -98,6 +99,7 @@ export default class GasModalPageContainer extends Component { }, isEthereumNetwork, customGasLimitMessage, + minimumGasLimit, } = this.props return ( @@ -116,6 +118,7 @@ export default class GasModalPageContainer extends Component { isSpeedUp={isSpeedUp} isRetry={isRetry} isEthereumNetwork={isEthereumNetwork} + minimumGasLimit={minimumGasLimit} /> ) } diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 54465f674..2be9e0067 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -62,6 +62,7 @@ import { calcGasTotal, isBalanceSufficient, } 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 GasModalPageContainer from './gas-modal-page-container.component' @@ -75,6 +76,7 @@ const mapStateToProps = (state, ownProps) => { customTotalSupplement = '', extraInfoRow = null, useFastestButtons = false, + minimumGasLimit = Number(MIN_GAS_LIMIT_DEC), } = modalProps || {} const { transaction = {} } = ownProps const selectedTransaction = isSwap @@ -158,7 +160,7 @@ const mapStateToProps = (state, ownProps) => { newTotalFiat, currentTimeEstimate, blockTime: getBasicGasEstimateBlockTime(state), - customPriceIsSafe: isCustomPriceSafe(state), + customPriceIsSafe: isCustomPriceSafe(state, isSwap), maxModeOn, gasPriceButtonGroupProps: { buttonDataLoading, @@ -202,6 +204,7 @@ const mapStateToProps = (state, ownProps) => { conversionRate, value, customTotalSupplement, + minimumGasLimit, } } @@ -264,6 +267,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { tokenBalance, customGasLimit, transaction, + minimumGasLimit, } = stateProps const { hideGasButtonGroup: dispatchHideGasButtonGroup, @@ -333,7 +337,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { disableSave: ( insufficientBalance || (isSpeedUp && customGasPrice === 0) || - customGasLimit < 21000 + customGasLimit < minimumGasLimit ), } } diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 57514e5fa..00e15f943 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -63,6 +63,7 @@ describe('gas-modal-page-container container', function () { id: 34, }, extraInfoRow: { label: 'mockLabel', value: 'mockValue' }, + minimumGasLimit: 21000, }, }, }, @@ -170,6 +171,7 @@ describe('gas-modal-page-container container', function () { id: 34, }, value: '0x640000000000000', + minimumGasLimit: 21000, } const baseMockOwnProps = { transaction: { id: 34 } } const tests = [ diff --git a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js index d78db3d14..e49309adc 100644 --- a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js +++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js @@ -104,15 +104,29 @@ export default class QrScanner extends Component { componentWillUnmount () { this.mounted = false clearTimeout(this.permissionChecker) + this.teardownCodeReader() + } + + teardownCodeReader () { if (this.codeReader) { this.codeReader.reset() + this.codeReader.stop() + this.codeReader = null } } initCamera = async () => { - this.codeReader = new BrowserQRCodeReader() + // 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() + } try { await this.codeReader.getVideoInputDevices() + this.checkPermissions() const content = await this.codeReader.decodeFromInputVideoDevice(undefined, 'video') const result = this.parseContent(content.text) if (!this.mounted) { @@ -162,7 +176,7 @@ export default class QrScanner extends Component { stopAndClose = () => { if (this.codeReader) { - this.codeReader.reset() + this.teardownCodeReader() } this.props.hideModal() } @@ -170,7 +184,7 @@ export default class QrScanner extends Component { tryAgain = () => { clearTimeout(this.permissionChecker) if (this.codeReader) { - this.codeReader.reset() + this.teardownCodeReader() } this.setState(this.getInitialState(), () => { this.checkEnvironment() diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index 9f54202f2..9953df9d4 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -50,9 +50,10 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce displayedStatusKey, isPending, senderAddress, + isSubmitted, } = useTransactionDisplayData(transactionGroup) - const timeRemaining = useTransactionTimeRemaining(isPending, isEarliestNonce, submittedTime, gasPrice) + const timeRemaining = useTransactionTimeRemaining(isSubmitted, isEarliestNonce, submittedTime, gasPrice) const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST const isApproval = category === TRANSACTION_CATEGORY_APPROVAL diff --git a/ui/app/components/ui/info-tooltip/index.scss b/ui/app/components/ui/info-tooltip/index.scss index ba0ca6259..cdc52fb97 100644 --- a/ui/app/components/ui/info-tooltip/index.scss +++ b/ui/app/components/ui/info-tooltip/index.scss @@ -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; } -.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; } -.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; } -.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; } .tippy-tooltip { - &#{&}-info-theme { + &#{&}-info-theme, + &#{&}-wideInfo-theme { background: white; color: black; box-shadow: 0 0 14px rgba(0, 0, 0, 0.18); @@ -38,4 +43,8 @@ color: $Grey-500; } } + + &#{&}-wideInfo-theme { + max-width: 260px; + } } diff --git a/ui/app/components/ui/info-tooltip/info-tooltip.js b/ui/app/components/ui/info-tooltip/info-tooltip.js index 73bf356c5..42b8af75d 100644 --- a/ui/app/components/ui/info-tooltip/info-tooltip.js +++ b/ui/app/components/ui/info-tooltip/info-tooltip.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames' import Tooltip from '../tooltip' const positionArrowClassMap = { @@ -12,17 +13,21 @@ const positionArrowClassMap = { export default function InfoTooltip ({ contentText = '', position = '', + containerClassName, + wrapperClassName, + wide, }) { return (
@@ -33,4 +38,7 @@ export default function InfoTooltip ({ InfoTooltip.propTypes = { contentText: PropTypes.string, position: PropTypes.oneOf(['top', 'left', 'bottom', 'right']), + wide: PropTypes.bool, + containerClassName: PropTypes.string, + wrapperClassName: PropTypes.string, } diff --git a/ui/app/hooks/tests/useTransactionDisplayData.test.js b/ui/app/hooks/tests/useTransactionDisplayData.test.js index 5291a3023..d44eb77ae 100644 --- a/ui/app/hooks/tests/useTransactionDisplayData.test.js +++ b/ui/app/hooks/tests/useTransactionDisplayData.test.js @@ -27,6 +27,7 @@ const expectedResults = [ secondaryCurrency: '-1 ETH', isPending: false, displayedStatusKey: 'confirmed', + isSubmitted: false, }, { title: 'Send ETH', diff --git a/ui/app/hooks/useTransactionDisplayData.js b/ui/app/hooks/useTransactionDisplayData.js index fa6f5773c..de027d41e 100644 --- a/ui/app/hooks/useTransactionDisplayData.js +++ b/ui/app/hooks/useTransactionDisplayData.js @@ -23,6 +23,7 @@ import { TOKEN_CATEGORY_HASH, SWAP, SWAP_APPROVAL, + SUBMITTED_STATUS, } from '../helpers/constants/transactions' import { getTokens } from '../ducks/metamask/metamask' import { useI18nContext } from './useI18nContext' @@ -74,6 +75,7 @@ export function useTransactionDisplayData (transactionGroup) { const displayedStatusKey = getStatusKey(primaryTransaction) const isPending = displayedStatusKey in PENDING_STATUS_HASH + const isSubmitted = displayedStatusKey === SUBMITTED_STATUS const primaryValue = primaryTransaction.txParams?.value let prefix = '-' @@ -213,5 +215,6 @@ export function useTransactionDisplayData (transactionGroup) { ) ? undefined : secondaryCurrency, displayedStatusKey, isPending, + isSubmitted, } } diff --git a/ui/app/hooks/useTransactionTimeRemaining.js b/ui/app/hooks/useTransactionTimeRemaining.js index db3e7f890..50f413d05 100644 --- a/ui/app/hooks/useTransactionTimeRemaining.js +++ b/ui/app/hooks/useTransactionTimeRemaining.js @@ -26,7 +26,7 @@ function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) { * 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 * 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 {number} submittedTime - the timestamp for when the transaction was submitted * @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 */ export function useTransactionTimeRemaining ( - isPending, + isSubmitted, isEarliestNonce, submittedTime, currentGasPrice, @@ -73,7 +73,7 @@ export function useTransactionTimeRemaining ( if ( (isMainNet && (transactionTimeFeatureActive || forceAllow)) && - isPending && + isSubmitted && isEarliestNonce && !isNaN(initialTimeEstimate) ) { @@ -93,10 +93,10 @@ export function useTransactionTimeRemaining ( isMainNet, transactionTimeFeatureActive, isEarliestNonce, - isPending, submittedTime, initialTimeEstimate, forceAllow, + isSubmitted, ]) // 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 does not have the transactionTime feature flag enabled // The transaction is not pending, or isn't the earliest nonce - const usedFormat = dontFormat - ? timeRemaining - : rtf.format(timeRemaining, 'minute') - return timeRemaining ? usedFormat : undefined + if (timeRemaining && dontFormat) { + return timeRemaining + } else if (timeRemaining) { + return rtf.format(timeRemaining, 'minute') + } + return undefined } diff --git a/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js index 1ee4f78c3..1ddb5252f 100644 --- a/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js @@ -19,7 +19,7 @@ import { useTransactionTimeRemaining } from '../../../hooks/useTransactionTimeRe import { usePrevious } from '../../../hooks/usePrevious' import Mascot from '../../../components/ui/mascot' 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 { QUOTES_EXPIRED_ERROR, @@ -28,6 +28,7 @@ import { QUOTES_NOT_AVAILABLE_ERROR, OFFLINE_FOR_MAINTENANCE, } from '../../../helpers/constants/swaps' +import { SUBMITTED_STATUS } from '../../../helpers/constants/transactions' import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes' import { getRenderableGasFeesForQuote } from '../swaps.util' @@ -96,7 +97,15 @@ export default function AwaitingSwap ({ 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 timeRemainingIsNumber = typeof timeRemaining === 'number' && !isNaN(timeRemaining) const previousTimeRemainingIsNumber = typeof previousTimeRemaining === 'number' && !isNaN(previousTimeRemaining) diff --git a/ui/app/pages/swaps/fee-card/fee-card.js b/ui/app/pages/swaps/fee-card/fee-card.js index 61c173015..67f2ee807 100644 --- a/ui/app/pages/swaps/fee-card/fee-card.js +++ b/ui/app/pages/swaps/fee-card/fee-card.js @@ -22,6 +22,30 @@ export default function FeeCard ({
{t('swapEstimatedNetworkFee')}
+ +

{ t('swapGasFeeSummary') }

+

{ t('swapEstimatedNetworkFeeSummary', [ + + { t('swapEstimatedNetworkFee') } + , + ]) } +

+

{ t('swapMaxNetworkFeeInfo', [ + + { t('swapMaxNetworkFees') } + , + ]) } +

+ + ) + } + containerClassName="fee-card__info-tooltip-content-container" + wrapperClassName="fee-card__row-label fee-card__info-tooltip-container" + wide + />
@@ -42,12 +66,6 @@ export default function FeeCard ({
{t('edit')}
-
- -
diff --git a/ui/app/pages/swaps/fee-card/fee-card.stories.js b/ui/app/pages/swaps/fee-card/fee-card.stories.js index db5029a95..9527dcda3 100644 --- a/ui/app/pages/swaps/fee-card/fee-card.stories.js +++ b/ui/app/pages/swaps/fee-card/fee-card.stories.js @@ -3,6 +3,15 @@ import { action } from '@storybook/addon-actions' import { text } from '@storybook/addon-knobs/react' import FeeCard from './fee-card' +const tokenApprovalTextComponent = ( + + ABC + +) + const containerStyle = { width: '300px', } @@ -24,19 +33,11 @@ export const WithAllProps = () => { fee: text('secondaryFee', '100 USD'), maxFee: text('secondaryMaxFee', '200 USD'), })} - maxFeeRow={({ - text: text('maxFeeText', 'Max Fee'), - 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: false, - })} + onFeeCardMaxRowClick={action('Clicked max fee row link')} + tokenApprovalTextComponent={tokenApprovalTextComponent} + tokenApprovalSourceTokenSymbol="ABC" + onTokenApprovalClick={action('Clicked third row link')} + hideTokenApprovalRow={false} />
) @@ -55,19 +56,8 @@ export const WithoutThirdRow = () => { fee: text('secondaryFee', '100 USD'), maxFee: text('secondaryMaxFee', '200 USD'), })} - maxFeeRow={({ - text: text('maxFeeText', 'Max Fee'), - 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, - })} + onFeeCardMaxRowClick={action('Clicked max fee row link')} + hideTokenApprovalRow />
) @@ -77,17 +67,12 @@ export const WithOnlyRequiredProps = () => { return (
) diff --git a/ui/app/pages/swaps/fee-card/index.scss b/ui/app/pages/swaps/fee-card/index.scss index 79e08c4ad..0a86f6ab7 100644 --- a/ui/app/pages/swaps/fee-card/index.scss +++ b/ui/app/pages/swaps/fee-card/index.scss @@ -27,7 +27,7 @@ &__row-header-text, &__row-header-text--bold { - margin-right: 6px; + margin-right: 4px; 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 { margin-right: 4px; } @@ -91,11 +106,8 @@ color: $Grey-500; } + &__row-header-secondary, &__row-header-secondary--bold { - margin-right: 16px; - } - - &__row-header-secondary { margin-right: 12px; } @@ -109,6 +121,10 @@ &__row-header-primary--bold { font-weight: bold; } + + &__bold { + font-weight: bold; + } } .info-tooltip { diff --git a/ui/app/pages/swaps/main-quote-summary/index.scss b/ui/app/pages/swaps/main-quote-summary/index.scss index ff53942a0..7993ea724 100644 --- a/ui/app/pages/swaps/main-quote-summary/index.scss +++ b/ui/app/pages/swaps/main-quote-summary/index.scss @@ -22,7 +22,7 @@ &__quote-backdrop { width: 310px; - height: 179.15px; + height: 164px; } &__details { @@ -49,13 +49,14 @@ } &__quote-details-top { - height: 137px; + height: 94px; display: flex; flex-flow: column; justify-content: center; align-items: center; width: 100%; padding: 12px; + padding-top: 2px; margin-top: 4px; } diff --git a/ui/app/pages/swaps/main-quote-summary/quote-backdrop.js b/ui/app/pages/swaps/main-quote-summary/quote-backdrop.js index f8f06ff33..1a01fa0ca 100644 --- a/ui/app/pages/swaps/main-quote-summary/quote-backdrop.js +++ b/ui/app/pages/swaps/main-quote-summary/quote-backdrop.js @@ -5,13 +5,13 @@ export default function QuotesBackdrop ({ withTopTab, }) { return ( - + - - {withTopTab && } + + {withTopTab && } - + @@ -20,11 +20,11 @@ export default function QuotesBackdrop ({ - + - + diff --git a/ui/app/pages/swaps/swaps.util.js b/ui/app/pages/swaps/swaps.util.js index 92d20556e..32974d5b6 100644 --- a/ui/app/pages/swaps/swaps.util.js +++ b/ui/app/pages/swaps/swaps.util.js @@ -275,7 +275,7 @@ export function getRenderableGasFeesForQuote (tradeGas, approveGas, gasPrice, cu const ethFee = getValueFromWeiHex({ value: gasTotalInWeiHex, toDenomination: 'ETH', - numberOfDecimals: 6, + numberOfDecimals: 5, }) const rawNetworkFees = getValueFromWeiHex({ value: gasTotalInWeiHex, @@ -315,7 +315,6 @@ export function quotesToRenderableData (quotes, gasPrice, conversionRate, curren conversionRate, ) - const metaMaskFee = `0.875%` const slippageMultiplier = (new BigNumber(100 - slippage)).div(100) const minimumAmountReceived = (new BigNumber(destinationValue)).times(slippageMultiplier).toFixed(6) @@ -348,7 +347,6 @@ export function quotesToRenderableData (quotes, gasPrice, conversionRate, curren destinationTokenValue: formatSwapsValueForDisplay(destinationValue), isBestQuote: quote.isBestQuote, liquiditySourceKey, - metaMaskFee, feeInEth, detailedNetworkFees: `${feeInEth} (${feeInFiat})`, networkFees: feeInFiat, diff --git a/ui/app/pages/swaps/view-quote/index.scss b/ui/app/pages/swaps/view-quote/index.scss index 7a6f8cb2d..1b76853fb 100644 --- a/ui/app/pages/swaps/view-quote/index.scss +++ b/ui/app/pages/swaps/view-quote/index.scss @@ -13,6 +13,7 @@ height: 100%; padding-left: 20px; padding-right: 20px; + justify-content: space-between; @media screen and (max-width: 576px) { overflow-y: auto; @@ -38,16 +39,11 @@ &__view-other-button-container { border-radius: 28px; - margin-top: 38px; width: 100%; position: relative; display: flex; align-items: center; justify-content: center; - - @media screen and (min-width: 576px) { - margin-top: auto; - } } &__view-other-button, @@ -131,13 +127,9 @@ &__fee-card-container { width: 100%; - margin-top: auto; + margin-top: 8px; margin-bottom: 8px; - @media screen and (max-width: 576px) { - margin-top: 16px; - } - @media screen and (min-width: 576px) { margin-bottom: 0; @@ -158,4 +150,19 @@ 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; + } } diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index a94b2319b..da08f7615 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -22,6 +22,7 @@ import { getBalanceError, getCustomSwapsGas, getDestinationTokenInfo, + getMetaMaskFeeAmount, getSwapsTradeTxParams, getTopQuote, navigateBackToBuildQuote, @@ -72,6 +73,7 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker' import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps' import CountdownTimer from '../countdown-timer' import SwapsFooter from '../swaps-footer' +import InfoTooltip from '../../../components/ui/info-tooltip' export default function ViewQuote () { const history = useHistory() @@ -131,12 +133,11 @@ export default function ViewQuote () { .round(0) .toString(16) - const maxGasLimit = (customMaxGas || - hexMax( - (`0x${decimalToHex(usedQuote?.maxGas || 0)}`), - usedGasLimitWithMultiplier, - ) + const nonCustomMaxGasLimit = hexMax( + (`0x${decimalToHex(usedQuote?.maxGas || 0)}`), + usedGasLimitWithMultiplier, ) + const maxGasLimit = customMaxGas || nonCustomMaxGasLimit const gasTotalInWeiHex = calcGasTotal(maxGasLimit, gasPrice) @@ -340,6 +341,8 @@ export default function ViewQuote () { } }, [sourceTokenSymbol, sourceTokenValue, destinationTokenSymbol, destinationTokenValue, fetchParams, topQuote, numberOfQuotes, feeInFiat, bestQuoteReviewedEvent, anonymousBestQuoteReviewedEvent]) + const metaMaskFee = useSelector(getMetaMaskFeeAmount) + const onFeeCardTokenApprovalClick = () => { anonymousEditSpendLimitOpened() editSpendLimitOpened() @@ -394,6 +397,7 @@ export default function ViewQuote () { : null ), useFastestButtons: true, + minimumGasLimit: Number(hexToDecimal(nonCustomMaxGasLimit)), })) const tokenApprovalTextComponent = ( @@ -494,6 +498,14 @@ export default function ViewQuote () {
+
+

{ t('swapQuoteIncludesRate', [metaMaskFee]) }

+ +