diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 7fcf47431..96b4c4383 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -2931,6 +2931,9 @@
"message": "By revoking permission, the following $1 will no longer be able to access your $2",
"description": "$1 is either key 'account' or 'contract', and $2 is either a string or link of a given token symbol or name"
},
+ "revokeSpendingCap": {
+ "message": "Revoke spending cap for your"
+ },
"revokeSpendingCapTooltipText": {
"message": "This contract will be unable to spend any more of your current or future tokens."
},
diff --git a/ui/components/app/approve-content-card/approve-content-card.js b/ui/components/app/approve-content-card/approve-content-card.js
index 34b7e3ee9..2e821eaa0 100644
--- a/ui/components/app/approve-content-card/approve-content-card.js
+++ b/ui/components/app/approve-content-card/approve-content-card.js
@@ -217,25 +217,88 @@ export default function ApproveContentCard({
}
ApproveContentCard.propTypes = {
+ /**
+ * Whether to show header including icon, transaction fee text and edit button
+ */
showHeader: PropTypes.bool,
+ /**
+ * Symbol icon
+ */
symbol: PropTypes.node,
+ /**
+ * Title to be included in the header
+ */
title: PropTypes.string,
+ /**
+ * Whether to show edit button or not
+ */
showEdit: PropTypes.bool,
+ /**
+ * Whether to show advanced gas fee options or not
+ */
showAdvanceGasFeeOptions: PropTypes.bool,
+ /**
+ * Should open customize gas modal when edit button is clicked
+ */
onEditClick: PropTypes.func,
+ /**
+ * Footer to be shown
+ */
footer: PropTypes.node,
+ /**
+ * Whether to include border-bottom or not
+ */
noBorder: PropTypes.bool,
+ /**
+ * Is enhanced gas fee enabled or not
+ */
supportsEIP1559V2: PropTypes.bool,
+ /**
+ * Whether to render transaction details content or not
+ */
renderTransactionDetailsContent: PropTypes.bool,
+ /**
+ * Whether to render data content or not
+ */
renderDataContent: PropTypes.bool,
+ /**
+ * Is multi-layer fee network or not
+ */
isMultiLayerFeeNetwork: PropTypes.bool,
+ /**
+ * Total sum of the transaction in native currency
+ */
ethTransactionTotal: PropTypes.string,
+ /**
+ * Current native currency
+ */
nativeCurrency: PropTypes.string,
+ /**
+ * Current transaction
+ */
fullTxData: PropTypes.object,
+ /**
+ * Total sum of the transaction converted to hex value
+ */
hexTransactionTotal: PropTypes.string,
+ /**
+ * Total sum of the transaction in fiat currency
+ */
fiatTransactionTotal: PropTypes.string,
+ /**
+ * Current fiat currency
+ */
currentCurrency: PropTypes.string,
+ /**
+ * Is set approve for all or not
+ */
isSetApproveForAll: PropTypes.bool,
+ /**
+ * Whether a current set approval for all transaction will approve or revoke access
+ */
isApprovalOrRejection: PropTypes.bool,
+ /**
+ * Current transaction data
+ */
data: PropTypes.string,
};
diff --git a/ui/components/app/approve-content-card/approve-content-card.stories.js b/ui/components/app/approve-content-card/approve-content-card.stories.js
new file mode 100644
index 000000000..113b35384
--- /dev/null
+++ b/ui/components/app/approve-content-card/approve-content-card.stories.js
@@ -0,0 +1,196 @@
+import React from 'react';
+import ApproveContentCard from './approve-content-card';
+
+export default {
+ title: 'Components/App/ApproveContentCard',
+ id: __filename,
+ argTypes: {
+ showHeader: {
+ control: 'boolean',
+ },
+ symbol: {
+ control: 'array',
+ },
+ title: {
+ control: 'text',
+ },
+ showEdit: {
+ control: 'boolean',
+ },
+ showAdvanceGasFeeOptions: {
+ control: 'boolean',
+ },
+ footer: {
+ control: 'array',
+ },
+ noBorder: {
+ control: 'boolean',
+ },
+ supportsEIP1559V2: {
+ control: 'boolean',
+ },
+ renderTransactionDetailsContent: {
+ control: 'boolean',
+ },
+ renderDataContent: {
+ control: 'boolean',
+ },
+ isMultiLayerFeeNetwork: {
+ control: 'boolean',
+ },
+ ethTransactionTotal: {
+ control: 'text',
+ },
+ nativeCurrency: {
+ control: 'text',
+ },
+ fullTxData: {
+ control: 'object',
+ },
+ hexTransactionTotal: {
+ control: 'text',
+ },
+ fiatTransactionTotal: {
+ control: 'text',
+ },
+ currentCurrency: {
+ control: 'text',
+ },
+ isSetApproveForAll: {
+ control: 'boolean',
+ },
+ isApprovalOrRejection: {
+ control: 'boolean',
+ },
+ data: {
+ control: 'text',
+ },
+ onEditClick: {
+ control: 'onEditClick',
+ },
+ },
+ args: {
+ showHeader: true,
+ symbol: ,
+ title: 'Transaction fee',
+ showEdit: true,
+ showAdvanceGasFeeOptions: true,
+ noBorder: true,
+ supportsEIP1559V2: false,
+ renderTransactionDetailsContent: true,
+ renderDataContent: false,
+ isMultiLayerFeeNetwork: false,
+ ethTransactionTotal: '0.0012',
+ nativeCurrency: 'GoerliETH',
+ hexTransactionTotal: '0x44364c5bb0000',
+ fiatTransactionTotal: '1.54',
+ currentCurrency: 'usd',
+ isSetApproveForAll: false,
+ isApprovalOrRejection: false,
+ data: '',
+ fullTxData: {
+ id: 3049568294499567,
+ time: 1664449552289,
+ status: 'unapproved',
+ metamaskNetworkId: '3',
+ originalGasEstimate: '0xea60',
+ userEditedGasLimit: false,
+ chainId: '0x3',
+ loadingDefaults: false,
+ dappSuggestedGasFees: {
+ gasPrice: '0x4a817c800',
+ gas: '0xea60',
+ },
+ sendFlowHistory: [],
+ txParams: {
+ from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
+ to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
+ value: '0x0',
+ data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
+ gas: '0xea60',
+ maxFeePerGas: '0x4a817c800',
+ maxPriorityFeePerGas: '0x4a817c800',
+ },
+ origin: 'https://metamask.github.io',
+ type: 'approve',
+ history: [
+ {
+ id: 3049568294499567,
+ time: 1664449552289,
+ status: 'unapproved',
+ metamaskNetworkId: '3',
+ originalGasEstimate: '0xea60',
+ userEditedGasLimit: false,
+ chainId: '0x3',
+ loadingDefaults: true,
+ dappSuggestedGasFees: {
+ gasPrice: '0x4a817c800',
+ gas: '0xea60',
+ },
+ sendFlowHistory: [],
+ txParams: {
+ from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
+ to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
+ value: '0x0',
+ data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
+ gas: '0xea60',
+ gasPrice: '0x4a817c800',
+ },
+ origin: 'https://metamask.github.io',
+ type: 'approve',
+ },
+ [
+ {
+ op: 'remove',
+ path: '/txParams/gasPrice',
+ note: 'Added new unapproved transaction.',
+ timestamp: 1664449553939,
+ },
+ {
+ op: 'add',
+ path: '/txParams/maxFeePerGas',
+ value: '0x4a817c800',
+ },
+ {
+ op: 'add',
+ path: '/txParams/maxPriorityFeePerGas',
+ value: '0x4a817c800',
+ },
+ {
+ op: 'replace',
+ path: '/loadingDefaults',
+ value: false,
+ },
+ {
+ op: 'add',
+ path: '/userFeeLevel',
+ value: 'custom',
+ },
+ {
+ op: 'add',
+ path: '/defaultGasEstimates',
+ value: {
+ estimateType: 'custom',
+ gas: '0xea60',
+ maxFeePerGas: '0x4a817c800',
+ maxPriorityFeePerGas: '0x4a817c800',
+ },
+ },
+ ],
+ ],
+ userFeeLevel: 'custom',
+ defaultGasEstimates: {
+ estimateType: 'custom',
+ gas: '0xea60',
+ maxFeePerGas: '0x4a817c800',
+ maxPriorityFeePerGas: '0x4a817c800',
+ },
+ },
+ },
+};
+
+export const DefaultStory = (args) => {
+ return ;
+};
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.js b/ui/components/app/custom-spending-cap/custom-spending-cap.js
index c19118e95..9afedc0b5 100644
--- a/ui/components/app/custom-spending-cap/custom-spending-cap.js
+++ b/ui/components/app/custom-spending-cap/custom-spending-cap.js
@@ -1,4 +1,5 @@
-import React, { useState, useContext } from 'react';
+import React, { useState, useContext, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box';
@@ -15,6 +16,8 @@ import {
JUSTIFY_CONTENT,
SIZES,
} from '../../../helpers/constants/design-system';
+import { getCustomTokenAmount } from '../../../selectors';
+import { setCustomTokenAmount } from '../../../ducks/app/app';
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
export default function CustomSpendingCap({
@@ -22,12 +25,17 @@ export default function CustomSpendingCap({
currentTokenBalance,
dappProposedValue,
siteOrigin,
- onEdit,
+ passTheErrorText,
}) {
const t = useContext(I18nContext);
- const [value, setValue] = useState('');
- const [customSpendingCapText, setCustomSpendingCapText] = useState('');
+ const dispatch = useDispatch();
+
+ const value = useSelector(getCustomTokenAmount);
+
const [error, setError] = useState('');
+ const [showUseDefaultButton, setShowUseDefaultButton] = useState(
+ value !== String(dappProposedValue) && true,
+ );
const inputLogicEmptyStateText = t('inputLogicEmptyState');
const getInputTextLogic = (inputNumber) => {
@@ -57,6 +65,10 @@ export default function CustomSpendingCap({
};
};
+ const [customSpendingCapText, setCustomSpendingCapText] = useState(
+ getInputTextLogic(value).description,
+ );
+
const handleChange = (valueInput) => {
let spendingCapError = '';
const inputTextLogic = getInputTextLogic(valueInput);
@@ -71,9 +83,19 @@ export default function CustomSpendingCap({
setError('');
}
- setValue(valueInput);
+ dispatch(setCustomTokenAmount(String(valueInput)));
};
+ useEffect(() => {
+ if (value !== String(dappProposedValue)) {
+ setShowUseDefaultButton(true);
+ }
+ }, [value, dappProposedValue]);
+
+ useEffect(() => {
+ passTheErrorText(error);
+ }, [error, passTheErrorText]);
+
const chooseTooltipContentText =
value > currentTokenBalance
? t('warningTooltipText', [
@@ -100,7 +122,6 @@ export default function CustomSpendingCap({
onClick={(e) => {
e.preventDefault();
handleChange(currentTokenBalance);
- setValue(currentTokenBalance);
}}
>
{t('max')}
@@ -131,6 +152,7 @@ export default function CustomSpendingCap({
}
>
{
- e.preventDefault();
- if (value <= currentTokenBalance || error) {
+ showUseDefaultButton && (
+
+ }}
+ >
+ {t('useDefault')}
+
+ )
}
titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }}
allowDecimals
@@ -202,7 +222,7 @@ CustomSpendingCap.propTypes = {
*/
siteOrigin: PropTypes.string,
/**
- * onClick handler for the Edit link
+ * Parent component's callback function passed in order to get the error text
*/
- onEdit: PropTypes.func,
+ passTheErrorText: PropTypes.func,
};
diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
index a8f757926..5c30a32e7 100644
--- a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
+++ b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
@@ -17,8 +17,8 @@ export default {
siteOrigin: {
control: { type: 'text' },
},
- onEdit: {
- action: 'onEdit',
+ passTheErrorText: {
+ action: 'passTheErrorText',
},
},
args: {
diff --git a/ui/components/app/custom-spending-cap/index.scss b/ui/components/app/custom-spending-cap/index.scss
index 00ae12252..e2ab68851 100644
--- a/ui/components/app/custom-spending-cap/index.scss
+++ b/ui/components/app/custom-spending-cap/index.scss
@@ -21,6 +21,7 @@
position: absolute;
margin-top: 55px;
margin-inline-start: -75px;
+ z-index: 1;
}
}
@@ -28,4 +29,10 @@
color: var(--color-error-default);
padding-inline-end: 60px;
}
+
+ input[type='number']::-webkit-inner-spin-button,
+ input[type='number']:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
}
diff --git a/ui/ducks/app/app.js b/ui/ducks/app/app.js
index c61c00a8e..9b62a4b71 100644
--- a/ui/ducks/app/app.js
+++ b/ui/ducks/app/app.js
@@ -58,6 +58,7 @@ export default function reduceApp(state = {}, action) {
newTokensImported: '',
newCustomNetworkAdded: {},
onboardedInThisUISession: false,
+ customTokenAmount: '',
...state,
};
@@ -401,6 +402,11 @@ export default function reduceApp(state = {}, action) {
...appState,
onboardedInThisUISession: action.value,
};
+ case actionConstants.SET_CUSTOM_TOKEN_AMOUNT:
+ return {
+ ...appState,
+ customTokenAmount: action.value,
+ };
default:
return appState;
}
@@ -463,3 +469,7 @@ export function setNewCustomNetworkAdded(value) {
export function setOnBoardedInThisUISession(value) {
return { type: actionConstants.ONBOARDED_IN_THIS_UI_SESSION, value };
}
+
+export function setCustomTokenAmount(value) {
+ return { type: actionConstants.SET_CUSTOM_TOKEN_AMOUNT, value };
+}
diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js
index 26c065b44..bbc17c3e7 100644
--- a/ui/pages/confirm-approve/confirm-approve.js
+++ b/ui/pages/confirm-approve/confirm-approve.js
@@ -180,14 +180,14 @@ export default function ConfirmApprove({
supportsEIP1559V2={supportsEIP1559V2}
userAddress={userAddress}
tokenAddress={tokenAddress}
- data={customData || transactionData}
+ data={transactionData}
isSetApproveForAll={isSetApproveForAll}
isApprovalOrRejection={isApprovalOrRejection}
- customTxParamsData={customData}
dappProposedTokenAmount={tokenAmount}
currentTokenBalance={tokenBalance}
toAddress={toAddress}
tokenSymbol={tokenSymbol}
+ decimals={decimals}
/>
{showCustomizeGasPopover && !supportsEIP1559V2 && (
transactionFeeSelector(state, fullTxData));
const methodData = useSelector((state) => getKnownMethodData(state, data));
+ const { balanceError } = useGasFeeContext();
+
+ const disableNextButton =
+ isFirstPage && (customTokenAmount === '' || errorText !== '');
+
+ const disableApproveButton = !isFirstPage && balanceError;
+
const networkName =
NETWORK_TO_NAME_MAP[fullTxData.chainId] || networkIdentifier;
@@ -105,9 +128,10 @@ export default function TokenAllowance({
: transactionData;
const handleReject = () => {
+ dispatch(updateCustomNonce(''));
+
dispatch(cancelTx(fullTxData)).then(() => {
dispatch(clearConfirmTransaction());
- dispatch(updateCustomNonce(''));
history.push(mostRecentOverviewPage);
});
};
@@ -128,17 +152,35 @@ export default function TokenAllowance({
fullTxData.originalApprovalAmount = dappProposedTokenAmount;
}
+ if (customTokenAmount) {
+ fullTxData.customTokenAmount = customTokenAmount;
+ fullTxData.finalApprovalAmount = customTokenAmount;
+ } else if (dappProposedTokenAmount !== undefined) {
+ fullTxData.finalApprovalAmount = dappProposedTokenAmount;
+ }
+
if (currentTokenBalance) {
fullTxData.currentTokenBalance = currentTokenBalance;
}
+ dispatch(updateCustomNonce(''));
+
dispatch(updateAndApproveTx(customNonceMerge(fullTxData))).then(() => {
dispatch(clearConfirmTransaction());
- dispatch(updateCustomNonce(''));
history.push(mostRecentOverviewPage);
});
};
+ const handleNextClick = () => {
+ setShowFullTxDetails(false);
+ setIsFirstPage(false);
+ };
+
+ const handleBackClick = () => {
+ setShowFullTxDetails(false);
+ setIsFirstPage(true);
+ };
+
return (
{!isFirstPage && (
-
-
+
- {isFirstPage ? t('setSpendingCap') : t('reviewSpendingCap')}
+ {isFirstPage && t('setSpendingCap')}
+ {!isFirstPage &&
+ (customTokenAmount === 0
+ ? t('revokeSpendingCap')
+ : t('reviewSpendingCap'))}
@@ -251,13 +297,28 @@ export default function TokenAllowance({
- setIsFirstPage(true)}
- />
+ {isFirstPage ? (
+ setErrorText(value)}
+ />
+ ) : (
+ handleBackClick()}
+ />
+ )}
+ {!isFirstPage && balanceError && (
+
+ )}
{!isFirstPage && (
@@ -328,7 +389,8 @@ export default function TokenAllowance({
cancelText={t('reject')}
submitText={isFirstPage ? t('next') : t('approveButtonText')}
onCancel={() => handleReject()}
- onSubmit={() => (isFirstPage ? setIsFirstPage(false) : handleApprove())}
+ onSubmit={() => (isFirstPage ? handleNextClick() : handleApprove())}
+ disabled={disableNextButton || disableApproveButton}
/>
{showContractDetails && (