1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Adding option to set Custom Nonce to Confirm Approve Page (#10595)

This commit is contained in:
Niranjana Binoy 2021-04-16 18:00:18 -04:00 committed by GitHub
parent 1b4bc46c7b
commit 18aa540f42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 446 additions and 25 deletions

View File

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

View File

@ -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');

View File

@ -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 && (
<Button
type={cancelType}
rounded={rounded}
onClick={onCancel}
className="modal-container__footer-button"
>
@ -69,6 +73,7 @@ export default class Modal extends PureComponent {
)}
<Button
type={submitType}
rounded={rounded || false}
onClick={onSubmit}
disabled={submitDisabled}
className="modal-container__footer-button"

View File

@ -0,0 +1,132 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Modal from '../../modal';
import TextField from '../../../ui/text-field';
import Button from '../../../ui/button';
import Typography from '../../../ui/typography';
import {
TYPOGRAPHY,
FONT_WEIGHT,
ALIGN_ITEMS,
BLOCK_SIZES,
DISPLAY,
} from '../../../../helpers/constants/design-system';
import Box from '../../../ui/box';
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
import { useI18nContext } from '../../../../hooks/useI18nContext';
const CustomizeNonce = ({
hideModal,
customNonceValue,
nextNonce,
updateCustomNonce,
getNextNonce,
}) => {
const [customNonce, setCustomNonce] = useState('');
const t = useI18nContext();
return (
<Modal
onSubmit={() => {
if (customNonce === '') {
updateCustomNonce(customNonceValue);
} else {
updateCustomNonce(customNonce);
}
getNextNonce();
hideModal();
}}
submitText={t('save')}
submitType="primary"
onCancel={() => hideModal()}
cancelText={t('cancel')}
cancelType="secondary"
rounded
contentClass="customize-nonce-modal-content"
containerClass="customize-nonce-modal-container"
>
<div className="customize-nonce-modal">
<div className="customize-nonce-modal__main-header">
<Typography
className="customize-nonce-modal__main-title"
variant={TYPOGRAPHY.H4}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('editNonceField')}
</Typography>
<button
className="fas fa-times customize-nonce-modal__close"
title={t('close')}
onClick={hideModal}
/>
</div>
<Box
marginTop={2}
display={DISPLAY.INLINE_FLEX}
alignItems={ALIGN_ITEMS.CENTER}
>
<Typography variant={TYPOGRAPHY.H6} fontWeight={FONT_WEIGHT.NORMAL}>
{t('editNonceMessage')}
<Button
type="link"
className="customize-nonce-modal__link"
rel="noopener noreferrer"
target="_blank"
href="https://metamask.zendesk.com/hc/en-us/articles/360015489251"
>
{t('learnMore')}
</Button>
</Typography>
</Box>
<Box marginTop={3}>
<Box alignItems={ALIGN_ITEMS.CENTER} display={DISPLAY.FLEX}>
<Typography
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.BOLD}
boxProps={{ width: BLOCK_SIZES.FIVE_SIXTHS }}
>
{t('editNonceField')}
</Typography>
<Box width={BLOCK_SIZES.ONE_SIXTH}>
<Button
type="link"
className="customize-nonce-modal__reset"
onClick={() => {
setCustomNonce(nextNonce);
}}
>
{t('reset')}
</Button>
</Box>
</Box>
<div className="customize-nonce-modal__input">
<TextField
type="number"
min="0"
placeholder={
customNonceValue ||
(typeof nextNonce === 'number' && nextNonce.toString())
}
onChange={(e) => {
setCustomNonce(e.target.value);
}}
fullWidth
margin="dense"
value={customNonce}
id="custom-nonce-id"
/>
</div>
</Box>
</div>
</Modal>
);
};
CustomizeNonce.propTypes = {
hideModal: PropTypes.func.isRequired,
customNonceValue: PropTypes.string,
nextNonce: PropTypes.number,
updateCustomNonce: PropTypes.func,
getNextNonce: PropTypes.func,
};
export default withModalProps(CustomizeNonce);

