1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-26 12:29:06 +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 Dan Miller
parent 99fecbf6d2
commit 0377e2c64e
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 { 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}
/>
<FormField
titleText={t('maxFee')}
@ -88,6 +97,7 @@ export default function AdvancedGasControls({
? getGasFormErrorText(gasErrors.maxFee, t)
: null
}
disabled={disableFormFields}
/>
</>
) : (
@ -107,6 +117,7 @@ export default function AdvancedGasControls({
? getGasFormErrorText(gasErrors.gasPrice, t)
: null
}
disabled={disableFormFields}
/>
</>
)}

View File

@ -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({
<Button
type="primary"
onClick={onSubmit}
disabled={hasGasErrors || isGasEstimatesLoading || balanceError}
disabled={
hasGasErrors ||
isGasEstimatesLoading ||
balanceError ||
gasLoadingAnimationIsShowing
}
>
{footerButtonText}
</Button>
@ -205,9 +211,7 @@ export default function EditGasPopover({
<EditGasDisplayEducation />
) : (
<>
{process.env.IN_TEST === 'true' ? null : (
<LoadingHeartBeat active={shouldAnimate} />
)}
{process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />}
<EditGasDisplay
showEducationButton={showEducationButton}
warning={warning}

View File

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

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { shallow } from 'enzyme';
import sinon from 'sinon';
@ -9,7 +10,13 @@ import messages from '../../../../app/_locales/en/messages.json';
import { getMessage } from '../../../helpers/utils/i18n-helper';
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 '.';
@ -35,58 +42,103 @@ const MOCK_FEE_ESTIMATE = {
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', () => {
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(<GasTiming />);
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(<GasTiming maxPriorityFeePerGas={10} />);
const wrapper = shallow(<GasTiming maxPriorityFeePerGas="10" />);
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(<GasTiming maxPriorityFeePerGas={8} />);
const wrapper = shallow(<GasTiming maxPriorityFeePerGas="8" />);
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(<GasTiming maxPriorityFeePerGas={3} />);
const wrapper = shallow(<GasTiming maxPriorityFeePerGas="3" />);
expect(wrapper.html()).toContain('gasTimingNegative');
});
});

View File

@ -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 (
<div className="transaction-detail">
{process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />}
{onEdit && (
<div className="transaction-detail-edit">
<button onClick={onEdit}>{t('edit')}</button>
<button
onClick={onEdit}
disabled={isGasEstimatesLoading || gasLoadingAnimationIsShowing}
>
{t('edit')}
</button>
</div>
)}
<div className="transaction-detail-rows">{rows}</div>

View File

@ -28,6 +28,7 @@ export default function FormField({
autoFocus,
password,
allowDecimals,
disabled,
}) {
return (
<div
@ -81,6 +82,7 @@ export default function FormField({
detailText={detailText}
autoFocus={autoFocus}
allowDecimals={allowDecimals}
disabled={disabled}
/>
) : (
<input
@ -91,6 +93,7 @@ export default function FormField({
value={value}
type={password ? 'password' : 'text'}
autoFocus={autoFocus}
disabled={disabled}
/>
)}
{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,
};

View File

@ -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 (
<div
className={classNames('loading-heartbeat', {
[LOADING_CLASS]: active,
})}
ref={heartNode}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
></div>
);
}
LoadingHeartBeat.propTypes = {
active: PropTypes.bool,
};

View File

@ -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;
}
}

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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,
}),
);

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 {
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]);
}

View File

@ -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()}

View File

@ -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,
};
};

View File

@ -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(<FeeCard {...props} />);
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
export const HIDE_WHATS_NEW_POPUP = 'HIDE_WHATS_NEW_POPUP';
export const TOGGLE_GAS_LOADING_ANIMATION = 'TOGGLE_GAS_LOADING_ANIMATION';