+ {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';