From 0377e2c64e45f9d4518923f5fec428c54ac6ae0c Mon Sep 17 00:00:00 2001 From: David Walsh Date: Thu, 5 Aug 2021 18:59:58 -0500 Subject: [PATCH] Restore heartbeat to transaction confirmation, use isGasEstimatesLoading more broadly (#11781) --- test/jest/mock-store.js | 1 + .../advanced-gas-controls.component.js | 11 ++ .../edit-gas-popover.component.js | 18 ++-- .../app/gas-timing/gas-timing.component.js | 16 +-- .../gas-timing/gas-timing.component.test.js | 100 +++++++++++++----- .../transaction-detail.component.js | 17 ++- ui/components/ui/form-field/form-field.js | 5 + ui/components/ui/loading-heartbeat/index.js | 37 +++---- ui/components/ui/loading-heartbeat/index.scss | 6 +- .../numeric-input/numeric-input.component.js | 3 + ui/ducks/app/app.js | 15 +++ ui/ducks/metamask/metamask.js | 23 ++++ ui/hooks/useGasFeeEstimates.js | 23 +--- ui/hooks/useGasFeeEstimates.test.js | 16 ++- ui/hooks/useShouldAnimateGasEstimations.js | 26 ++++- .../confirm-transaction-base.component.js | 4 +- .../confirm-transaction-base.container.js | 11 +- ui/pages/swaps/fee-card/fee-card.test.js | 40 +++++-- ui/store/actionConstants.js | 2 + 19 files changed, 276 insertions(+), 98 deletions(-) diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 0e93e04ee..f57ae13ae 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -234,6 +234,7 @@ export const createSwapsMockStore = () => { }, }, }, + gasLoadingAnimationIsShowing: false, }, }; }; diff --git a/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js b/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js index 66ced39b1..75281a2e0 100644 --- a/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js +++ b/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js @@ -7,6 +7,8 @@ import FormField from '../../ui/form-field'; import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; import { getGasFormErrorText } from '../../../helpers/constants/gas'; import { checkNetworkAndAccountSupports1559 } from '../../../selectors'; +import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask'; +import { getGasLoadingAnimationIsShowing } from '../../../ducks/app/app'; export default function AdvancedGasControls({ gasEstimateType, @@ -28,6 +30,12 @@ export default function AdvancedGasControls({ const networkAndAccountSupport1559 = useSelector( checkNetworkAndAccountSupports1559, ); + const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading); + const isGasLoadingAnimationIsShowing = useSelector( + getGasLoadingAnimationIsShowing, + ); + const disableFormFields = + isGasEstimatesLoading || isGasLoadingAnimationIsShowing; const showFeeMarketFields = networkAndAccountSupport1559 && @@ -71,6 +79,7 @@ export default function AdvancedGasControls({ ? getGasFormErrorText(gasErrors.maxPriorityFee, t) : null } + disabled={disableFormFields} /> ) : ( @@ -107,6 +117,7 @@ export default function AdvancedGasControls({ ? getGasFormErrorText(gasErrors.gasPrice, t) : null } + disabled={disableFormFields} /> )} diff --git a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js index c213feac3..432aee2c8 100644 --- a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js +++ b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs'; -import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations'; +import { getGasLoadingAnimationIsShowing } from '../../../ducks/app/app'; import { EDIT_GAS_MODES, GAS_LIMITS } from '../../../../shared/constants/gas'; @@ -45,8 +45,9 @@ export default function EditGasPopover({ const networkAndAccountSupport1559 = useSelector( checkNetworkAndAccountSupports1559, ); - - const shouldAnimate = useShouldAnimateGasEstimations(); + const gasLoadingAnimationIsShowing = useSelector( + getGasLoadingAnimationIsShowing, + ); const showEducationButton = (mode === EDIT_GAS_MODES.MODIFY_IN_PLACE || @@ -192,7 +193,12 @@ export default function EditGasPopover({ @@ -205,9 +211,7 @@ export default function EditGasPopover({ ) : ( <> - {process.env.IN_TEST === 'true' ? null : ( - - )} + {process.env.IN_TEST === 'true' ? null : } { + const actual = jest.requireActual('react-redux'); + + return { + ...actual, + useSelector: jest.fn(), + }; +}); + +const DEFAULT_OPTS = { + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates: { + low: '10', + medium: '20', + high: '30', + }, + isGasEstimatesLoading: true, +}; + +const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => { + if (selector === checkNetworkAndAccountSupports1559) { + return true; + } + if (selector === getGasEstimateType) { + return opts.gasEstimateType ?? DEFAULT_OPTS.gasEstimateType; + } + if (selector === getGasFeeEstimates) { + return opts.gasFeeEstimates ?? DEFAULT_OPTS.gasFeeEstimates; + } + if (selector === getIsGasEstimatesLoading) { + return opts.isGasEstimatesLoading ?? DEFAULT_OPTS.isGasEstimatesLoading; + } + return undefined; +}; + describe('Gas timing', () => { beforeEach(() => { const useI18nContext = sinon.stub(i18nhooks, 'useI18nContext'); useI18nContext.returns((key, variables) => getMessage('en', messages, key, variables), ); + jest.clearAllMocks(); + useSelector.mockImplementation(generateUseSelectorRouter()); }); afterEach(() => { sinon.restore(); }); it('renders nothing when gas is loading', () => { - sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ - isGasEstimatesLoading: true, - gasFeeEstimates: null, - gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, - }); + useSelector.mockImplementation( + generateUseSelectorRouter({ + isGasEstimatesLoading: true, + gasFeeEstimates: null, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + }), + ); const wrapper = shallow(); expect(wrapper.html()).toBeNull(); }); it('renders "very likely" when high estimate is chosen', () => { - sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ - isGasEstimatesLoading: false, - gasFeeEstimates: MOCK_FEE_ESTIMATE, - gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, - }); + useSelector.mockImplementation( + generateUseSelectorRouter({ + isGasEstimatesLoading: false, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + }), + ); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.html()).toContain('gasTimingVeryPositive'); }); it('renders "likely" when medium estimate is chosen', () => { - sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ - isGasEstimatesLoading: false, - gasFeeEstimates: MOCK_FEE_ESTIMATE, - gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, - }); + useSelector.mockImplementation( + generateUseSelectorRouter({ + isGasEstimatesLoading: false, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + }), + ); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.html()).toContain('gasTimingPositive'); }); it('renders "maybe" when low estimate is chosen', () => { - sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ - isGasEstimatesLoading: false, - gasFeeEstimates: MOCK_FEE_ESTIMATE, - gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, - }); + useSelector.mockImplementation( + generateUseSelectorRouter({ + isGasEstimatesLoading: false, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + }), + ); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.html()).toContain('gasTimingNegative'); }); }); diff --git a/ui/components/app/transaction-detail/transaction-detail.component.js b/ui/components/app/transaction-detail/transaction-detail.component.js index 0829df684..2f12d89da 100644 --- a/ui/components/app/transaction-detail/transaction-detail.component.js +++ b/ui/components/app/transaction-detail/transaction-detail.component.js @@ -1,18 +1,33 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; + +import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask'; +import { getGasLoadingAnimationIsShowing } from '../../../ducks/app/app'; import { I18nContext } from '../../../contexts/i18n'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; +import LoadingHeartBeat from '../../ui/loading-heartbeat'; export default function TransactionDetail({ rows = [], onEdit }) { const t = useContext(I18nContext); + const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading); + const gasLoadingAnimationIsShowing = useSelector( + getGasLoadingAnimationIsShowing, + ); return (
+ {process.env.IN_TEST === 'true' ? null : } {onEdit && (
- +
)}
{rows}
diff --git a/ui/components/ui/form-field/form-field.js b/ui/components/ui/form-field/form-field.js index 9152937a6..d735636ac 100644 --- a/ui/components/ui/form-field/form-field.js +++ b/ui/components/ui/form-field/form-field.js @@ -28,6 +28,7 @@ export default function FormField({ autoFocus, password, allowDecimals, + disabled, }) { return (
) : ( )} {error && ( @@ -120,6 +123,7 @@ FormField.propTypes = { numeric: PropTypes.bool, password: PropTypes.bool, allowDecimals: PropTypes.bool, + disabled: PropTypes.bool, }; FormField.defaultProps = { @@ -135,4 +139,5 @@ FormField.defaultProps = { numeric: false, password: false, allowDecimals: true, + disabled: false, }; diff --git a/ui/components/ui/loading-heartbeat/index.js b/ui/components/ui/loading-heartbeat/index.js index fae2e886d..2c8e491e8 100644 --- a/ui/components/ui/loading-heartbeat/index.js +++ b/ui/components/ui/loading-heartbeat/index.js @@ -1,36 +1,25 @@ import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { useEffect, useRef } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { getGasLoadingAnimationIsShowing } from '../../../ducks/app/app'; +import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations'; -export default function LoadingHeartBeat({ active }) { - const heartNode = useRef(null); +const BASE_CLASS = 'loading-heartbeat'; +const LOADING_CLASS = `${BASE_CLASS}--active`; - const LOADING_CLASS = 'loading-heartbeat--active'; - - // When the loading animation completes, remove the className to disappear again - useEffect(() => { - const eventName = 'animationend'; - const node = heartNode?.current; - const eventHandler = () => { - node?.classList.remove(LOADING_CLASS); - }; - - node?.addEventListener(eventName, eventHandler); - return () => { - node?.removeEventListener(eventName, eventHandler); - }; - }, [heartNode]); +export default function LoadingHeartBeat() { + useShouldAnimateGasEstimations(); + const active = useSelector(getGasLoadingAnimationIsShowing); return (
{ + e.preventDefault(); + e.stopPropagation(); + }} >
); } - -LoadingHeartBeat.propTypes = { - active: PropTypes.bool, -}; diff --git a/ui/components/ui/loading-heartbeat/index.scss b/ui/components/ui/loading-heartbeat/index.scss index 0b1b095f2..0e3c4d19d 100644 --- a/ui/components/ui/loading-heartbeat/index.scss +++ b/ui/components/ui/loading-heartbeat/index.scss @@ -7,10 +7,14 @@ opacity: 0; background: #fff; display: none; + pointer-events: none; &--active { display: block; - animation: heartbeat 2s ease-in-out; + animation-name: heartbeat; + animation-duration: 2s; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; } } diff --git a/ui/components/ui/numeric-input/numeric-input.component.js b/ui/components/ui/numeric-input/numeric-input.component.js index 1fe9a18d2..0befdf4f6 100644 --- a/ui/components/ui/numeric-input/numeric-input.component.js +++ b/ui/components/ui/numeric-input/numeric-input.component.js @@ -11,6 +11,7 @@ export default function NumericInput({ error = '', autoFocus = false, allowDecimals = true, + disabled = false, }) { return (
{detailText && ( @@ -46,4 +48,5 @@ NumericInput.propTypes = { error: PropTypes.string, autoFocus: PropTypes.bool, allowDecimals: PropTypes.bool, + disabled: PropTypes.bool, }; diff --git a/ui/ducks/app/app.js b/ui/ducks/app/app.js index e3b3e275f..93a21a16f 100644 --- a/ui/ducks/app/app.js +++ b/ui/ducks/app/app.js @@ -53,6 +53,7 @@ export default function reduceApp(state = {}, action) { singleExceptions: { testKey: null, }, + gasLoadingAnimationIsShowing: false, ...state, }; @@ -358,6 +359,12 @@ export default function reduceApp(state = {}, action) { }, }; + case actionConstants.TOGGLE_GAS_LOADING_ANIMATION: + return { + ...appState, + gasLoadingAnimationIsShowing: action.value, + }; + default: return appState; } @@ -377,7 +384,15 @@ export function hideWhatsNewPopup() { }; } +export function toggleGasLoadingAnimation(value) { + return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value }; +} + // Selectors export function getQrCodeData(state) { return state.appState.qrCodeData; } + +export function getGasLoadingAnimationIsShowing(state) { + return state.appState.gasLoadingAnimationIsShowing; +} diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 8d93677a1..b2bef9558 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -4,11 +4,13 @@ import { ALERT_TYPES } from '../../../shared/constants/alerts'; import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; import { accountsWithSendEtherInfoSelector, + checkNetworkAndAccountSupports1559, getAddressBook, } from '../../selectors'; import { updateTransaction } from '../../store/actions'; import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util'; +import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas'; export default function reduceMetamask(state = {}, action) { const metamaskState = { @@ -299,3 +301,24 @@ export function getGasFeeEstimates(state) { export function getEstimatedGasFeeTimeBounds(state) { return state.metamask.estimatedGasFeeTimeBounds; } + +export function getIsGasEstimatesLoading(state) { + const networkAndAccountSupports1559 = checkNetworkAndAccountSupports1559( + state, + ); + const gasEstimateType = getGasEstimateType(state); + + // We consider the gas estimate to be loading if the gasEstimateType is + // 'NONE' or if the current gasEstimateType cannot be supported by the current + // network + const isEIP1559TolerableEstimateType = + gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET || + gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE; + const isGasEstimatesLoading = + gasEstimateType === GAS_ESTIMATE_TYPES.NONE || + (networkAndAccountSupports1559 && !isEIP1559TolerableEstimateType) || + (!networkAndAccountSupports1559 && + gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET); + + return isGasEstimatesLoading; +} diff --git a/ui/hooks/useGasFeeEstimates.js b/ui/hooks/useGasFeeEstimates.js index 094e575f9..7bbdd4fc0 100644 --- a/ui/hooks/useGasFeeEstimates.js +++ b/ui/hooks/useGasFeeEstimates.js @@ -1,17 +1,12 @@ import { useSelector } from 'react-redux'; -import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas'; import { getEstimatedGasFeeTimeBounds, getGasEstimateType, getGasFeeEstimates, + getIsGasEstimatesLoading, } from '../ducks/metamask/metamask'; -import { checkNetworkAndAccountSupports1559 } from '../selectors'; import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling'; -/** - * @typedef {keyof typeof GAS_ESTIMATE_TYPES} GasEstimateTypes - */ - /** * @typedef {object} GasEstimates * @property {GasEstimateTypes} gasEstimateType - The type of estimate provided @@ -35,26 +30,12 @@ import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling'; * @returns {GasFeeEstimates} - GasFeeEstimates object */ export function useGasFeeEstimates() { - const networkAndAccountSupports1559 = useSelector( - checkNetworkAndAccountSupports1559, - ); const gasEstimateType = useSelector(getGasEstimateType); const gasFeeEstimates = useSelector(getGasFeeEstimates); const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds); + const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading); useSafeGasEstimatePolling(); - // We consider the gas estimate to be loading if the gasEstimateType is - // 'NONE' or if the current gasEstimateType cannot be supported by the current - // network - const isEIP1559TolerableEstimateType = - gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET || - gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE; - const isGasEstimatesLoading = - gasEstimateType === GAS_ESTIMATE_TYPES.NONE || - (networkAndAccountSupports1559 && !isEIP1559TolerableEstimateType) || - (!networkAndAccountSupports1559 && - gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET); - return { gasFeeEstimates, gasEstimateType, diff --git a/ui/hooks/useGasFeeEstimates.test.js b/ui/hooks/useGasFeeEstimates.test.js index 0af584e1c..574bb7904 100644 --- a/ui/hooks/useGasFeeEstimates.test.js +++ b/ui/hooks/useGasFeeEstimates.test.js @@ -5,12 +5,14 @@ import createRandomId from '../../shared/modules/random-id'; import { getGasEstimateType, getGasFeeEstimates, + getIsGasEstimatesLoading, } from '../ducks/metamask/metamask'; import { checkNetworkAndAccountSupports1559 } from '../selectors'; import { disconnectGasFeeEstimatePoller, getGasFeeEstimatesAndStartPolling, } from '../store/actions'; + import { useGasFeeEstimates } from './useGasFeeEstimates'; jest.mock('../store/actions', () => ({ @@ -37,6 +39,7 @@ const DEFAULT_OPTS = { medium: '20', high: '30', }, + isGasEstimatesLoading: true, }; const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => { @@ -52,6 +55,9 @@ const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => { if (selector === getGasFeeEstimates) { return opts.gasFeeEstimates ?? DEFAULT_OPTS.gasFeeEstimates; } + if (selector === getIsGasEstimatesLoading) { + return opts.isGasEstimatesLoading ?? DEFAULT_OPTS.isGasEstimatesLoading; + } return undefined; }; @@ -68,15 +74,16 @@ describe('useGasFeeEstimates', () => { disconnectGasFeeEstimatePoller.mockImplementation((token) => { tokens = tokens.filter((tkn) => tkn !== token); }); - useSelector.mockImplementation(generateUseSelectorRouter()); }); it('registers with the controller', () => { + useSelector.mockImplementation(generateUseSelectorRouter()); renderHook(() => useGasFeeEstimates()); expect(tokens).toHaveLength(1); }); it('clears token with the controller on unmount', async () => { + useSelector.mockImplementation(generateUseSelectorRouter()); renderHook(() => useGasFeeEstimates()); expect(tokens).toHaveLength(1); const expectedToken = tokens[0]; @@ -87,6 +94,11 @@ describe('useGasFeeEstimates', () => { }); it('works with LEGACY gas prices', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + isGasEstimatesLoading: false, + }), + ); const { result: { current }, } = renderHook(() => useGasFeeEstimates()); @@ -104,6 +116,7 @@ describe('useGasFeeEstimates', () => { generateUseSelectorRouter({ gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, gasFeeEstimates, + isGasEstimatesLoading: false, }), ); @@ -145,6 +158,7 @@ describe('useGasFeeEstimates', () => { checkNetworkAndAccountSupports1559: true, gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasFeeEstimates, + isGasEstimatesLoading: false, }), ); diff --git a/ui/hooks/useShouldAnimateGasEstimations.js b/ui/hooks/useShouldAnimateGasEstimations.js index df588c0ec..cfd150c30 100644 --- a/ui/hooks/useShouldAnimateGasEstimations.js +++ b/ui/hooks/useShouldAnimateGasEstimations.js @@ -1,10 +1,20 @@ -import { useRef } from 'react'; +import { useRef, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { isEqual } from 'lodash'; +import { + getGasLoadingAnimationIsShowing, + toggleGasLoadingAnimation, +} from '../ducks/app/app'; import { useGasFeeEstimates } from './useGasFeeEstimates'; export function useShouldAnimateGasEstimations() { const { isGasEstimatesLoading, gasFeeEstimates } = useGasFeeEstimates(); + const dispatch = useDispatch(); + + const isGasLoadingAnimationActive = useSelector( + getGasLoadingAnimationIsShowing, + ); // Do the animation only when gas prices have changed... const lastGasEstimates = useRef(gasFeeEstimates); @@ -24,5 +34,17 @@ export function useShouldAnimateGasEstimations() { const showLoadingAnimation = isGasEstimatesLoading || (gasEstimatesChanged && !gasJustLoaded); - return showLoadingAnimation; + useEffect(() => { + if ( + isGasLoadingAnimationActive === false && + showLoadingAnimation === true + ) { + dispatch(toggleGasLoadingAnimation(true)); + + setTimeout(() => { + console.log('Killing the toggleGasLoadingAnimation to false'); + dispatch(toggleGasLoadingAnimation(false)); + }, 2000); + } + }, [dispatch, isGasLoadingAnimationActive, showLoadingAnimation]); } diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index 164573169..f1bb14ba5 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -107,6 +107,7 @@ export default class ConfirmTransactionBase extends Component { setDefaultHomeActiveTabName: PropTypes.func, primaryTotalTextOverride: PropTypes.string, secondaryTotalTextOverride: PropTypes.string, + gasIsLoading: PropTypes.bool, }; state = { @@ -772,6 +773,7 @@ export default class ConfirmTransactionBase extends Component { hideSenderToRecipient, showAccountInHeader, txData, + gasIsLoading, } = this.props; const { submitting, @@ -838,7 +840,7 @@ export default class ConfirmTransactionBase extends Component { lastTx={lastTx} ofText={ofText} requestsWaitingText={requestsWaitingText} - disabled={!valid || submitting} + disabled={!valid || submitting || gasIsLoading} onEdit={() => this.handleEdit()} onCancelAll={() => this.handleCancelAll()} onCancel={() => this.handleCancel()} diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index 90d349ad8..561ee28ec 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -32,7 +32,11 @@ import { import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { transactionMatchesNetwork } from '../../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; -import { updateTransactionGasFees } from '../../ducks/metamask/metamask'; +import { + updateTransactionGasFees, + getIsGasEstimatesLoading, +} from '../../ducks/metamask/metamask'; +import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app'; import ConfirmTransactionBase from './confirm-transaction-base.component'; const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { @@ -60,6 +64,10 @@ const mapStateToProps = (state, ownProps) => { const { id: paramsTransactionId } = params; const isMainnet = getIsMainnet(state); const supportsEIP1599 = checkNetworkAndAccountSupports1559(state); + + const isGasEstimatesLoading = getIsGasEstimatesLoading(state); + const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state); + const { confirmTransaction, metamask } = state; const { ensResolutionsByAddress, @@ -185,6 +193,7 @@ const mapStateToProps = (state, ownProps) => { isEthGasPrice, noGasPrice, supportsEIP1599, + gasIsLoading: isGasEstimatesLoading || gasLoadingAnimationIsShowing, }; }; diff --git a/ui/pages/swaps/fee-card/fee-card.test.js b/ui/pages/swaps/fee-card/fee-card.test.js index b6183fcca..9327ad69e 100644 --- a/ui/pages/swaps/fee-card/fee-card.test.js +++ b/ui/pages/swaps/fee-card/fee-card.test.js @@ -1,6 +1,14 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { useSelector } from 'react-redux'; + +import { checkNetworkAndAccountSupports1559 } from '../../../selectors'; +import { + getGasEstimateType, + getGasFeeEstimates, + getIsGasEstimatesLoading, +} from '../../../ducks/metamask/metamask'; import { renderWithProvider, @@ -13,21 +21,34 @@ import FeeCard from '.'; const middleware = [thunk]; -jest.mock('../../../hooks/useGasFeeEstimates', () => { +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { - useGasFeeEstimates: () => { - return { - gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(), - gasEstimateType: 'fee-market', - estimatedGasFeeTimeBounds: undefined, - isGasEstimatesLoading: false, - }; - }, + ...actual, + useSelector: jest.fn(), }; }); +const generateUseSelectorRouter = () => (selector) => { + if (selector === checkNetworkAndAccountSupports1559) { + return true; + } + if (selector === getGasEstimateType) { + return 'fee-market'; + } + if (selector === getGasFeeEstimates) { + return MOCKS.createGasFeeEstimatesForFeeMarket(); + } + if (selector === getIsGasEstimatesLoading) { + return false; + } + return undefined; +}; + setBackgroundConnection({ getGasFeeTimeEstimate: jest.fn(), + getGasFeeEstimatesAndStartPolling: jest.fn(), }); const createProps = (customProps = {}) => { @@ -65,6 +86,7 @@ const createProps = (customProps = {}) => { describe('FeeCard', () => { it('renders the component with initial props', () => { + useSelector.mockImplementation(generateUseSelectorRouter()); const props = createProps(); const { getByText } = renderWithProvider(); expect(getByText('Using the best quote')).toBeInTheDocument(); diff --git a/ui/store/actionConstants.js b/ui/store/actionConstants.js index 8046ea05d..d62e3f5b1 100644 --- a/ui/store/actionConstants.js +++ b/ui/store/actionConstants.js @@ -102,3 +102,5 @@ export const SET_OPEN_METAMASK_TAB_IDS = 'SET_OPEN_METAMASK_TAB_IDS'; // Home Screen export const HIDE_WHATS_NEW_POPUP = 'HIDE_WHATS_NEW_POPUP'; + +export const TOGGLE_GAS_LOADING_ANIMATION = 'TOGGLE_GAS_LOADING_ANIMATION';