1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Restore heartbeat to transaction confirmation, use isGasEstimatesLoading more broadly (#11781)

This commit is contained in:
David Walsh 2021-08-05 18:59:58 -05:00 committed by GitHub
parent 804fefcd36
commit a0bd496d56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 276 additions and 98 deletions

View File

@ -234,6 +234,7 @@ export const createSwapsMockStore = () => {
}, },
}, },
}, },
gasLoadingAnimationIsShowing: false,
}, },
}; };
}; };

View File

@ -7,6 +7,8 @@ import FormField from '../../ui/form-field';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import { getGasFormErrorText } from '../../../helpers/constants/gas'; import { getGasFormErrorText } from '../../../helpers/constants/gas';
import { checkNetworkAndAccountSupports1559 } from '../../../selectors'; import { checkNetworkAndAccountSupports1559 } from '../../../selectors';
import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask';
import { getGasLoadingAnimationIsShowing } from '../../../ducks/app/app';
export default function AdvancedGasControls({ export default function AdvancedGasControls({
gasEstimateType, gasEstimateType,
@ -28,6 +30,12 @@ export default function AdvancedGasControls({
const networkAndAccountSupport1559 = useSelector( const networkAndAccountSupport1559 = useSelector(
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
); );
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
const isGasLoadingAnimationIsShowing = useSelector(
getGasLoadingAnimationIsShowing,
);
const disableFormFields =
isGasEstimatesLoading || isGasLoadingAnimationIsShowing;
const showFeeMarketFields = const showFeeMarketFields =
networkAndAccountSupport1559 && networkAndAccountSupport1559 &&
@ -71,6 +79,7 @@ export default function AdvancedGasControls({
? getGasFormErrorText(gasErrors.maxPriorityFee, t) ? getGasFormErrorText(gasErrors.maxPriorityFee, t)
: null : null
} }
disabled={disableFormFields}
/> />
<FormField <FormField
titleText={t('maxFee')} titleText={t('maxFee')}
@ -88,6 +97,7 @@ export default function AdvancedGasControls({
? getGasFormErrorText(gasErrors.maxFee, t) ? getGasFormErrorText(gasErrors.maxFee, t)
: null : null
} }
disabled={disableFormFields}
/> />
</> </>
) : ( ) : (
@ -107,6 +117,7 @@ export default function AdvancedGasControls({
? getGasFormErrorText(gasErrors.gasPrice, t) ? getGasFormErrorText(gasErrors.gasPrice, t)
: null : null
} }
disabled={disableFormFields}
/> />
</> </>
)} )}

View File

@ -2,7 +2,7 @@ import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs'; 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'; import { EDIT_GAS_MODES, GAS_LIMITS } from '../../../../shared/constants/gas';
@ -45,8 +45,9 @@ export default function EditGasPopover({
const networkAndAccountSupport1559 = useSelector( const networkAndAccountSupport1559 = useSelector(
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
); );
const gasLoadingAnimationIsShowing = useSelector(
const shouldAnimate = useShouldAnimateGasEstimations(); getGasLoadingAnimationIsShowing,
);
const showEducationButton = const showEducationButton =
(mode === EDIT_GAS_MODES.MODIFY_IN_PLACE || (mode === EDIT_GAS_MODES.MODIFY_IN_PLACE ||
@ -192,7 +193,12 @@ export default function EditGasPopover({
<Button <Button
type="primary" type="primary"
onClick={onSubmit} onClick={onSubmit}
disabled={hasGasErrors || isGasEstimatesLoading || balanceError} disabled={
hasGasErrors ||
isGasEstimatesLoading ||
balanceError ||
gasLoadingAnimationIsShowing
}
> >
{footerButtonText} {footerButtonText}
</Button> </Button>
@ -205,9 +211,7 @@ export default function EditGasPopover({
<EditGasDisplayEducation /> <EditGasDisplayEducation />
) : ( ) : (
<> <>
{process.env.IN_TEST === 'true' ? null : ( {process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />}
<LoadingHeartBeat active={shouldAnimate} />
)}
<EditGasDisplay <EditGasDisplay
showEducationButton={showEducationButton} showEducationButton={showEducationButton}
warning={warning} warning={warning}

View File

@ -1,14 +1,20 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates';
import { usePrevious } from '../../../hooks/usePrevious'; import { usePrevious } from '../../../hooks/usePrevious';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import {
getGasEstimateType,
getGasFeeEstimates,
getIsGasEstimatesLoading,
} from '../../../ducks/metamask/metamask';
import Typography from '../../ui/typography/typography'; import Typography from '../../ui/typography/typography';
import { import {
TYPOGRAPHY, TYPOGRAPHY,
@ -33,11 +39,9 @@ export default function GasTiming({
maxFeePerGas = 0, maxFeePerGas = 0,
maxPriorityFeePerGas = 0, maxPriorityFeePerGas = 0,
}) { }) {
const { const gasEstimateType = useSelector(getGasEstimateType);
gasFeeEstimates, const gasFeeEstimates = useSelector(getGasFeeEstimates);
isGasEstimatesLoading, const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
gasEstimateType,
} = useGasFeeEstimates();
const [customEstimatedTime, setCustomEstimatedTime] = useState(null); const [customEstimatedTime, setCustomEstimatedTime] = useState(null);

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import sinon from 'sinon'; import sinon from 'sinon';
@ -9,7 +10,13 @@ import messages from '../../../../app/_locales/en/messages.json';
import { getMessage } from '../../../helpers/utils/i18n-helper'; import { getMessage } from '../../../helpers/utils/i18n-helper';
import * as i18nhooks from '../../../hooks/useI18nContext'; import * as i18nhooks from '../../../hooks/useI18nContext';
import * as useGasFeeEstimatesExport from '../../../hooks/useGasFeeEstimates';
import { checkNetworkAndAccountSupports1559 } from '../../../selectors';
import {
getGasEstimateType,
getGasFeeEstimates,
getIsGasEstimatesLoading,
} from '../../../ducks/metamask/metamask';
import GasTiming from '.'; import GasTiming from '.';
@ -35,58 +42,103 @@ const MOCK_FEE_ESTIMATE = {
estimatedBaseFee: '50', estimatedBaseFee: '50',
}; };
jest.mock('react-redux', () => {
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', () => { describe('Gas timing', () => {
beforeEach(() => { beforeEach(() => {
const useI18nContext = sinon.stub(i18nhooks, 'useI18nContext'); const useI18nContext = sinon.stub(i18nhooks, 'useI18nContext');
useI18nContext.returns((key, variables) => useI18nContext.returns((key, variables) =>
getMessage('en', messages, key, variables), getMessage('en', messages, key, variables),
); );
jest.clearAllMocks();
useSelector.mockImplementation(generateUseSelectorRouter());
}); });
afterEach(() => { afterEach(() => {
sinon.restore(); sinon.restore();
}); });
it('renders nothing when gas is loading', () => { it('renders nothing when gas is loading', () => {
sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ useSelector.mockImplementation(
isGasEstimatesLoading: true, generateUseSelectorRouter({
gasFeeEstimates: null, isGasEstimatesLoading: true,
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasFeeEstimates: null,
}); gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
}),
);
const wrapper = shallow(<GasTiming />); const wrapper = shallow(<GasTiming />);
expect(wrapper.html()).toBeNull(); expect(wrapper.html()).toBeNull();
}); });
it('renders "very likely" when high estimate is chosen', () => { it('renders "very likely" when high estimate is chosen', () => {
sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ useSelector.mockImplementation(
isGasEstimatesLoading: false, generateUseSelectorRouter({
gasFeeEstimates: MOCK_FEE_ESTIMATE, isGasEstimatesLoading: false,
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasFeeEstimates: MOCK_FEE_ESTIMATE,
}); gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
}),
);
const wrapper = shallow(<GasTiming maxPriorityFeePerGas={10} />); const wrapper = shallow(<GasTiming maxPriorityFeePerGas="10" />);
expect(wrapper.html()).toContain('gasTimingVeryPositive'); expect(wrapper.html()).toContain('gasTimingVeryPositive');
}); });
it('renders "likely" when medium estimate is chosen', () => { it('renders "likely" when medium estimate is chosen', () => {
sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ useSelector.mockImplementation(
isGasEstimatesLoading: false, generateUseSelectorRouter({
gasFeeEstimates: MOCK_FEE_ESTIMATE, isGasEstimatesLoading: false,
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasFeeEstimates: MOCK_FEE_ESTIMATE,
}); gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
}),
);
const wrapper = shallow(<GasTiming maxPriorityFeePerGas={8} />); const wrapper = shallow(<GasTiming maxPriorityFeePerGas="8" />);
expect(wrapper.html()).toContain('gasTimingPositive'); expect(wrapper.html()).toContain('gasTimingPositive');
}); });
it('renders "maybe" when low estimate is chosen', () => { it('renders "maybe" when low estimate is chosen', () => {
sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ useSelector.mockImplementation(
isGasEstimatesLoading: false, generateUseSelectorRouter({
gasFeeEstimates: MOCK_FEE_ESTIMATE, isGasEstimatesLoading: false,
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasFeeEstimates: MOCK_FEE_ESTIMATE,
}); gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
}),
);
const wrapper = shallow(<GasTiming maxPriorityFeePerGas={3} />); const wrapper = shallow(<GasTiming maxPriorityFeePerGas="3" />);
expect(wrapper.html()).toContain('gasTimingNegative'); expect(wrapper.html()).toContain('gasTimingNegative');
}); });
}); });

View File

@ -1,18 +1,33 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; 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 { I18nContext } from '../../../contexts/i18n';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
import LoadingHeartBeat from '../../ui/loading-heartbeat';
export default function TransactionDetail({ rows = [], onEdit }) { export default function TransactionDetail({ rows = [], onEdit }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
const gasLoadingAnimationIsShowing = useSelector(
getGasLoadingAnimationIsShowing,
);
return ( return (
<div className="transaction-detail"> <div className="transaction-detail">
{process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />}
{onEdit && ( {onEdit && (
<div className="transaction-detail-edit"> <div className="transaction-detail-edit">
<button onClick={onEdit}>{t('edit')}</button> <button
onClick={onEdit}
disabled={isGasEstimatesLoading || gasLoadingAnimationIsShowing}
>
{t('edit')}
</button>
</div> </div>
)} )}
<div className="transaction-detail-rows">{rows}</div> <div className="transaction-detail-rows">{rows}</div>

View File

@ -28,6 +28,7 @@ export default function FormField({
autoFocus, autoFocus,
password, password,
allowDecimals, allowDecimals,
disabled,
}) { }) {
return ( return (
<div <div
@ -81,6 +82,7 @@ export default function FormField({
detailText={detailText} detailText={detailText}
autoFocus={autoFocus} autoFocus={autoFocus}
allowDecimals={allowDecimals} allowDecimals={allowDecimals}
disabled={disabled}
/> />
) : ( ) : (
<input <input
@ -91,6 +93,7 @@ export default function FormField({
value={value} value={value}
type={password ? 'password' : 'text'} type={password ? 'password' : 'text'}
autoFocus={autoFocus} autoFocus={autoFocus}
disabled={disabled}
/> />
)} )}
{error && ( {error && (
@ -120,6 +123,7 @@ FormField.propTypes = {
numeric: PropTypes.bool, numeric: PropTypes.bool,
password: PropTypes.bool, password: PropTypes.bool,
allowDecimals: PropTypes.bool, allowDecimals: PropTypes.bool,
disabled: PropTypes.bool,
}; };
FormField.defaultProps = { FormField.defaultProps = {
@ -135,4 +139,5 @@ FormField.defaultProps = {
numeric: false, numeric: false,
password: false, password: false,
allowDecimals: true, allowDecimals: true,
disabled: false,
}; };

View File

@ -1,36 +1,25 @@
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import React from 'react';
import React, { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux';
import { getGasLoadingAnimationIsShowing } from '../../../ducks/app/app';
import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations';
export default function LoadingHeartBeat({ active }) { const BASE_CLASS = 'loading-heartbeat';
const heartNode = useRef(null); const LOADING_CLASS = `${BASE_CLASS}--active`;
const LOADING_CLASS = 'loading-heartbeat--active'; export default function LoadingHeartBeat() {
useShouldAnimateGasEstimations();
// When the loading animation completes, remove the className to disappear again const active = useSelector(getGasLoadingAnimationIsShowing);
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]);
return ( return (
<div <div
className={classNames('loading-heartbeat', { className={classNames('loading-heartbeat', {
[LOADING_CLASS]: active, [LOADING_CLASS]: active,
})} })}
ref={heartNode} onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
></div> ></div>
); );
} }
LoadingHeartBeat.propTypes = {
active: PropTypes.bool,
};

View File

@ -7,10 +7,14 @@
opacity: 0; opacity: 0;
background: #fff; background: #fff;
display: none; display: none;
pointer-events: none;
&--active { &--active {
display: block; display: block;
animation: heartbeat 2s ease-in-out; animation-name: heartbeat;
animation-duration: 2s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
} }
} }

View File

@ -11,6 +11,7 @@ export default function NumericInput({
error = '', error = '',
autoFocus = false, autoFocus = false,
allowDecimals = true, allowDecimals = true,
disabled = false,
}) { }) {
return ( return (
<div <div
@ -29,6 +30,7 @@ export default function NumericInput({
}} }}
min="0" min="0"
autoFocus={autoFocus} autoFocus={autoFocus}
disabled={disabled}
/> />
{detailText && ( {detailText && (
<Typography color={COLORS.UI4} variant={TYPOGRAPHY.H7} tag="span"> <Typography color={COLORS.UI4} variant={TYPOGRAPHY.H7} tag="span">
@ -46,4 +48,5 @@ NumericInput.propTypes = {
error: PropTypes.string, error: PropTypes.string,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
allowDecimals: PropTypes.bool, allowDecimals: PropTypes.bool,
disabled: PropTypes.bool,
}; };

View File

@ -53,6 +53,7 @@ export default function reduceApp(state = {}, action) {
singleExceptions: { singleExceptions: {
testKey: null, testKey: null,
}, },
gasLoadingAnimationIsShowing: false,
...state, ...state,
}; };
@ -358,6 +359,12 @@ export default function reduceApp(state = {}, action) {
}, },
}; };
case actionConstants.TOGGLE_GAS_LOADING_ANIMATION:
return {
...appState,
gasLoadingAnimationIsShowing: action.value,
};
default: default:
return appState; return appState;
} }
@ -377,7 +384,15 @@ export function hideWhatsNewPopup() {
}; };
} }
export function toggleGasLoadingAnimation(value) {
return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value };
}
// Selectors // Selectors
export function getQrCodeData(state) { export function getQrCodeData(state) {
return state.appState.qrCodeData; return state.appState.qrCodeData;
} }
export function getGasLoadingAnimationIsShowing(state) {
return state.appState.gasLoadingAnimationIsShowing;
}

View File

@ -4,11 +4,13 @@ import { ALERT_TYPES } from '../../../shared/constants/alerts';
import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; import { NETWORK_TYPE_RPC } from '../../../shared/constants/network';
import { import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
checkNetworkAndAccountSupports1559,
getAddressBook, getAddressBook,
} from '../../selectors'; } from '../../selectors';
import { updateTransaction } from '../../store/actions'; import { updateTransaction } from '../../store/actions';
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util'; import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
export default function reduceMetamask(state = {}, action) { export default function reduceMetamask(state = {}, action) {
const metamaskState = { const metamaskState = {
@ -299,3 +301,24 @@ export function getGasFeeEstimates(state) {
export function getEstimatedGasFeeTimeBounds(state) { export function getEstimatedGasFeeTimeBounds(state) {
return state.metamask.estimatedGasFeeTimeBounds; 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;
}

View File

@ -1,17 +1,12 @@
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
import { import {
getEstimatedGasFeeTimeBounds, getEstimatedGasFeeTimeBounds,
getGasEstimateType, getGasEstimateType,
getGasFeeEstimates, getGasFeeEstimates,
getIsGasEstimatesLoading,
} from '../ducks/metamask/metamask'; } from '../ducks/metamask/metamask';
import { checkNetworkAndAccountSupports1559 } from '../selectors';
import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling'; import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling';
/**
* @typedef {keyof typeof GAS_ESTIMATE_TYPES} GasEstimateTypes
*/
/** /**
* @typedef {object} GasEstimates * @typedef {object} GasEstimates
* @property {GasEstimateTypes} gasEstimateType - The type of estimate provided * @property {GasEstimateTypes} gasEstimateType - The type of estimate provided
@ -35,26 +30,12 @@ import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling';
* @returns {GasFeeEstimates} - GasFeeEstimates object * @returns {GasFeeEstimates} - GasFeeEstimates object
*/ */
export function useGasFeeEstimates() { export function useGasFeeEstimates() {
const networkAndAccountSupports1559 = useSelector(
checkNetworkAndAccountSupports1559,
);
const gasEstimateType = useSelector(getGasEstimateType); const gasEstimateType = useSelector(getGasEstimateType);
const gasFeeEstimates = useSelector(getGasFeeEstimates); const gasFeeEstimates = useSelector(getGasFeeEstimates);
const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds); const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds);
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
useSafeGasEstimatePolling(); 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 { return {
gasFeeEstimates, gasFeeEstimates,
gasEstimateType, gasEstimateType,

View File

@ -5,12 +5,14 @@ import createRandomId from '../../shared/modules/random-id';
import { import {
getGasEstimateType, getGasEstimateType,
getGasFeeEstimates, getGasFeeEstimates,
getIsGasEstimatesLoading,
} from '../ducks/metamask/metamask'; } from '../ducks/metamask/metamask';
import { checkNetworkAndAccountSupports1559 } from '../selectors'; import { checkNetworkAndAccountSupports1559 } from '../selectors';
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling, getGasFeeEstimatesAndStartPolling,
} from '../store/actions'; } from '../store/actions';
import { useGasFeeEstimates } from './useGasFeeEstimates'; import { useGasFeeEstimates } from './useGasFeeEstimates';
jest.mock('../store/actions', () => ({ jest.mock('../store/actions', () => ({
@ -37,6 +39,7 @@ const DEFAULT_OPTS = {
medium: '20', medium: '20',
high: '30', high: '30',
}, },
isGasEstimatesLoading: true,
}; };
const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => { const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => {
@ -52,6 +55,9 @@ const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => {
if (selector === getGasFeeEstimates) { if (selector === getGasFeeEstimates) {
return opts.gasFeeEstimates ?? DEFAULT_OPTS.gasFeeEstimates; return opts.gasFeeEstimates ?? DEFAULT_OPTS.gasFeeEstimates;
} }
if (selector === getIsGasEstimatesLoading) {
return opts.isGasEstimatesLoading ?? DEFAULT_OPTS.isGasEstimatesLoading;
}
return undefined; return undefined;
}; };
@ -68,15 +74,16 @@ describe('useGasFeeEstimates', () => {
disconnectGasFeeEstimatePoller.mockImplementation((token) => { disconnectGasFeeEstimatePoller.mockImplementation((token) => {
tokens = tokens.filter((tkn) => tkn !== token); tokens = tokens.filter((tkn) => tkn !== token);
}); });
useSelector.mockImplementation(generateUseSelectorRouter());
}); });
it('registers with the controller', () => { it('registers with the controller', () => {
useSelector.mockImplementation(generateUseSelectorRouter());
renderHook(() => useGasFeeEstimates()); renderHook(() => useGasFeeEstimates());
expect(tokens).toHaveLength(1); expect(tokens).toHaveLength(1);
}); });
it('clears token with the controller on unmount', async () => { it('clears token with the controller on unmount', async () => {
useSelector.mockImplementation(generateUseSelectorRouter());
renderHook(() => useGasFeeEstimates()); renderHook(() => useGasFeeEstimates());
expect(tokens).toHaveLength(1); expect(tokens).toHaveLength(1);
const expectedToken = tokens[0]; const expectedToken = tokens[0];
@ -87,6 +94,11 @@ describe('useGasFeeEstimates', () => {
}); });
it('works with LEGACY gas prices', () => { it('works with LEGACY gas prices', () => {
useSelector.mockImplementation(
generateUseSelectorRouter({
isGasEstimatesLoading: false,
}),
);
const { const {
result: { current }, result: { current },
} = renderHook(() => useGasFeeEstimates()); } = renderHook(() => useGasFeeEstimates());
@ -104,6 +116,7 @@ describe('useGasFeeEstimates', () => {
generateUseSelectorRouter({ generateUseSelectorRouter({
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
gasFeeEstimates, gasFeeEstimates,
isGasEstimatesLoading: false,
}), }),
); );
@ -145,6 +158,7 @@ describe('useGasFeeEstimates', () => {
checkNetworkAndAccountSupports1559: true, checkNetworkAndAccountSupports1559: true,
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
gasFeeEstimates, gasFeeEstimates,
isGasEstimatesLoading: false,
}), }),
); );

View File

@ -1,10 +1,20 @@
import { useRef } from 'react'; import { useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import {
getGasLoadingAnimationIsShowing,
toggleGasLoadingAnimation,
} from '../ducks/app/app';
import { useGasFeeEstimates } from './useGasFeeEstimates'; import { useGasFeeEstimates } from './useGasFeeEstimates';
export function useShouldAnimateGasEstimations() { export function useShouldAnimateGasEstimations() {
const { isGasEstimatesLoading, gasFeeEstimates } = useGasFeeEstimates(); const { isGasEstimatesLoading, gasFeeEstimates } = useGasFeeEstimates();
const dispatch = useDispatch();
const isGasLoadingAnimationActive = useSelector(
getGasLoadingAnimationIsShowing,
);
// Do the animation only when gas prices have changed... // Do the animation only when gas prices have changed...
const lastGasEstimates = useRef(gasFeeEstimates); const lastGasEstimates = useRef(gasFeeEstimates);
@ -24,5 +34,17 @@ export function useShouldAnimateGasEstimations() {
const showLoadingAnimation = const showLoadingAnimation =
isGasEstimatesLoading || (gasEstimatesChanged && !gasJustLoaded); 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]);
} }

View File

@ -107,6 +107,7 @@ export default class ConfirmTransactionBase extends Component {
setDefaultHomeActiveTabName: PropTypes.func, setDefaultHomeActiveTabName: PropTypes.func,
primaryTotalTextOverride: PropTypes.string, primaryTotalTextOverride: PropTypes.string,
secondaryTotalTextOverride: PropTypes.string, secondaryTotalTextOverride: PropTypes.string,
gasIsLoading: PropTypes.bool,
}; };
state = { state = {
@ -772,6 +773,7 @@ export default class ConfirmTransactionBase extends Component {
hideSenderToRecipient, hideSenderToRecipient,
showAccountInHeader, showAccountInHeader,
txData, txData,
gasIsLoading,
} = this.props; } = this.props;
const { const {
submitting, submitting,
@ -838,7 +840,7 @@ export default class ConfirmTransactionBase extends Component {
lastTx={lastTx} lastTx={lastTx}
ofText={ofText} ofText={ofText}
requestsWaitingText={requestsWaitingText} requestsWaitingText={requestsWaitingText}
disabled={!valid || submitting} disabled={!valid || submitting || gasIsLoading}
onEdit={() => this.handleEdit()} onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()} onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()} onCancel={() => this.handleCancel()}

View File

@ -32,7 +32,11 @@ import {
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { transactionMatchesNetwork } from '../../../shared/modules/transaction.utils'; import { transactionMatchesNetwork } from '../../../shared/modules/transaction.utils';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-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'; import ConfirmTransactionBase from './confirm-transaction-base.component';
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
@ -60,6 +64,10 @@ const mapStateToProps = (state, ownProps) => {
const { id: paramsTransactionId } = params; const { id: paramsTransactionId } = params;
const isMainnet = getIsMainnet(state); const isMainnet = getIsMainnet(state);
const supportsEIP1599 = checkNetworkAndAccountSupports1559(state); const supportsEIP1599 = checkNetworkAndAccountSupports1559(state);
const isGasEstimatesLoading = getIsGasEstimatesLoading(state);
const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state);
const { confirmTransaction, metamask } = state; const { confirmTransaction, metamask } = state;
const { const {
ensResolutionsByAddress, ensResolutionsByAddress,
@ -185,6 +193,7 @@ const mapStateToProps = (state, ownProps) => {
isEthGasPrice, isEthGasPrice,
noGasPrice, noGasPrice,
supportsEIP1599, supportsEIP1599,
gasIsLoading: isGasEstimatesLoading || gasLoadingAnimationIsShowing,
}; };
}; };

View File

@ -1,6 +1,14 @@
import React from 'react'; import React from 'react';
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { useSelector } from 'react-redux';
import { checkNetworkAndAccountSupports1559 } from '../../../selectors';
import {
getGasEstimateType,
getGasFeeEstimates,
getIsGasEstimatesLoading,
} from '../../../ducks/metamask/metamask';
import { import {
renderWithProvider, renderWithProvider,
@ -13,21 +21,34 @@ import FeeCard from '.';
const middleware = [thunk]; const middleware = [thunk];
jest.mock('../../../hooks/useGasFeeEstimates', () => { jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return { return {
useGasFeeEstimates: () => { ...actual,
return { useSelector: jest.fn(),
gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(),
gasEstimateType: 'fee-market',
estimatedGasFeeTimeBounds: undefined,
isGasEstimatesLoading: false,
};
},
}; };
}); });
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({ setBackgroundConnection({
getGasFeeTimeEstimate: jest.fn(), getGasFeeTimeEstimate: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(),
}); });
const createProps = (customProps = {}) => { const createProps = (customProps = {}) => {
@ -65,6 +86,7 @@ const createProps = (customProps = {}) => {
describe('FeeCard', () => { describe('FeeCard', () => {
it('renders the component with initial props', () => { it('renders the component with initial props', () => {
useSelector.mockImplementation(generateUseSelectorRouter());
const props = createProps(); const props = createProps();
const { getByText } = renderWithProvider(<FeeCard {...props} />); const { getByText } = renderWithProvider(<FeeCard {...props} />);
expect(getByText('Using the best quote')).toBeInTheDocument(); expect(getByText('Using the best quote')).toBeInTheDocument();

View File

@ -102,3 +102,5 @@ export const SET_OPEN_METAMASK_TAB_IDS = 'SET_OPEN_METAMASK_TAB_IDS';
// Home Screen // Home Screen
export const HIDE_WHATS_NEW_POPUP = 'HIDE_WHATS_NEW_POPUP'; export const HIDE_WHATS_NEW_POPUP = 'HIDE_WHATS_NEW_POPUP';
export const TOGGLE_GAS_LOADING_ANIMATION = 'TOGGLE_GAS_LOADING_ANIMATION';