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 && ( - - setIsFirstPage(true)} - /> + {isFirstPage ? ( + setErrorText(value)} + /> + ) : ( + handleBackClick()} + /> + )} + {!isFirstPage && balanceError && ( + + {t('insufficientFundsForGas')} + + )} {!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 && (