diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 61552da61..04d76f58c 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -2927,6 +2927,9 @@
"revealTheSeedPhrase": {
"message": "Reveal seed phrase"
},
+ "reviewSpendingCap": {
+ "message": "Review your spending cap"
+ },
"revokeAllTokensTitle": {
"message": "Revoke permission to access all of your $1?",
"description": "$1 is the symbol of the token for which the user is revoking approval"
@@ -3122,6 +3125,9 @@
"message": "Approve $1 with no spend limit",
"description": "The token symbol that is being approved"
},
+ "setSpendingCap": {
+ "message": "Set a spending cap for your"
+ },
"settings": {
"message": "Settings"
},
@@ -4230,6 +4236,9 @@
"userName": {
"message": "Username"
},
+ "verifyContractDetails": {
+ "message": "Verify contract details"
+ },
"verifyThisTokenDecimalOn": {
"message": "Token decimal can be found on $1",
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
@@ -4251,6 +4260,9 @@
"viewContact": {
"message": "View contact"
},
+ "viewDetails": {
+ "message": "View details"
+ },
"viewFullTransactionDetails": {
"message": "View full transaction details"
},
diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss
index aab3eec49..fad86dbc3 100644
--- a/ui/components/app/app-components.scss
+++ b/ui/components/app/app-components.scss
@@ -96,3 +96,4 @@
@import 'detected-token/detected-token-ignored-popover/index';
@import 'detected-token/detected-token-selection-popover/index';
@import 'network-account-balance-header/index';
+@import 'approve-content-card/index';
diff --git a/ui/components/app/approve-content-card/approve-content-card.js b/ui/components/app/approve-content-card/approve-content-card.js
new file mode 100644
index 000000000..34b7e3ee9
--- /dev/null
+++ b/ui/components/app/approve-content-card/approve-content-card.js
@@ -0,0 +1,241 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import Box from '../../ui/box/box';
+import Button from '../../ui/button';
+import EditGasFeeButton from '../edit-gas-fee-button/edit-gas-fee-button';
+import Typography from '../../ui/typography/typography';
+import {
+ ALIGN_ITEMS,
+ BLOCK_SIZES,
+ COLORS,
+ DISPLAY,
+ FLEX_DIRECTION,
+ FONT_WEIGHT,
+ JUSTIFY_CONTENT,
+ TEXT_ALIGN,
+ TYPOGRAPHY,
+} from '../../../helpers/constants/design-system';
+import { I18nContext } from '../../../../.storybook/i18n';
+import GasDetailsItem from '../gas-details-item/gas-details-item';
+import MultiLayerFeeMessage from '../multilayer-fee-message/multi-layer-fee-message';
+import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
+
+export default function ApproveContentCard({
+ showHeader = true,
+ symbol,
+ title,
+ showEdit,
+ showAdvanceGasFeeOptions = false,
+ onEditClick,
+ footer,
+ noBorder,
+ supportsEIP1559V2,
+ renderTransactionDetailsContent,
+ renderDataContent,
+ isMultiLayerFeeNetwork,
+ ethTransactionTotal,
+ nativeCurrency,
+ fullTxData,
+ hexTransactionTotal,
+ fiatTransactionTotal,
+ currentCurrency,
+ isSetApproveForAll,
+ isApprovalOrRejection,
+ data,
+}) {
+ const t = useContext(I18nContext);
+
+ return (
+
+ {showHeader && (
+
+ {supportsEIP1559V2 && title === t('transactionFee') ? null : (
+ <>
+
+ {symbol}
+
+
+
+ {title}
+
+
+ >
+ )}
+ {showEdit && (!showAdvanceGasFeeOptions || !supportsEIP1559V2) && (
+
+
+
+ )}
+ {showEdit && showAdvanceGasFeeOptions && supportsEIP1559V2 && (
+
+ )}
+
+ )}
+
+ {renderTransactionDetailsContent &&
+ (!isMultiLayerFeeNetwork && supportsEIP1559V2 ? (
+
+ ) : (
+
+ {isMultiLayerFeeNetwork ? (
+
+
+
+ {t('transactionDetailLayer2GasHeading')}
+ {`${ethTransactionTotal} ${nativeCurrency}`}
+
+
+
+
+ ) : (
+ <>
+
+
+ {t('feeAssociatedRequest')}
+
+
+
+
+
+ {formatCurrency(fiatTransactionTotal, currentCurrency)}
+
+
+
+
+ {`${ethTransactionTotal} ${nativeCurrency}`}
+
+
+
+ >
+ )}
+
+ ))}
+ {renderDataContent && (
+
+
+
+ {isSetApproveForAll
+ ? t('functionSetApprovalForAll')
+ : t('functionApprove')}
+
+
+ {isSetApproveForAll && isApprovalOrRejection !== undefined ? (
+
+
+ {`${t('parameters')}: ${isApprovalOrRejection}`}
+
+
+ ) : null}
+
+
+ {data}
+
+
+
+ )}
+
+ {footer}
+
+ );
+}
+
+ApproveContentCard.propTypes = {
+ showHeader: PropTypes.bool,
+ symbol: PropTypes.node,
+ title: PropTypes.string,
+ showEdit: PropTypes.bool,
+ showAdvanceGasFeeOptions: PropTypes.bool,
+ onEditClick: PropTypes.func,
+ footer: PropTypes.node,
+ noBorder: PropTypes.bool,
+ supportsEIP1559V2: PropTypes.bool,
+ renderTransactionDetailsContent: PropTypes.bool,
+ renderDataContent: PropTypes.bool,
+ isMultiLayerFeeNetwork: PropTypes.bool,
+ ethTransactionTotal: PropTypes.string,
+ nativeCurrency: PropTypes.string,
+ fullTxData: PropTypes.object,
+ hexTransactionTotal: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ currentCurrency: PropTypes.string,
+ isSetApproveForAll: PropTypes.bool,
+ isApprovalOrRejection: PropTypes.bool,
+ data: PropTypes.string,
+};
diff --git a/ui/components/app/approve-content-card/index.js b/ui/components/app/approve-content-card/index.js
new file mode 100644
index 000000000..02838ddb5
--- /dev/null
+++ b/ui/components/app/approve-content-card/index.js
@@ -0,0 +1 @@
+export { default } from './approve-content-card';
diff --git a/ui/components/app/approve-content-card/index.scss b/ui/components/app/approve-content-card/index.scss
new file mode 100644
index 000000000..8dd9badc1
--- /dev/null
+++ b/ui/components/app/approve-content-card/index.scss
@@ -0,0 +1,51 @@
+.approve-content-card-container {
+ &__card,
+ &__card--no-border {
+ border-bottom: 1px solid var(--color-border-default);
+ position: relative;
+ padding-inline-start: 24px;
+ padding-inline-end: 24px;
+ }
+
+ &__card--no-border {
+ border-bottom: none;
+ }
+
+ &__card-header {
+ position: relative;
+
+ &__symbol {
+ width: auto;
+ }
+
+ &__symbol--aligned {
+ width: 100%;
+ }
+
+ &__title {
+ width: 100%;
+ }
+
+ &__title--aligned {
+ margin-inline-start: 27px;
+ position: absolute;
+ width: auto;
+ }
+ }
+
+ &__card-content--aligned {
+ margin-inline-start: 42px;
+ }
+
+ &__transaction-details-extra-content {
+ width: 100%;
+ }
+
+ &__data {
+ width: 100%;
+
+ &__data-block {
+ overflow-wrap: break-word;
+ }
+ }
+}
diff --git a/ui/components/app/modals/contract-details-modal/contract-details-modal.js b/ui/components/app/modals/contract-details-modal/contract-details-modal.js
index 5ac65d8b2..818920afe 100644
--- a/ui/components/app/modals/contract-details-modal/contract-details-modal.js
+++ b/ui/components/app/modals/contract-details-modal/contract-details-modal.js
@@ -1,5 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { getAccountLink } from '@metamask/etherscan-link';
+import { useSelector } from 'react-redux';
+import classnames from 'classnames';
import Box from '../../../ui/box';
import IconCopy from '../../../ui/icon/icon-copy';
import IconBlockExplorer from '../../../ui/icon/icon-block-explorer';
@@ -19,9 +22,27 @@ import {
SIZES,
BORDER_STYLE,
} from '../../../../helpers/constants/design-system';
+import { useCopyToClipboard } from '../../../../hooks/useCopyToClipboard';
+import UrlIcon from '../../../ui/url-icon/url-icon';
+import { getAddressBookEntry } from '../../../../selectors';
-export default function ContractDetailsModal({ onClose, address, tokenName }) {
+export default function ContractDetailsModal({
+ onClose,
+ tokenName,
+ tokenAddress,
+ toAddress,
+ chainId,
+ rpcPrefs,
+ origin,
+ siteImage,
+}) {
const t = useI18nContext();
+ const [copiedTokenAddress, handleCopyTokenAddress] = useCopyToClipboard();
+ const [copiedToAddress, handleCopyToAddress] = useCopyToClipboard();
+
+ const addressBookEntry = useSelector((state) => ({
+ data: getAddressBookEntry(state, toAddress),
+ }));
return (
@@ -65,7 +86,7 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
>
@@ -74,7 +95,7 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
variant={TYPOGRAPHY.H5}
marginTop={4}
>
- {tokenName || ellipsify(address)}
+ {tokenName || ellipsify(tokenAddress)}
{tokenName && (
- {ellipsify(address)}
+ {ellipsify(tokenAddress)}
)}
@@ -91,10 +112,20 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
className="contract-details-modal__content__contract__buttons"
>
-
+
@@ -105,6 +136,19 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
);
}
ContractDetailsModal.propTypes = {
+ /**
+ * Function that should close the modal
+ */
onClose: PropTypes.func,
- address: PropTypes.string,
+ /**
+ * Name of the token that is waiting to be allowed
+ */
tokenName: PropTypes.string,
+ /**
+ * Address of the token that is waiting to be allowed
+ */
+ tokenAddress: PropTypes.string,
+ /**
+ * Contract address requesting spending cap
+ */
+ toAddress: PropTypes.string,
+ /**
+ * Current network chainId
+ */
+ chainId: PropTypes.string,
+ /**
+ * RPC prefs of the current network
+ */
+ rpcPrefs: PropTypes.object,
+ /**
+ * Dapp URL
+ */
+ origin: PropTypes.string,
+ /**
+ * Dapp image
+ */
+ siteImage: PropTypes.string,
};
diff --git a/ui/components/app/modals/contract-details-modal/contract-details-modal.stories.js b/ui/components/app/modals/contract-details-modal/contract-details-modal.stories.js
index 8cf6272fc..fb022d134 100644
--- a/ui/components/app/modals/contract-details-modal/contract-details-modal.stories.js
+++ b/ui/components/app/modals/contract-details-modal/contract-details-modal.stories.js
@@ -1,23 +1,44 @@
-import React, { useState } from 'react';
-import Button from '../../../ui/button';
+import React from 'react';
import ContractDetailsModal from './contract-details-modal';
export default {
title: 'Components/App/Modals/ContractDetailsModal',
id: __filename,
argTypes: {
- onClosePopover: {
- action: 'Close Contract Details',
- },
- onOpenPopover: {
- action: 'Open Contract Details',
+ onClose: {
+ action: 'onClose',
},
tokenName: {
control: {
type: 'text',
},
},
- address: {
+ tokenAddress: {
+ control: {
+ type: 'text',
+ },
+ },
+ toAddress: {
+ control: {
+ type: 'text',
+ },
+ },
+ chainId: {
+ control: {
+ type: 'text',
+ },
+ },
+ rpcPrefs: {
+ control: {
+ type: 'object',
+ },
+ },
+ origin: {
+ control: {
+ type: 'text',
+ },
+ },
+ siteImage: {
control: {
type: 'text',
},
@@ -25,33 +46,17 @@ export default {
},
args: {
tokenName: 'DAI',
- address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ toAddress: '0x9bc5baf874d2da8d216ae9f137804184ee5afef4',
+ chainId: '0x3',
+ rpcPrefs: {},
+ origin: 'https://metamask.github.io',
+ siteImage: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
},
};
export const DefaultStory = (args) => {
- const [showContractDetails, setshowContractDetails] = useState(false);
- return (
- <>
- {
- args.onOpenPopover();
- setshowContractDetails(true);
- }}
- >
- Verify contract details
-
- {showContractDetails && (
- {
- args.onClosePopover();
- setshowContractDetails(false);
- }}
- {...args}
- />
- )}
- >
- );
+ return ;
};
DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/modals/contract-details-modal/index.scss b/ui/components/app/modals/contract-details-modal/index.scss
index b36649fd7..52af5a240 100644
--- a/ui/components/app/modals/contract-details-modal/index.scss
+++ b/ui/components/app/modals/contract-details-modal/index.scss
@@ -9,6 +9,10 @@
margin: 16px 16px 38px 16px;
}
+ &__identicon-for-unknown-contact {
+ margin: 16px;
+ }
+
&__buttons {
flex-grow: 1;
@@ -22,10 +26,4 @@
}
}
}
-
- &__footer {
- button + button {
- margin-inline-start: 1rem;
- }
- }
}
diff --git a/ui/components/ui/contract-token-values/contract-token-values.js b/ui/components/ui/contract-token-values/contract-token-values.js
index 1c7f4edb0..1b2635ad6 100644
--- a/ui/components/ui/contract-token-values/contract-token-values.js
+++ b/ui/components/ui/contract-token-values/contract-token-values.js
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { getAccountLink } from '@metamask/etherscan-link';
import IconCopy from '../icon/icon-copy';
import IconBlockExplorer from '../icon/icon-block-explorer';
import Box from '../box/box';
@@ -18,7 +19,12 @@ import {
import Button from '../button';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
-export default function ContractTokenValues({ address, tokenName }) {
+export default function ContractTokenValues({
+ address,
+ tokenName,
+ chainId,
+ rpcPrefs,
+}) {
const t = useI18nContext();
const [copied, handleCopy] = useCopyToClipboard();
@@ -62,6 +68,19 @@ export default function ContractTokenValues({ address, tokenName }) {
{
+ const blockExplorerTokenLink = getAccountLink(
+ address,
+ chainId,
+ {
+ blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
+ },
+ null,
+ );
+ global.platform.openTab({
+ url: blockExplorerTokenLink,
+ });
+ }}
>
@@ -80,4 +99,12 @@ ContractTokenValues.propTypes = {
* Displayed the token name currently tracked in state
*/
tokenName: PropTypes.string,
+ /**
+ * Current network chainId
+ */
+ chainId: PropTypes.string,
+ /**
+ * RPC prefs
+ */
+ rpcPrefs: PropTypes.object,
};
diff --git a/ui/components/ui/review-spending-cap/index.scss b/ui/components/ui/review-spending-cap/index.scss
index 110edfdf4..fdcb3d562 100644
--- a/ui/components/ui/review-spending-cap/index.scss
+++ b/ui/components/ui/review-spending-cap/index.scss
@@ -8,7 +8,7 @@
width: 180px;
&__warning-icon {
- color: var(--color-warning-default);
+ color: var(--color-error-default);
}
&__question-icon {
diff --git a/ui/components/ui/review-spending-cap/review-spending-cap.js b/ui/components/ui/review-spending-cap/review-spending-cap.js
index 760471628..38761de63 100644
--- a/ui/components/ui/review-spending-cap/review-spending-cap.js
+++ b/ui/components/ui/review-spending-cap/review-spending-cap.js
@@ -70,7 +70,7 @@ export default function ReviewSpendingCap({
key="tooltip-text"
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.BOLD}
- color={COLORS.WARNING_DEFAULT}
+ color={COLORS.ERROR_DEFAULT}
>
{' '}
{t('beCareful')}
@@ -110,7 +110,7 @@ export default function ReviewSpendingCap({
as={TYPOGRAPHY.H6}
color={
tokenValue > currentTokenBalance
- ? COLORS.WARNING_DEFAULT
+ ? COLORS.ERROR_DEFAULT
: COLORS.TEXT_DEFAULT
}
variant={TYPOGRAPHY.H6}
diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js
index 5af747801..26c065b44 100644
--- a/ui/pages/confirm-approve/confirm-approve.js
+++ b/ui/pages/confirm-approve/confirm-approve.js
@@ -36,6 +36,7 @@ import Loading from '../../components/ui/loading-screen';
import { parseStandardTokenTransactionData } from '../../../shared/modules/transaction.utils';
import { ERC1155, ERC20, ERC721 } from '../../../shared/constants/transaction';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
+import TokenAllowance from '../token-allowance/token-allowance';
import { getCustomTxParamsData } from './confirm-approve.util';
import ConfirmApproveContent from './confirm-approve-content';
@@ -157,130 +158,174 @@ export default function ConfirmApprove({
parseStandardTokenTransactionData(transactionData);
const isApprovalOrRejection = getTokenApprovedParam(parsedTransactionData);
- return tokenSymbol === undefined && assetName === undefined ? (
-
- ) : (
- !process.env.TOKEN_ALLOWANCE_IMPROVEMENTS && (
+ if (tokenSymbol === undefined && assetName === undefined) {
+ return ;
+ }
+ if (process.env.TOKEN_ALLOWANCE_IMPROVEMENTS && assetStandard === ERC20) {
+ return (
-
-
- dispatch(
- showModal({
- name: 'EDIT_APPROVAL_PERMISSION',
- customTokenAmount,
- decimals,
- origin,
- setCustomAmount,
- tokenAmount,
- tokenBalance,
- tokenSymbol,
- tokenId,
- assetStandard,
- }),
- )
- }
- data={customData || transactionData}
- toAddress={toAddress}
- currentCurrency={currentCurrency}
- nativeCurrency={nativeCurrency}
- ethTransactionTotal={ethTransactionTotal}
- fiatTransactionTotal={fiatTransactionTotal}
- hexTransactionTotal={hexTransactionTotal}
- 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}
- txData={transaction}
- fromAddressIsLedger={fromAddressIsLedger}
- chainId={chainId}
- rpcPrefs={rpcPrefs}
- isContract={isContract}
- isMultiLayerFeeNetwork={isMultiLayerFeeNetwork}
- supportsEIP1559V2={supportsEIP1559V2}
- />
- {showCustomizeGasPopover && !supportsEIP1559V2 && (
-
- )}
- {supportsEIP1559V2 && (
- <>
-
-
- >
- )}
-
- }
- hideSenderToRecipient
- customTxParamsData={customData}
- assetStandard={assetStandard}
- />
+
+
+ {showCustomizeGasPopover && !supportsEIP1559V2 && (
+
+ )}
+ {supportsEIP1559V2 && (
+ <>
+
+
+ >
+ )}
+
- )
+ );
+ }
+ return (
+
+
+
+ dispatch(
+ showModal({
+ name: 'EDIT_APPROVAL_PERMISSION',
+ customTokenAmount,
+ decimals,
+ origin,
+ setCustomAmount,
+ tokenAmount,
+ tokenBalance,
+ tokenSymbol,
+ tokenId,
+ assetStandard,
+ }),
+ )
+ }
+ data={customData || transactionData}
+ toAddress={toAddress}
+ currentCurrency={currentCurrency}
+ nativeCurrency={nativeCurrency}
+ ethTransactionTotal={ethTransactionTotal}
+ fiatTransactionTotal={fiatTransactionTotal}
+ hexTransactionTotal={hexTransactionTotal}
+ 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}
+ txData={transaction}
+ fromAddressIsLedger={fromAddressIsLedger}
+ chainId={chainId}
+ rpcPrefs={rpcPrefs}
+ isContract={isContract}
+ isMultiLayerFeeNetwork={isMultiLayerFeeNetwork}
+ supportsEIP1559V2={supportsEIP1559V2}
+ />
+ {showCustomizeGasPopover && !supportsEIP1559V2 && (
+
+ )}
+ {supportsEIP1559V2 && (
+ <>
+
+
+ >
+ )}
+
+ }
+ hideSenderToRecipient
+ customTxParamsData={customData}
+ assetStandard={assetStandard}
+ />
+
);
}
diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss
index d6160c24f..ec51b6783 100644
--- a/ui/pages/pages.scss
+++ b/ui/pages/pages.scss
@@ -21,6 +21,7 @@
@import 'send/send';
@import 'settings/index';
@import 'swaps/index';
+@import 'token-allowance/index';
@import 'token-details/index';
@import 'unlock-page/index';
@import 'onboarding-flow/index';
diff --git a/ui/pages/token-allowance/index.js b/ui/pages/token-allowance/index.js
new file mode 100644
index 000000000..4f7bc6c57
--- /dev/null
+++ b/ui/pages/token-allowance/index.js
@@ -0,0 +1 @@
+export { default } from './token-allowance';
diff --git a/ui/pages/token-allowance/index.scss b/ui/pages/token-allowance/index.scss
new file mode 100644
index 000000000..057be3d10
--- /dev/null
+++ b/ui/pages/token-allowance/index.scss
@@ -0,0 +1,41 @@
+.token-allowance-container {
+ &__icon-display-content {
+ width: fit-content;
+ height: 40px;
+ box-sizing: border-box;
+ border-radius: 100px;
+ position: relative;
+
+ &__siteimage-identicon {
+ width: 24px;
+ height: 24px;
+ box-shadow: none;
+ background: none;
+ }
+ }
+
+ a.token-allowance-container__verify-link {
+ width: fit-content;
+ margin-inline-start: 96px;
+ margin-inline-end: 96px;
+ padding: 0;
+ }
+
+ a.token-allowance-container__view-details {
+ width: fit-content;
+ margin-inline-start: 108px;
+ margin-inline-end: 108px;
+ }
+
+ &__card-wrapper {
+ width: 100%;
+ }
+
+ &__data {
+ width: 100%;
+ }
+
+ &__full-tx-content {
+ max-width: 100%;
+ }
+}
diff --git a/ui/pages/token-allowance/token-allowance.js b/ui/pages/token-allowance/token-allowance.js
new file mode 100644
index 000000000..201c0a364
--- /dev/null
+++ b/ui/pages/token-allowance/token-allowance.js
@@ -0,0 +1,437 @@
+import React, { useState, useContext } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import Box from '../../components/ui/box/box';
+import NetworkAccountBalanceHeader from '../../components/app/network-account-balance-header/network-account-balance-header';
+import UrlIcon from '../../components/ui/url-icon/url-icon';
+import Typography from '../../components/ui/typography/typography';
+import {
+ ALIGN_ITEMS,
+ BORDER_STYLE,
+ COLORS,
+ DISPLAY,
+ FLEX_DIRECTION,
+ FONT_WEIGHT,
+ JUSTIFY_CONTENT,
+ TEXT_ALIGN,
+ TYPOGRAPHY,
+} from '../../helpers/constants/design-system';
+import { I18nContext } from '../../contexts/i18n';
+import ContractTokenValues from '../../components/ui/contract-token-values/contract-token-values';
+import Button from '../../components/ui/button';
+import ReviewSpendingCap from '../../components/ui/review-spending-cap/review-spending-cap';
+import { PageContainerFooter } from '../../components/ui/page-container';
+import ContractDetailsModal from '../../components/app/modals/contract-details-modal/contract-details-modal';
+import {
+ getCurrentAccountWithSendEtherInfo,
+ getNetworkIdentifier,
+ transactionFeeSelector,
+ getKnownMethodData,
+ getRpcPrefsForCurrentProvider,
+} from '../../selectors';
+import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
+import {
+ cancelTx,
+ updateAndApproveTx,
+ updateCustomNonce,
+} from '../../store/actions';
+import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
+import { getMostRecentOverviewPage } from '../../ducks/history/history';
+import ApproveContentCard from '../../components/app/approve-content-card/approve-content-card';
+
+export default function TokenAllowance({
+ origin,
+ siteImage,
+ showCustomizeGasModal,
+ useNonceField,
+ currentCurrency,
+ nativeCurrency,
+ ethTransactionTotal,
+ fiatTransactionTotal,
+ hexTransactionTotal,
+ txData,
+ isMultiLayerFeeNetwork,
+ supportsEIP1559V2,
+ userAddress,
+ tokenAddress,
+ data,
+ isSetApproveForAll,
+ isApprovalOrRejection,
+ customTxParamsData,
+ dappProposedTokenAmount,
+ currentTokenBalance,
+ toAddress,
+ tokenSymbol,
+}) {
+ const t = useContext(I18nContext);
+ const dispatch = useDispatch();
+ const history = useHistory();
+ const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
+
+ const [showContractDetails, setShowContractDetails] = useState(false);
+ const [showFullTxDetails, setShowFullTxDetails] = useState(false);
+ const [isFirstPage, setIsFirstPage] = useState(false);
+
+ const currentAccount = useSelector(getCurrentAccountWithSendEtherInfo);
+ const networkIdentifier = useSelector(getNetworkIdentifier);
+ const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
+
+ let fullTxData = { ...txData };
+
+ if (customTxParamsData) {
+ fullTxData = {
+ ...fullTxData,
+ txParams: {
+ ...fullTxData.txParams,
+ data: customTxParamsData,
+ },
+ };
+ }
+
+ const fee = useSelector((state) => transactionFeeSelector(state, fullTxData));
+ const methodData = useSelector((state) => getKnownMethodData(state, data));
+
+ const networkName =
+ NETWORK_TO_NAME_MAP[fullTxData.chainId] || networkIdentifier;
+
+ const customNonceValue = '';
+ const customNonceMerge = (transactionData) =>
+ customNonceValue
+ ? {
+ ...transactionData,
+ customNonceValue,
+ }
+ : transactionData;
+
+ const handleReject = () => {
+ dispatch(cancelTx(fullTxData)).then(() => {
+ dispatch(clearConfirmTransaction());
+ dispatch(updateCustomNonce(''));
+ history.push(mostRecentOverviewPage);
+ });
+ };
+
+ const handleApprove = () => {
+ const { name } = methodData;
+
+ if (fee.gasEstimationObject.baseFeePerGas) {
+ fullTxData.estimatedBaseFee = fee.gasEstimationObject.baseFeePerGas;
+ }
+
+ if (name) {
+ fullTxData.contractMethodName = name;
+ }
+
+ if (dappProposedTokenAmount) {
+ fullTxData.dappProposedTokenAmount = dappProposedTokenAmount;
+ fullTxData.originalApprovalAmount = dappProposedTokenAmount;
+ }
+
+ if (currentTokenBalance) {
+ fullTxData.currentTokenBalance = currentTokenBalance;
+ }
+
+ dispatch(updateAndApproveTx(customNonceMerge(fullTxData))).then(() => {
+ dispatch(clearConfirmTransaction());
+ dispatch(updateCustomNonce(''));
+ history.push(mostRecentOverviewPage);
+ });
+ };
+
+ return (
+
+
+
+ {!isFirstPage && (
+ setIsFirstPage(true)}>
+
+ {'<'} {t('back')}
+
+
+ )}
+
+
+
+ {isFirstPage ? 1 : 2} {t('ofTextNofM')} 2
+
+
+
+
+
+
+
+
+ {origin}
+
+
+
+
+
+ {isFirstPage ? t('setSpendingCap') : t('reviewSpendingCap')}
+
+
+
+
+
+
+ setShowContractDetails(true)}
+ className="token-allowance-container__verify-link"
+ >
+
+ {t('verifyContractDetails')}
+
+
+
+
+ setIsFirstPage(true)}
+ />
+
+ {!isFirstPage && (
+
+ }
+ title={t('transactionFee')}
+ showEdit
+ showAdvanceGasFeeOptions
+ onEditClick={showCustomizeGasModal}
+ renderTransactionDetailsContent
+ noBorder={useNonceField || !showFullTxDetails}
+ supportsEIP1559V2={supportsEIP1559V2}
+ isMultiLayerFeeNetwork={isMultiLayerFeeNetwork}
+ ethTransactionTotal={ethTransactionTotal}
+ nativeCurrency={nativeCurrency}
+ fullTxData={fullTxData}
+ hexTransactionTotal={hexTransactionTotal}
+ fiatTransactionTotal={fiatTransactionTotal}
+ currentCurrency={currentCurrency}
+ />
+
+ )}
+
+ setShowFullTxDetails(!showFullTxDetails)}
+ className="token-allowance-container__view-details"
+ >
+
+ {t('viewDetails')}
+
+ {showFullTxDetails ? (
+
+ ) : (
+
+ )}
+
+
+ {showFullTxDetails ? (
+
+
+ }
+ title={t('data')}
+ renderDataContent
+ noBorder
+ supportsEIP1559V2={supportsEIP1559V2}
+ isSetApproveForAll={isSetApproveForAll}
+ isApprovalOrRejection={isApprovalOrRejection}
+ data={data}
+ />
+
+
+ ) : null}
+ handleReject()}
+ onSubmit={() => (isFirstPage ? setIsFirstPage(false) : handleApprove())}
+ />
+ {showContractDetails && (
+ setShowContractDetails(false)}
+ tokenAddress={tokenAddress}
+ toAddress={toAddress}
+ chainId={fullTxData.chainId}
+ rpcPrefs={rpcPrefs}
+ origin={origin}
+ siteImage={siteImage}
+ />
+ )}
+
+ );
+}
+
+TokenAllowance.propTypes = {
+ /**
+ * Dapp URL
+ */
+ origin: PropTypes.string,
+ /**
+ * Dapp image
+ */
+ siteImage: PropTypes.string,
+ /**
+ * Function that is supposed to open the customized gas modal
+ */
+ showCustomizeGasModal: PropTypes.func,
+ /**
+ * Whether nonce field should be used or not
+ */
+ useNonceField: PropTypes.bool,
+ /**
+ * Current fiat currency (e.g. USD)
+ */
+ currentCurrency: PropTypes.string,
+ /**
+ * Current native currency (e.g. RopstenETH)
+ */
+ nativeCurrency: PropTypes.string,
+ /**
+ * Total sum of the transaction in native currency
+ */
+ ethTransactionTotal: PropTypes.string,
+ /**
+ * Total sum of the transaction in fiat currency
+ */
+ fiatTransactionTotal: PropTypes.string,
+ /**
+ * Total sum of the transaction converted to hex value
+ */
+ hexTransactionTotal: PropTypes.string,
+ /**
+ * Current transaction
+ */
+ txData: PropTypes.object,
+ /**
+ * Is multi-layer fee network or not
+ */
+ isMultiLayerFeeNetwork: PropTypes.bool,
+ /**
+ * Is the enhanced gas fee enabled or not
+ */
+ supportsEIP1559V2: PropTypes.bool,
+ /**
+ * User's address
+ */
+ userAddress: PropTypes.string,
+ /**
+ * Address of the token that is waiting to be allowed
+ */
+ tokenAddress: PropTypes.string,
+ /**
+ * Current transaction data
+ */
+ data: 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,
+ /**
+ * Custom transaction parameters data made by the user (fees)
+ */
+ customTxParamsData: PropTypes.object,
+ /**
+ * Token amount proposed by the Dapp
+ */
+ dappProposedTokenAmount: PropTypes.string,
+ /**
+ * Token balance of the current account
+ */
+ currentTokenBalance: PropTypes.string,
+ /**
+ * Contract address requesting spending cap
+ */
+ toAddress: PropTypes.string,
+ /**
+ * Symbol of the token that is waiting to be allowed
+ */
+ tokenSymbol: PropTypes.string,
+};
diff --git a/ui/pages/token-allowance/token-allowance.stories.js b/ui/pages/token-allowance/token-allowance.stories.js
new file mode 100644
index 000000000..39875bfc6
--- /dev/null
+++ b/ui/pages/token-allowance/token-allowance.stories.js
@@ -0,0 +1,201 @@
+import React from 'react';
+import TokenAllowance from './token-allowance';
+
+export default {
+ title: 'Pages/TokenAllowance',
+ id: __filename,
+ argTypes: {
+ origin: {
+ control: 'text',
+ },
+ siteImage: {
+ control: 'text',
+ },
+ showCustomizeGasModal: {
+ action: 'showCustomizeGasModal',
+ },
+ useNonceField: {
+ control: 'boolean',
+ },
+ currentCurrency: {
+ control: 'text',
+ },
+ nativeCurrency: {
+ control: 'text',
+ },
+ ethTransactionTotal: {
+ control: 'text',
+ },
+ fiatTransactionTotal: {
+ control: 'text',
+ },
+ hexTransactionTotal: {
+ control: 'text',
+ },
+ isMultiLayerFeeNetwork: {
+ control: 'text',
+ },
+ supportsEIP1559V2: {
+ control: 'boolean',
+ },
+ userAddress: {
+ control: 'text',
+ },
+ tokenAddress: {
+ control: 'text',
+ },
+ data: {
+ control: 'text',
+ },
+ isSetApproveForAll: {
+ control: 'boolean',
+ },
+ setApproveForAllArg: {
+ control: 'boolean',
+ },
+ customTxParamsData: {
+ control: 'object',
+ },
+ dappProposedTokenAmount: {
+ control: 'text',
+ },
+ currentTokenBalance: {
+ control: 'text',
+ },
+ toAddress: {
+ control: 'text',
+ },
+ tokenSymbol: {
+ control: 'text',
+ },
+ txData: {
+ control: 'object',
+ },
+ },
+ args: {
+ origin: 'https://metamask.github.io',
+ siteImage: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
+ useNonceField: false,
+ currentCurrency: 'usd',
+ nativeCurrency: 'RopstenETH',
+ ethTransactionTotal: '0.0012',
+ fiatTransactionTotal: '1.6',
+ hexTransactionTotal: '0x44364c5bb0000',
+ isMultiLayerFeeNetwork: false,
+ supportsEIP1559V2: false,
+ userAddress: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
+ tokenAddress: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
+ data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
+ isSetApproveForAll: false,
+ setApproveForAllArg: false,
+ customTxParamsData: {},
+ dappProposedTokenAmount: '7',
+ currentTokenBalance: '10',
+ toAddress: '0x9bc5baf874d2da8d216ae9f137804184ee5afef4',
+ tokenSymbol: 'TST',
+ txData: {
+ 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';