View File

@ -0,0 +1 @@
export { default } from './customize-nonce.component';

View File

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

View File

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

View File

@ -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: <CustomizeNonceModal />,
mobileModalStyle: {
...modalContainerMobileStyle,
},
laptopModalStyle: {
...modalContainerLaptopStyle,
},
contentStyle: {
borderRadius: '8px',
},
},
DEFAULT: {
contents: [],
mobileModalStyle: {},

View File

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

View File

@ -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 (
<div
className={classnames({
@ -49,22 +68,27 @@ export default class ConfirmApproveContent extends Component {
'confirm-approve-content__card--no-border': noBorder,
})}
>
<div className="confirm-approve-content__card-header">
<div className="confirm-approve-content__card-header__symbol">
{symbol}
</div>
<div className="confirm-approve-content__card-header__title">
{title}
</div>
{showEdit && (
<div
className="confirm-approve-content__small-blue-text cursor-pointer"
onClick={() => onEditClick()}
>
Edit
{showHeader && (
<div className="confirm-approve-content__card-header">
<div className="confirm-approve-content__card-header__symbol">
{symbol}
</div>
)}
</div>
<div className="confirm-approve-content__card-header__title">
{title}
</div>
{showEdit && (
<Box width={BLOCK_SIZES.ONE_SIXTH}>
<Button
type="link"
className="confirm-approve-content__small-blue-text"
onClick={() => onEditClick()}
>
{t('edit')}
</Button>
</Box>
)}
</div>
)}
<div className="confirm-approve-content__card-content">{content}</div>
{footer}
</div>
@ -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 && (
<div className="confirm-approve-content__custom-nonce-content">
<Box
className="confirm-approve-content__custom-nonce-header"
justifyContent={JUSTIFY_CONTENT.FLEX_START}
>
<Typography
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.NORMAL}
>
{t('nonce')}
</Typography>
<Button
type="link"
className="confirm-approve-content__custom-nonce-edit"
onClick={() =>
showCustomizeNonceModal({
nextNonce,
customNonceValue,
updateCustomNonce,
getNextNonce,
})
}
>
{t('edit')}
</Button>
</Box>
<Typography
className="confirm-approve-content__custom-nonce-value"
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.BOLD}
>
{customNonceValue || nextNonce}
</Typography>
</div>
)}
</>
);
}
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 && (
<div className="confirm-approve-content__custom-nonce-warning">
<ConfirmPageContainerWarning warning={warning} />
</div>
)}
<div className="confirm-approve-content__identicon-wrapper">
<Identicon
className="confirm-approve-content__identicon"
@ -208,8 +291,8 @@ export default class ConfirmApproveContent extends Component {
showEdit: true,
onEditClick: showCustomizeGasModal,
content: this.renderTransactionDetailsContent(),
noBorder: !showFullTxDetails,
footer: (
noBorder: useNonceField || !showFullTxDetails,
footer: !useNonceField && (
<div
className="confirm-approve-content__view-full-tx-button-wrapper"
onClick={() =>
@ -232,6 +315,35 @@ export default class ConfirmApproveContent extends Component {
</div>
),
})}
{useNonceField &&
this.renderApproveContentCard({
showHeader: false,
content: this.renderCustomNonceContent(),
useNonceField,
noBorder: !showFullTxDetails,
footer: (
<div
className="confirm-approve-content__view-full-tx-button-wrapper"
onClick={() =>
this.setState({
showFullTxDetails: !this.state.showFullTxDetails,
})
}
>
<div className="confirm-approve-content__view-full-tx-button cursor-pointer">
<div className="confirm-approve-content__small-blue-text">
View full transaction details
</div>
<i
className={classnames({
'fa fa-caret-up': showFullTxDetails,
'fa fa-caret-down': !showFullTxDetails,
})}
/>
</div>
</div>
),
})}
</div>
{showFullTxDetails ? (

View File

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

View File

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

View File

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

View File

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