diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b3cbda168..f4ad0c777 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -710,6 +710,9 @@ "editGasEducationModalTitle": { "message": "How to choose?" }, + "editGasFeeModalTitle": { + "message": "Edit gas fee" + }, "editGasHigh": { "message": "High" }, @@ -1011,6 +1014,9 @@ "message": "Gas limit must be at least $1", "description": "$1 is the custom gas limit, in decimal." }, + "gasOption": { + "message": "Gas option" + }, "gasPrice": { "message": "Gas Price (GWEI)" }, @@ -1032,10 +1038,18 @@ "gasPriceLabel": { "message": "Gas price" }, + "gasTimingHoursShort": { + "message": "$1 hrs", + "description": "$1 represents a number of hours" + }, "gasTimingMinutes": { "message": "$1 minutes", "description": "$1 represents a number of minutes" }, + "gasTimingMinutesShort": { + "message": "$1 min", + "description": "$1 represents a number of minutes" + }, "gasTimingNegative": { "message": "Maybe in $1", "description": "$1 represents an amount of time" @@ -1048,6 +1062,10 @@ "message": "$1 seconds", "description": "$1 represents a number of seconds" }, + "gasTimingSecondsShort": { + "message": "$1 sec", + "description": "$1 represents a number of seconds" + }, "gasTimingVeryPositive": { "message": "Very likely in < $1", "description": "$1 represents an amount of time" @@ -2793,6 +2811,9 @@ "thisWillCreate": { "message": "This will create a new wallet and Secret Recovery Phrase" }, + "time": { + "message": "Time" + }, "tips": { "message": "Tips" }, diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index d76c2998f..e5a5a1882 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -13,6 +13,8 @@ @import 'connected-status-indicator/index'; @import 'edit-gas-display/index'; @import 'edit-gas-display-education/index'; +@import 'edit-gas-fee-popover/index'; +@import 'edit-gas-fee-popover/edit-gas-item/index'; @import 'gas-customization/gas-modal-page-container/index'; @import 'gas-customization/gas-price-button-group/index'; @import 'gas-customization/index'; diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 117318e3d..33c07d56a 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -8,12 +8,16 @@ import { GasFeeContextProvider } from '../../../contexts/gasFee'; import ErrorMessage from '../../ui/error-message'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; import Dialog from '../../ui/dialog'; +import EditGasFeePopover from '../edit-gas-fee-popover/edit-gas-fee-popover'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, ConfirmPageContainerNavigation, } from '.'; +// eslint-disable-next-line prefer-destructuring +const EIP_1559_V2 = process.env.EIP_1559_V2; + export default class ConfirmPageContainer extends Component { static contextTypes = { t: PropTypes.func, @@ -225,13 +229,16 @@ export default class ConfirmPageContainer extends Component { )} )} - {editingGas && ( + {editingGas && !EIP_1559_V2 && ( )} + {editingGas && EIP_1559_V2 && ( + + )} ); diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.js b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.js new file mode 100644 index 000000000..84f31a256 --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useI18nContext } from '../../../hooks/useI18nContext'; +import Popover from '../../ui/popover'; +import I18nValue from '../../ui/i18n-value'; +import LoadingHeartBeat from '../../ui/loading-heartbeat'; + +import EditGasItem from './edit-gas-item'; + +const EditGasFeePopover = ({ onClose }) => { + const t = useI18nContext(); + + return ( + + <> + {process.env.IN_TEST === 'true' ? null : } +
+
+
+ + + + + + + + + +
+ + + +
+
+ +
+ ); +}; + +EditGasFeePopover.propTypes = { + onClose: PropTypes.func, +}; + +export default EditGasFeePopover; diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js new file mode 100644 index 000000000..cd4f772dc --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js @@ -0,0 +1,93 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; + +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { ETH } from '../../../helpers/constants/common'; +import configureStore from '../../../store/store'; +import { GasFeeContextProvider } from '../../../contexts/gasFee'; + +import EditGasFeePopover from './edit-gas-fee-popover'; + +jest.mock('../../../store/actions', () => ({ + disconnectGasFeeEstimatePoller: jest.fn(), + getGasFeeEstimatesAndStartPolling: jest + .fn() + .mockImplementation(() => Promise.resolve()), + addPollingTokenToAppState: jest.fn(), +})); + +const MOCK_FEE_ESTIMATE = { + low: { + minWaitTimeEstimate: 360000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 30000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', +}; + +const renderComponent = () => { + const store = configureStore({ + metamask: { + nativeCurrency: ETH, + provider: {}, + cachedBalances: {}, + accounts: { + '0xAddress': { + address: '0xAddress', + balance: '0x176e5b6f173ebe66', + }, + }, + selectedAddress: '0xAddress', + featureFlags: { advancedInlineGas: true }, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + }, + }); + + return renderWithProvider( + + + , + store, + ); +}; + +describe('EditGasFeePopover', () => { + it('should renders low / medium / high options', () => { + renderComponent(); + + expect(screen.queryByText('🐢')).toBeInTheDocument(); + expect(screen.queryByText('🦊')).toBeInTheDocument(); + expect(screen.queryByText('🦍')).toBeInTheDocument(); + expect(screen.queryByText('Low')).toBeInTheDocument(); + expect(screen.queryByText('Market')).toBeInTheDocument(); + expect(screen.queryByText('Aggressive')).toBeInTheDocument(); + }); + + it('should show time estimates', () => { + renderComponent(); + console.log(document.body.innerHTML); + expect(screen.queryByText('6 min')).toBeInTheDocument(); + expect(screen.queryByText('30 sec')).toBeInTheDocument(); + expect(screen.queryByText('15 sec')).toBeInTheDocument(); + }); + + it('should show gas fee estimates', () => { + renderComponent(); + expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument(); + expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument(); + expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.js b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.js new file mode 100644 index 000000000..59cf3ac01 --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils'; +import { PRIORITY_LEVEL_ICON_MAP } from '../../../../helpers/constants/gas'; +import { PRIMARY } from '../../../../helpers/constants/common'; +import { + decGWEIToHexWEI, + decimalToHex, +} from '../../../../helpers/utils/conversions.util'; +import { toHumanReadableTime } from '../../../../helpers/utils/util'; +import { useGasFeeContext } from '../../../../contexts/gasFee'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import I18nValue from '../../../ui/i18n-value'; +import InfoTooltip from '../../../ui/info-tooltip'; +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'; + +const EditGasItem = ({ estimateType, onClose }) => { + const { + estimateUsed, + gasFeeEstimates, + gasLimit, + setEstimateToUse, + updateTransaction, + } = useGasFeeContext(); + const t = useI18nContext(); + + const { minWaitTimeEstimate, suggestedMaxFeePerGas } = + gasFeeEstimates[estimateType] || {}; + const hexMaximumTransactionFee = suggestedMaxFeePerGas + ? getMaximumGasTotalInHexWei({ + gasLimit: decimalToHex(gasLimit), + maxFeePerGas: decGWEIToHexWEI(suggestedMaxFeePerGas), + }) + : null; + + const onOptionSelect = () => { + setEstimateToUse(estimateType); + updateTransaction(estimateType); + onClose(); + }; + + return ( +
+ + + {PRIORITY_LEVEL_ICON_MAP[estimateType]} + + + + + {minWaitTimeEstimate && toHumanReadableTime(t, minWaitTimeEstimate)} + + + + + + + +
+ ); +}; + +EditGasItem.propTypes = { + estimateType: PropTypes.string, + onClose: PropTypes.func, +}; + +export default EditGasItem; diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js new file mode 100644 index 000000000..4d8a40f88 --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js @@ -0,0 +1,93 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; + +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import { ETH } from '../../../../helpers/constants/common'; +import configureStore from '../../../../store/store'; +import { GasFeeContextProvider } from '../../../../contexts/gasFee'; + +import EditGasItem from './edit-gas-item'; + +jest.mock('../../../../store/actions', () => ({ + disconnectGasFeeEstimatePoller: jest.fn(), + getGasFeeEstimatesAndStartPolling: jest + .fn() + .mockImplementation(() => Promise.resolve()), + addPollingTokenToAppState: jest.fn(), +})); + +const MOCK_FEE_ESTIMATE = { + low: { + minWaitTimeEstimate: 360000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 30000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', +}; + +const renderComponent = (props) => { + const store = configureStore({ + metamask: { + nativeCurrency: ETH, + provider: {}, + cachedBalances: {}, + accounts: { + '0xAddress': { + address: '0xAddress', + balance: '0x176e5b6f173ebe66', + }, + }, + selectedAddress: '0xAddress', + featureFlags: { advancedInlineGas: true }, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + }, + }); + + return renderWithProvider( + + + , + store, + ); +}; + +describe('EditGasItem', () => { + it('should renders low gas estimate options for estimateType low', () => { + renderComponent({ estimateType: 'low' }); + + expect(screen.queryByText('🐢')).toBeInTheDocument(); + expect(screen.queryByText('Low')).toBeInTheDocument(); + expect(screen.queryByText('6 min')).toBeInTheDocument(); + expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument(); + }); + + it('should renders market gas estimate options for estimateType medium', () => { + renderComponent({ estimateType: 'medium' }); + + expect(screen.queryByText('🦊')).toBeInTheDocument(); + expect(screen.queryByText('Market')).toBeInTheDocument(); + expect(screen.queryByText('30 sec')).toBeInTheDocument(); + expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument(); + }); + + it('should renders aggressive gas estimate options for estimateType high', () => { + renderComponent({ estimateType: 'high' }); + + expect(screen.queryByText('🦍')).toBeInTheDocument(); + expect(screen.queryByText('15 sec')).toBeInTheDocument(); + expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.js b/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.js new file mode 100644 index 000000000..3ba916857 --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.js @@ -0,0 +1 @@ +export { default } from './edit-gas-item'; diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.scss b/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.scss new file mode 100644 index 000000000..17072a813 --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.scss @@ -0,0 +1,56 @@ +.edit-gas-item { + border-radius: 24px; + color: $ui-4; + cursor: pointer; + font-size: 12px; + margin: 12px 0; + padding: 4px 12px; + height: 32px; + + &--selected { + background-color: $ui-1; + } + + &__name { + display: inline-block; + color: $ui-black; + font-size: 12px; + font-weight: bold; + width: 40%; + } + + &__icon { + margin-right: 4px; + } + + &__time-estimate { + display: inline-block; + width: 20%; + } + + &__fee-estimate { + display: inline-block; + width: 30%; + white-space: nowrap; + } + + &__tooltip { + display: inline-block; + text-align: right; + width: 10%; + + .info-tooltip { + display: inline-block; + } + } + + &__time-estimate-low, + &__fee-estimate-high { + color: $secondary-1; + } + + &__time-estimate-medium, + &__time-estimate-high { + color: $success-3; + } +} diff --git a/ui/components/app/edit-gas-fee-popover/index.js b/ui/components/app/edit-gas-fee-popover/index.js new file mode 100644 index 000000000..d2e6862b9 --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/index.js @@ -0,0 +1 @@ +export { default } from './edit-gas-fee-popover'; diff --git a/ui/components/app/edit-gas-fee-popover/index.scss b/ui/components/app/edit-gas-fee-popover/index.scss new file mode 100644 index 000000000..0e149efbc --- /dev/null +++ b/ui/components/app/edit-gas-fee-popover/index.scss @@ -0,0 +1,35 @@ +.edit-gas-fee-popover { + @media screen and (min-width: $break-large) { + max-height: 84vh; + } + + &__wrapper { + border-top: 1px solid $ui-grey; + } + + &__content { + padding: 16px 12px; + + &__header { + color: $ui-4; + font-size: 10px; + font-weight: 700; + margin: 0 12px; + + &-option { + display: inline-block; + width: 40%; + } + + &-time { + display: inline-block; + width: 20%; + } + + &-max-fee { + display: inline-block; + width: 30%; + } + } + } +} diff --git a/ui/components/app/gas-timing/gas-timing.component.js b/ui/components/app/gas-timing/gas-timing.component.js index 41fc6bed5..1da1c6a43 100644 --- a/ui/components/app/gas-timing/gas-timing.component.js +++ b/ui/components/app/gas-timing/gas-timing.component.js @@ -50,7 +50,7 @@ export default function GasTiming({ const [customEstimatedTime, setCustomEstimatedTime] = useState(null); const t = useContext(I18nContext); - const { estimateToUse } = useGasFeeContext(); + const { estimateUsed } = useGasFeeContext(); // If the user has chosen a value lower than the low gas fee estimate, // We'll need to use the useEffect hook below to make a call to calculate @@ -155,7 +155,7 @@ export default function GasTiming({ ]); } } else { - if (!EIP_1559_V2 || estimateToUse === 'low') { + if (!EIP_1559_V2 || estimateUsed === 'low') { attitude = 'negative'; } // If the user has chosen a value less than our low estimate, diff --git a/ui/components/app/transaction-detail/transaction-detail.component.js b/ui/components/app/transaction-detail/transaction-detail.component.js index b06d43af8..64677883a 100644 --- a/ui/components/app/transaction-detail/transaction-detail.component.js +++ b/ui/components/app/transaction-detail/transaction-detail.component.js @@ -1,40 +1,29 @@ -import React, { useContext } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { I18nContext } from '../../../contexts/i18n'; import { useGasFeeContext } from '../../../contexts/gasFee'; import InfoTooltip from '../../ui/info-tooltip/info-tooltip'; import Typography from '../../ui/typography/typography'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; import { COLORS } from '../../../helpers/constants/design-system'; - -const GasLevelIconMap = { - low: '🐢', - medium: '🦊', - high: '🦍', - dappSuggested: '🌐', - custom: '⚙', -}; +import { PRIORITY_LEVEL_ICON_MAP } from '../../../helpers/constants/gas'; +import { useI18nContext } from '../../../hooks/useI18nContext'; export default function TransactionDetail({ rows = [], onEdit }) { // eslint-disable-next-line prefer-destructuring const EIP_1559_V2 = process.env.EIP_1559_V2; - const t = useContext(I18nContext); + const t = useI18nContext(); const { - estimateToUse, gasLimit, gasPrice, - isUsingDappSuggestedGasFees, + estimateUsed, maxFeePerGas, maxPriorityFeePerGas, transaction, supportsEIP1559, } = useGasFeeContext(); - const estimateUsed = isUsingDappSuggestedGasFees - ? 'dappSuggested' - : estimateToUse; if (EIP_1559_V2 && estimateUsed) { return ( @@ -42,7 +31,7 @@ export default function TransactionDetail({ rows = [], onEdit }) {