diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 8ccd4bfa6..d77ad124f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -574,6 +574,12 @@ "editContact": { "message": "Edit Contact" }, + "editNonceField": { + "message": "Edit Nonce" + }, + "editNonceMessage": { + "message": "This is an advanced feature, use cautiously." + }, "editPermission": { "message": "Edit Permission" }, @@ -1194,6 +1200,9 @@ "noWebcamFoundTitle": { "message": "Webcam not found" }, + "nonce": { + "message": "Nonce" + }, "nonceField": { "message": "Customize transaction nonce" }, diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index b927796ff..a1c4e3878 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1289,9 +1289,7 @@ describe('MetaMask', function () { }); it('customizes gas', async function () { - await driver.clickElement( - '.confirm-approve-content__small-blue-text.cursor-pointer', - ); + await driver.clickElement('.confirm-approve-content__small-blue-text'); await driver.delay(regularDelayMs); // wait for gas modal to be visible @@ -1323,9 +1321,9 @@ describe('MetaMask', function () { it('edits the permission', async function () { const editButtons = await driver.findClickableElements( - '.confirm-approve-content__small-blue-text.cursor-pointer', + '.confirm-approve-content__small-blue-text', ); - await editButtons[1].click(); + await editButtons[2].click(); // wait for permission modal to be visible const permissionModal = await driver.findVisibleElement('span .modal'); diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index 89b814610..3ad24196b 100644 --- a/ui/app/components/app/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -21,11 +21,13 @@ export default class Modal extends PureComponent { onCancel: PropTypes.func, cancelType: PropTypes.string, cancelText: PropTypes.string, + rounded: PropTypes.bool, }; static defaultProps = { submitType: 'secondary', cancelType: 'default', + rounded: false, }; render() { @@ -43,6 +45,7 @@ export default class Modal extends PureComponent { contentClass, containerClass, hideFooter, + rounded, } = this.props; return ( @@ -61,6 +64,7 @@ export default class Modal extends PureComponent { {onCancel && ( + + + + + + {t('editNonceField')} + + + + + +
+ { + setCustomNonce(e.target.value); + }} + fullWidth + margin="dense" + value={customNonce} + id="custom-nonce-id" + /> +
+
+ + + ); +}; + +CustomizeNonce.propTypes = { + hideModal: PropTypes.func.isRequired, + customNonceValue: PropTypes.string, + nextNonce: PropTypes.number, + updateCustomNonce: PropTypes.func, + getNextNonce: PropTypes.func, +}; +export default withModalProps(CustomizeNonce); diff --git a/ui/app/components/app/modals/customize-nonce/index.js b/ui/app/components/app/modals/customize-nonce/index.js new file mode 100644 index 000000000..27fa704ae --- /dev/null +++ b/ui/app/components/app/modals/customize-nonce/index.js @@ -0,0 +1 @@ +export { default } from './customize-nonce.component'; diff --git a/ui/app/components/app/modals/customize-nonce/index.scss b/ui/app/components/app/modals/customize-nonce/index.scss new file mode 100644 index 000000000..533071977 --- /dev/null +++ b/ui/app/components/app/modals/customize-nonce/index.scss @@ -0,0 +1,53 @@ +.customize-nonce-modal { + padding-left: 24px; + padding-right: 18px; + display: flex; + flex-flow: column nowrap; + + &__main-header { + display: flex; + align-items: center; + padding-top: 24px; + } + + &__main-title { + flex: 1; + } + + &__close { + @include H4; + + color: $ui-black; + background: none; + flex: 0; + align-self: flex-start; + } + + & &__link { + @include H6; + + display: inline; + padding-left: 5px; + } + + & &__reset { + @include H7; + } + + &__input { + input { + @include Paragraph; + + width: 100%; + } + } +} + +.customize-nonce-modal-content { + padding: 0; +} + +.customize-nonce-modal-container { + height: 324px; + width: 100%; +} diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss index 0a48e41c4..742190ab9 100644 --- a/ui/app/components/app/modals/index.scss +++ b/ui/app/components/app/modals/index.scss @@ -11,6 +11,7 @@ @import 'new-account-modal/index'; @import 'qr-scanner/index'; @import 'transaction-confirmed/index'; +@import 'customize-nonce/index'; .modal { z-index: 1050; diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js index 9aa659d32..74373354c 100644 --- a/ui/app/components/app/modals/modal.js +++ b/ui/app/components/app/modals/modal.js @@ -29,6 +29,7 @@ import ConfirmDeleteNetwork from './confirm-delete-network'; import AddToAddressBookModal from './add-to-addressbook-modal'; import EditApprovalPermission from './edit-approval-permission'; import NewAccountModal from './new-account-modal'; +import CustomizeNonceModal from './customize-nonce'; const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -376,6 +377,19 @@ const MODALS = { }, }, + CUSTOMIZE_NONCE: { + contents: , + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + DEFAULT: { contents: [], mobileModalStyle: {}, diff --git a/ui/app/components/ui/box/box.js b/ui/app/components/ui/box/box.js index 6e3ebc100..47d9617e6 100644 --- a/ui/app/components/ui/box/box.js +++ b/ui/app/components/ui/box/box.js @@ -76,8 +76,9 @@ export default function Box({ width, height, children, + className, }) { - const boxClassName = classnames('box', { + const boxClassName = classnames('box', className, { // ---Borders--- // if borderWidth or borderColor is supplied w/o style, default to solid 'box--border-style-solid': @@ -151,4 +152,5 @@ Box.propTypes = { display: PropTypes.oneOf(Object.values(DISPLAY)), width: PropTypes.oneOf(Object.values(BLOCK_SIZES)), height: PropTypes.oneOf(Object.values(BLOCK_SIZES)), + className: PropTypes.string, }; diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index 42798fcd6..b12fa2896 100644 --- a/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -4,6 +4,16 @@ import classnames from 'classnames'; import Identicon from '../../../components/ui/identicon'; import { addressSummary } from '../../../helpers/utils/util'; import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'; +import { ConfirmPageContainerWarning } from '../../../components/app/confirm-page-container/confirm-page-container-content'; +import Typography from '../../../components/ui/typography'; +import { + TYPOGRAPHY, + FONT_WEIGHT, + BLOCK_SIZES, + JUSTIFY_CONTENT, +} from '../../../helpers/constants/design-system'; +import Box from '../../../components/ui/box'; +import Button from '../../../components/ui/button'; export default class ConfirmApproveContent extends Component { static contextTypes = { @@ -27,6 +37,13 @@ export default class ConfirmApproveContent extends Component { nativeCurrency: PropTypes.string, fiatTransactionTotal: PropTypes.string, ethTransactionTotal: PropTypes.string, + useNonceField: PropTypes.bool, + customNonceValue: PropTypes.string, + updateCustomNonce: PropTypes.func, + getNextNonce: PropTypes.func, + nextNonce: PropTypes.number, + showCustomizeNonceModal: PropTypes.func, + warning: PropTypes.string, }; state = { @@ -34,6 +51,7 @@ export default class ConfirmApproveContent extends Component { }; renderApproveContentCard({ + showHeader = true, symbol, title, showEdit, @@ -42,6 +60,7 @@ export default class ConfirmApproveContent extends Component { footer, noBorder, }) { + const { t } = this.context; return (
-
-
- {symbol} -
-
- {title} -
- {showEdit && ( -
onEditClick()} - > - Edit + {showHeader && ( +
+
+ {symbol}
- )} -
+
+ {title} +
+ {showEdit && ( + + + + )} +
+ )}
{content}
{footer}
@@ -147,6 +171,58 @@ export default class ConfirmApproveContent extends Component { ); } + renderCustomNonceContent() { + const { t } = this.context; + const { + useNonceField, + customNonceValue, + updateCustomNonce, + getNextNonce, + nextNonce, + showCustomizeNonceModal, + } = this.props; + return ( + <> + {useNonceField && ( +
+ + + {t('nonce')} + + + + + {customNonceValue || nextNonce} + +
+ )} + + ); + } + render() { const { t } = this.context; const { @@ -160,6 +236,8 @@ export default class ConfirmApproveContent extends Component { showEditApprovalPermissionModal, setCustomAmount, tokenBalance, + useNonceField, + warning, } = this.props; const { showFullTxDetails } = this.state; @@ -169,6 +247,11 @@ export default class ConfirmApproveContent extends Component { 'confirm-approve-content--full': showFullTxDetails, })} > + {warning && ( +
+ +
+ )}
@@ -232,6 +315,35 @@ export default class ConfirmApproveContent extends Component {
), })} + {useNonceField && + this.renderApproveContentCard({ + showHeader: false, + content: this.renderCustomNonceContent(), + useNonceField, + noBorder: !showFullTxDetails, + footer: ( +
+ this.setState({ + showFullTxDetails: !this.state.showFullTxDetails, + }) + } + > +
+
+ View full transaction details +
+ +
+
+ ), + })}
{showFullTxDetails ? ( diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/index.scss b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss index e912f6675..9122644a4 100644 --- a/ui/app/pages/confirm-approve/confirm-approve-content/index.scss +++ b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss @@ -292,6 +292,38 @@ margin-left: 16px; } } + + &__custom-nonce-warning { + width: 100%; + height: 30px; + } + + &__custom-nonce-content { + display: flex; + height: 49px; + margin-top: 5px; + margin-bottom: 6px; + padding: 12px 12px 14px 12px; + border: 1px solid #bbc0c5; + box-sizing: border-box; + border-radius: 6px; + align-items: center; + } + + &__custom-nonce-header { + flex: 1; + align-items: center; + } + + &__custom-nonce-value { + flex: 0; + } + + & &__custom-nonce-edit { + @include H7; + + width: auto; + } } .confirm-approve-content--full { diff --git a/ui/app/pages/confirm-approve/confirm-approve.js b/ui/app/pages/confirm-approve/confirm-approve.js index c27b1160a..9eb51bd2a 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.js +++ b/ui/app/pages/confirm-approve/confirm-approve.js @@ -2,7 +2,11 @@ import React, { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import ConfirmTransactionBase from '../confirm-transaction-base'; -import { showModal } from '../../store/actions'; +import { + showModal, + updateCustomNonce, + getNextNonce, +} from '../../store/actions'; import { getTokenData } from '../../helpers/utils/transactions.util'; import { calcTokenAmount, @@ -17,6 +21,9 @@ import { getCurrentCurrency, getDomainMetadata, getNativeCurrency, + getUseNonceField, + getCustomNonceValue, + getNextSuggestedNonce, } from '../../selectors'; import { currentNetworkTxListSelector } from '../../selectors/transactions'; import Loading from '../../components/ui/loading-screen'; @@ -36,6 +43,9 @@ export default function ConfirmApprove() { const currentNetworkTxList = useSelector(currentNetworkTxListSelector); const domainMetadata = useSelector(getDomainMetadata); const tokens = useSelector(getTokens); + const useNonceField = useSelector(getUseNonceField); + const nextNonce = useSelector(getNextSuggestedNonce); + const customNonceValue = useSelector(getCustomNonceValue); const transaction = currentNetworkTxList.find( @@ -72,6 +82,25 @@ export default function ConfirmApprove() { previousTokenAmount.current = tokenAmount; }, [customPermissionAmount, tokenAmount]); + const [submitWarning, setSubmitWarning] = useState(''); + const prevNonce = useRef(nextNonce); + const prevCustomNonce = useRef(customNonceValue); + useEffect(() => { + if ( + prevNonce.current !== nextNonce || + prevCustomNonce.current !== customNonceValue + ) { + if (nextNonce !== null && customNonceValue > nextNonce) { + setSubmitWarning( + `Nonce is higher than suggested nonce of ${nextNonce}`, + ); + } else { + setSubmitWarning(''); + } + } + prevCustomNonce.current = customNonceValue; + prevNonce.current = nextNonce; + }, [customNonceValue, nextNonce]); const { origin } = transaction; const formattedOrigin = origin ? origin[0].toUpperCase() + origin.slice(1) @@ -139,6 +168,34 @@ export default function ConfirmApprove() { nativeCurrency={nativeCurrency} ethTransactionTotal={ethTransactionTotal} fiatTransactionTotal={fiatTransactionTotal} + useNonceField={useNonceField} + nextNonce={nextNonce} + customNonceValue={customNonceValue} + updateCustomNonce={(value) => { + dispatch(updateCustomNonce(value)); + }} + getNextNonce={() => dispatch(getNextNonce())} + showCustomizeNonceModal={({ + /* eslint-disable no-shadow */ + useNonceField, + nextNonce, + customNonceValue, + updateCustomNonce, + getNextNonce, + /* eslint-disable no-shadow */ + }) => + dispatch( + showModal({ + name: 'CUSTOMIZE_NONCE', + useNonceField, + nextNonce, + customNonceValue, + updateCustomNonce, + getNextNonce, + }), + ) + } + warning={submitWarning} /> } hideSenderToRecipient diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index cb5332a22..7a761bd9b 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -149,6 +149,7 @@ const mapStateToProps = (state, ownProps) => { }, }; } + customNonceValue = getCustomNonceValue(state); return { balance, @@ -179,7 +180,7 @@ const mapStateToProps = (state, ownProps) => { }, advancedInlineGasShown: getAdvancedInlineGasShown(state), useNonceField: getUseNonceField(state), - customNonceValue: getCustomNonceValue(state), + customNonceValue, insufficientBalance, hideSubtitle: !isMainnet && !showFiatInTestnets, hideFiatConversion: !isMainnet && !showFiatInTestnets, diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 26c423c56..6b26d18f9 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -490,3 +490,7 @@ export function getNativeCurrencyImage(state) { const nativeCurrency = getNativeCurrency(state).toUpperCase(); return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency]; } + +export function getNextSuggestedNonce(state) { + return Number(state.metamask.nextNonce); +}