diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2878872a9..08f6039bf 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -163,6 +163,9 @@ "advanced": { "message": "Advanced" }, + "advancedGasFeeModalTitle": { + "message": "Advanced gas fee" + }, "advancedGasPriceTitle": { "message": "Gas price" }, diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js new file mode 100644 index 000000000..41bfced9a --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js @@ -0,0 +1,35 @@ +import React from 'react'; + +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { useTransactionModalContext } from '../../../contexts/transaction-modal'; + +import Box from '../../ui/box'; +import Button from '../../ui/button'; +import I18nValue from '../../ui/i18n-value'; +import Popover from '../../ui/popover'; + +const AdvancedGasFeePopover = () => { + const t = useI18nContext(); + const { closeModal, currentModal } = useTransactionModalContext(); + + if (currentModal !== 'advancedGasFee') return null; + + // todo: align styles to edit gas fee modal + return ( + closeModal('advancedGasFee')} + onClose={() => closeModal('advancedGasFee')} + footer={ + + + + } + > + + + ); +}; + +export default AdvancedGasFeePopover; diff --git a/ui/components/app/advanced-gas-fee-popover/index.js b/ui/components/app/advanced-gas-fee-popover/index.js new file mode 100644 index 000000000..224b2237e --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/index.js @@ -0,0 +1 @@ +export { default } from './advanced-gas-fee-popover'; diff --git a/ui/components/app/advanced-gas-fee-popover/index.scss b/ui/components/app/advanced-gas-fee-popover/index.scss new file mode 100644 index 000000000..1e4959be6 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/index.scss @@ -0,0 +1,8 @@ +.advanced-gas-fee-popover { + .popover-header { + border-radius: 0; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + border-bottom: 1px solid $Grey-200; + } +} diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index e5e245d87..da57af878 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -50,4 +50,5 @@ @import 'transaction-total-banner/index'; @import 'wallet-overview/index'; @import 'whats-new-popup/index'; -@import 'loading-network-screen/index' +@import 'loading-network-screen/index'; +@import 'advanced-gas-fee-popover/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 33c07d56a..90e755b1d 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,7 +8,8 @@ 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 AdvancedGasFeePopover from '../advanced-gas-fee-popover'; +import EditGasFeePopover from '../edit-gas-fee-popover'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, @@ -236,9 +237,8 @@ export default class ConfirmPageContainer extends Component { transaction={currentTransaction} /> )} - {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 index abfdbca98..e738e7a8e 100644 --- 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 @@ -1,8 +1,9 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { PRIORITY_LEVELS } from '../../../../shared/constants/gas'; import { useI18nContext } from '../../../hooks/useI18nContext'; +import { useTransactionModalContext } from '../../../contexts/transaction-modal'; + import I18nValue from '../../ui/i18n-value'; import LoadingHeartBeat from '../../ui/loading-heartbeat'; import Popover from '../../ui/popover'; @@ -12,13 +13,16 @@ import { COLORS } from '../../../helpers/constants/design-system'; import EditGasItem from './edit-gas-item'; import NetworkStatus from './network-status'; -const EditGasFeePopover = ({ onClose }) => { +const EditGasFeePopover = () => { const t = useI18nContext(); + const { closeModal, currentModal } = useTransactionModalContext(); + + if (currentModal !== 'editGasFee') return null; return ( closeModal('editGasFee')} className="edit-gas-fee-popover" > <> @@ -36,27 +40,12 @@ const EditGasFeePopover = ({ onClose }) => { - - - + + + - - + + { ); }; -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 index 2131e0205..de48fd0be 100644 --- 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 @@ -16,6 +16,13 @@ jest.mock('../../../store/actions', () => ({ addPollingTokenToAppState: jest.fn(), })); +jest.mock('../../../contexts/transaction-modal', () => ({ + useTransactionModalContext: () => ({ + closeModal: () => undefined, + currentModal: 'editGasFee', + }), +})); + const MOCK_FEE_ESTIMATE = { low: { minWaitTimeEstimate: 360000, 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 index 9fe5d5889..bdef6d77d 100644 --- 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 @@ -16,13 +16,14 @@ import { getAdvancedGasFeeValues } from '../../../../selectors'; import { toHumanReadableTime } from '../../../../helpers/utils/util'; import { useGasFeeContext } from '../../../../contexts/gasFee'; import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { useTransactionModalContext } from '../../../../contexts/transaction-modal'; import I18nValue from '../../../ui/i18n-value'; import InfoTooltip from '../../../ui/info-tooltip'; import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'; import { useCustomTimeEstimate } from './useCustomTimeEstimate'; -const EditGasItem = ({ priorityLevel, onClose }) => { +const EditGasItem = ({ priorityLevel }) => { const { estimateUsed, gasFeeEstimates, @@ -34,6 +35,8 @@ const EditGasItem = ({ priorityLevel, onClose }) => { } = useGasFeeContext(); const t = useI18nContext(); const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); + const { closeModal, openModal } = useTransactionModalContext(); + let maxFeePerGas; let maxPriorityFeePerGas; let minWaitTime; @@ -83,11 +86,12 @@ const EditGasItem = ({ priorityLevel, onClose }) => { : null; const onOptionSelect = () => { - if (priorityLevel !== PRIORITY_LEVELS.CUSTOM) { + if (priorityLevel === PRIORITY_LEVELS.CUSTOM) { + openModal('advancedGasFee'); + } else { updateTransactionUsingGasFeeEstimates(priorityLevel); + closeModal('editGasFee'); } - // todo: open advance modal if priorityLevel is custom - onClose(); }; return ( @@ -144,7 +148,6 @@ const EditGasItem = ({ priorityLevel, onClose }) => { EditGasItem.propTypes = { priorityLevel: PropTypes.string, - onClose: PropTypes.func, }; export default EditGasItem; diff --git a/ui/components/app/transaction-detail-item/index.scss b/ui/components/app/transaction-detail-item/index.scss index 45c6e4f9b..5732e92c5 100644 --- a/ui/components/app/transaction-detail-item/index.scss +++ b/ui/components/app/transaction-detail-item/index.scss @@ -13,7 +13,7 @@ display: flex; flex-wrap: wrap; justify-content: end; - width: 65%; + width: 55%; } .info-tooltip { diff --git a/ui/components/app/transaction-detail/transaction-detail.component.js b/ui/components/app/transaction-detail/transaction-detail.component.js index 2f000e59e..815861228 100644 --- a/ui/components/app/transaction-detail/transaction-detail.component.js +++ b/ui/components/app/transaction-detail/transaction-detail.component.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useGasFeeContext } from '../../../contexts/gasFee'; +import { useTransactionModalContext } from '../../../contexts/transaction-modal'; import InfoTooltip from '../../ui/info-tooltip/info-tooltip'; import Typography from '../../ui/typography/typography'; @@ -22,12 +23,13 @@ export default function TransactionDetail({ rows = [], onEdit }) { maxPriorityFeePerGas, transaction, } = useGasFeeContext(); + const { openModal } = useTransactionModalContext(); if (EIP_1559_V2 && estimateUsed) { return ( - + openModal('editGasFee')}> {`${PRIORITY_LEVEL_ICON_MAP[estimateUsed]} `} @@ -37,7 +39,9 @@ export default function TransactionDetail({ rows = [], onEdit }) { {estimateUsed === 'custom' && onEdit && ( - {t('edit')} + openModal('advancedGasFee')}> + {t('edit')} + )} {estimateUsed === 'dappSuggested' && ( { + const [openModals, setOpenModals] = useState([]); + const metricsEvent = useMetaMetricsContext(); + const { transaction: { origin } = {} } = useGasFeeContext(); + + const captureEvent = () => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'User clicks "Edit" on gas', + }, + customVariables: { + recipientKnown: null, + functionType: + actionKey || + getMethodName(methodData.name) || + TRANSACTION_TYPES.CONTRACT_INTERACTION, + origin, + }, + }); + }; + + const closeModal = (modalName) => { + const index = openModals.indexOf(modalName); + if (openModals < 0) return; + const modals = [...openModals]; + modals.splice(index, 1); + setOpenModals(modals); + }; + + const openModal = (modalName) => { + if (openModals.includes(modalName)) return; + captureEvent(); + const modals = [...openModals]; + modals.push(modalName); + setOpenModals(modals); + }; + + return ( + + {children} + + ); +}; + +export function useTransactionModalContext() { + return useContext(TransactionModalContext); +} + +TransactionModalContextProvider.propTypes = { + actionKey: PropTypes.string, + children: PropTypes.node.isRequired, + methodData: PropTypes.object, +}; diff --git a/ui/helpers/utils/metric.test.js b/ui/helpers/utils/metric.test.js new file mode 100644 index 000000000..b3a4981d5 --- /dev/null +++ b/ui/helpers/utils/metric.test.js @@ -0,0 +1,13 @@ +import { getMethodName } from './metrics'; + +describe('getMethodName', () => { + it('should get correct method names', () => { + expect(getMethodName(undefined)).toStrictEqual(''); + expect(getMethodName({})).toStrictEqual(''); + expect(getMethodName('confirm')).toStrictEqual('confirm'); + expect(getMethodName('balanceOf')).toStrictEqual('balance Of'); + expect(getMethodName('ethToTokenSwapInput')).toStrictEqual( + 'eth To Token Swap Input', + ); + }); +}); diff --git a/ui/helpers/utils/metrics.js b/ui/helpers/utils/metrics.js new file mode 100644 index 000000000..c085545f2 --- /dev/null +++ b/ui/helpers/utils/metrics.js @@ -0,0 +1,10 @@ +export function getMethodName(camelCase) { + if (!camelCase || typeof camelCase !== 'string') { + return ''; + } + + return camelCase + .replace(/([a-z])([A-Z])/gu, '$1 $2') + .replace(/([A-Z])([a-z])/gu, ' $1$2') + .replace(/ +/gu, ' '); +} 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 92d9d2cb3..62ff262b5 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -25,9 +25,11 @@ import { TRANSACTION_TYPES, TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; +import { getMethodName } from '../../helpers/utils/metrics'; import { getTransactionTypeTitle } from '../../helpers/utils/transactions.util'; import { toBuffer } from '../../../shared/modules/buffer-utils'; +import { TransactionModalContextProvider } from '../../contexts/transaction-modal'; import TransactionDetail from '../../components/app/transaction-detail/transaction-detail.component'; import TransactionDetailItem from '../../components/app/transaction-detail-item/transaction-detail-item.component'; import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip'; @@ -913,6 +915,7 @@ export default class ConfirmTransactionBase extends Component { render() { const { t } = this.context; const { + actionKey, fromName, fromAddress, toName, @@ -976,71 +979,65 @@ export default class ConfirmTransactionBase extends Component { } } return ( - this.handleNextTx(txId)} - firstTx={firstTx} - lastTx={lastTx} - ofText={ofText} - requestsWaitingText={requestsWaitingText} - hideConfirmAnyways={!isDisabled()} - disabled={ - renderSimulationFailureWarning || - !valid || - submitting || - hardwareWalletRequiresConnection || - (gasIsLoading && !gasFeeIsCustom) - } - onEdit={() => this.handleEdit()} - onCancelAll={() => this.handleCancelAll()} - onCancel={() => this.handleCancel()} - onSubmit={() => this.handleSubmit()} - onConfirmAnyways={() => this.handleConfirmAnyways()} - hideSenderToRecipient={hideSenderToRecipient} - origin={txData.origin} - ethGasPriceWarning={ethGasPriceWarning} - editingGas={editingGas} - handleCloseEditGas={() => this.handleCloseEditGas()} - currentTransaction={txData} - /> + + this.handleNextTx(txId)} + firstTx={firstTx} + lastTx={lastTx} + ofText={ofText} + requestsWaitingText={requestsWaitingText} + hideConfirmAnyways={!isDisabled()} + disabled={ + renderSimulationFailureWarning || + !valid || + submitting || + hardwareWalletRequiresConnection || + (gasIsLoading && !gasFeeIsCustom) + } + onEdit={() => this.handleEdit()} + onCancelAll={() => this.handleCancelAll()} + onCancel={() => this.handleCancel()} + onSubmit={() => this.handleSubmit()} + onConfirmAnyways={() => this.handleConfirmAnyways()} + hideSenderToRecipient={hideSenderToRecipient} + origin={txData.origin} + ethGasPriceWarning={ethGasPriceWarning} + editingGas={editingGas} + handleCloseEditGas={() => this.handleCloseEditGas()} + currentTransaction={txData} + /> + ); } } - -export function getMethodName(camelCase) { - if (!camelCase || typeof camelCase !== 'string') { - return ''; - } - - return camelCase - .replace(/([a-z])([A-Z])/gu, '$1 $2') - .replace(/([A-Z])([a-z])/gu, ' $1$2') - .replace(/ +/gu, ' '); -} diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.test.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.test.js deleted file mode 100644 index b4d9caa37..000000000 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import { getMethodName } from './confirm-transaction-base.component'; - -describe('ConfirmTransactionBase Component', () => { - describe('getMethodName', () => { - it('should get correct method names', () => { - expect(getMethodName(undefined)).toStrictEqual(''); - expect(getMethodName({})).toStrictEqual(''); - expect(getMethodName('confirm')).toStrictEqual('confirm'); - expect(getMethodName('balanceOf')).toStrictEqual('balance Of'); - expect(getMethodName('ethToTokenSwapInput')).toStrictEqual( - 'eth To Token Swap Input', - ); - }); - }); -});