Review spending cap screen (#15919)

feature/default_network_editable
Filip Sekulic 2 years ago committed by GitHub
parent d97b9c7eef
commit a993509afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      app/_locales/en/messages.json
  2. 1
      ui/components/app/app-components.scss
  3. 241
      ui/components/app/approve-content-card/approve-content-card.js
  4. 1
      ui/components/app/approve-content-card/index.js
  5. 51
      ui/components/app/approve-content-card/index.scss
  6. 146
      ui/components/app/modals/contract-details-modal/contract-details-modal.js
  7. 67
      ui/components/app/modals/contract-details-modal/contract-details-modal.stories.js
  8. 10
      ui/components/app/modals/contract-details-modal/index.scss
  9. 29
      ui/components/ui/contract-token-values/contract-token-values.js
  10. 2
      ui/components/ui/review-spending-cap/index.scss
  11. 4
      ui/components/ui/review-spending-cap/review-spending-cap.js
  12. 289
      ui/pages/confirm-approve/confirm-approve.js
  13. 1
      ui/pages/pages.scss
  14. 1
      ui/pages/token-allowance/index.js
  15. 41
      ui/pages/token-allowance/index.scss
  16. 437
      ui/pages/token-allowance/token-allowance.js
  17. 201
      ui/pages/token-allowance/token-allowance.stories.js

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

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

@ -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 (
<Box
className={classnames({
'approve-content-card-container__card': !noBorder,
'approve-content-card-container__card--no-border': noBorder,
})}
>
{showHeader && (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.FLEX_END}
className="approve-content-card-container__card-header"
>
{supportsEIP1559V2 && title === t('transactionFee') ? null : (
<>
<Box className="approve-content-card-container__card-header__symbol">
{symbol}
</Box>
<Box
marginLeft={4}
className="approve-content-card-container__card-header__title"
>
<Typography
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.BOLD}
>
{title}
</Typography>
</Box>
</>
)}
{showEdit && (!showAdvanceGasFeeOptions || !supportsEIP1559V2) && (
<Box width={BLOCK_SIZES.ONE_SIXTH}>
<Button type="link" onClick={() => onEditClick()}>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.PRIMARY_DEFAULT}
>
{t('edit')}
</Typography>
</Button>
</Box>
)}
{showEdit && showAdvanceGasFeeOptions && supportsEIP1559V2 && (
<EditGasFeeButton />
)}
</Box>
)}
<Box
marginTop={1}
marginBottom={3}
className="approve-content-card-container__card-content"
>
{renderTransactionDetailsContent &&
(!isMultiLayerFeeNetwork && supportsEIP1559V2 ? (
<GasDetailsItem />
) : (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
>
{isMultiLayerFeeNetwork ? (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
className="approve-content-card-container__transaction-details-extra-content"
>
<Box
display={DISPLAY.FLEX}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
>
<Typography
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.NORMAL}
color={COLORS.TEXT_MUTED}
>
<span>{t('transactionDetailLayer2GasHeading')}</span>
{`${ethTransactionTotal} ${nativeCurrency}`}
</Typography>
</Box>
<MultiLayerFeeMessage
transaction={fullTxData}
layer2fee={hexTransactionTotal}
nativeCurrency={nativeCurrency}
plainStyle
/>
</Box>
) : (
<>
<Box>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_ALTERNATIVE}
>
{t('feeAssociatedRequest')}
</Typography>
</Box>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={ALIGN_ITEMS.FLEX_END}
textAlign={TEXT_ALIGN.RIGHT}
>
<Box>
<Typography
variant={TYPOGRAPHY.H4}
fontWeight={FONT_WEIGHT.BOLD}
color={COLORS.TEXT_DEFAULT}
>
{formatCurrency(fiatTransactionTotal, currentCurrency)}
</Typography>
</Box>
<Box>
<Typography
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.NORMAL}
color={COLORS.TEXT_MUTED}
>
{`${ethTransactionTotal} ${nativeCurrency}`}
</Typography>
</Box>
</Box>
</>
)}
</Box>
))}
{renderDataContent && (
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.COLUMN}>
<Box>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_ALTERNATIVE}
>
{isSetApproveForAll
? t('functionSetApprovalForAll')
: t('functionApprove')}
</Typography>
</Box>
{isSetApproveForAll && isApprovalOrRejection !== undefined ? (
<Box>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_ALTERNATIVE}
>
{`${t('parameters')}: ${isApprovalOrRejection}`}
</Typography>
</Box>
) : null}
<Box
marginRight={4}
className="approve-content-card-container__data__data-block"
>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_ALTERNATIVE}
>
{data}
</Typography>
</Box>
</Box>
)}
</Box>
{footer}
</Box>
);
}
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,
};

@ -0,0 +1 @@
export { default } from './approve-content-card';

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

@ -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 (
<Popover className="contract-details-modal">
@ -65,7 +86,7 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
>
<Identicon
className="contract-details-modal__content__contract__identicon"
address={address}
address={tokenAddress}
diameter={24}
/>
<Box data-testid="recipient">
@ -74,7 +95,7 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
variant={TYPOGRAPHY.H5}
marginTop={4}
>
{tokenName || ellipsify(address)}
{tokenName || ellipsify(tokenAddress)}
</Typography>
{tokenName && (
<Typography
@ -82,7 +103,7 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
display={DISPLAY.FLEX}
color={COLORS.TEXT_ALTERNATIVE}
>
{ellipsify(address)}
{ellipsify(tokenAddress)}
</Typography>
)}
</Box>
@ -91,10 +112,20 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
className="contract-details-modal__content__contract__buttons"
>
<Box marginTop={4} marginRight={5}>
<Tooltip position="top" title={t('copyToClipboard')}>
<Tooltip
position="top"
title={
copiedTokenAddress
? t('copiedExclamation')
: t('copyToClipboard')
}
>
<Button
className="contract-details-modal__content__contract__buttons__copy"
type="link"
onClick={() => {
handleCopyTokenAddress(tokenAddress);
}}
>
<IconCopy color="var(--color-icon-muted)" />
</Button>
@ -105,6 +136,19 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
<Button
className="contract-details-modal__content__contract__buttons__block-explorer"
type="link"
onClick={() => {
const blockExplorerTokenLink = getAccountLink(
tokenAddress,
chainId,
{
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
},
null,
);
global.platform.openTab({
url: blockExplorerTokenLink,
});
}}
>
<IconBlockExplorer
size={16}
@ -131,10 +175,21 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
borderColor={COLORS.BORDER_DEFAULT}
className="contract-details-modal__content__contract"
>
<Identicon
className="contract-details-modal__content__contract__identicon"
address={address}
diameter={24}
<UrlIcon
className={classnames({
'contract-details-modal__content__contract__identicon-for-unknown-contact':
addressBookEntry?.data?.name === undefined,
'contract-details-modal__content__contract__identicon':
addressBookEntry?.data?.name !== undefined,
})}
fallbackClassName={classnames({
'contract-details-modal__content__contract__identicon-for-unknown-contact':
addressBookEntry?.data?.name === undefined,
'contract-details-modal__content__contract__identicon':
addressBookEntry?.data?.name !== undefined,
})}
name={origin}
url={siteImage}
/>
<Box data-testid="recipient">
<Typography
@ -142,15 +197,15 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
variant={TYPOGRAPHY.H5}
marginTop={4}
>
{tokenName || ellipsify(address)}
{addressBookEntry?.data?.name || ellipsify(toAddress)}
</Typography>
{tokenName && (
{addressBookEntry?.data?.name && (
<Typography
variant={TYPOGRAPHY.H6}
display={DISPLAY.FLEX}
color={COLORS.TEXT_ALTERNATIVE}
>
{ellipsify(address)}
{ellipsify(toAddress)}
</Typography>
)}
</Box>
@ -159,10 +214,20 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
className="contract-details-modal__content__contract__buttons"
>
<Box marginTop={4} marginRight={5}>
<Tooltip position="top" title={t('copyToClipboard')}>
<Tooltip
position="top"
title={
copiedToAddress
? t('copiedExclamation')
: t('copyToClipboard')
}
>
<Button
className="contract-details-modal__content__contract__buttons__copy"
type="link"
onClick={() => {
handleCopyToAddress(toAddress);
}}
>
<IconCopy color="var(--color-icon-muted)" />
</Button>
@ -173,6 +238,19 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
<Button
className="contract-details-modal__content__contract__buttons__block-explorer"
type="link"
onClick={() => {
const blockExplorerTokenLink = getAccountLink(
toAddress,
chainId,
{
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
},
null,
);
global.platform.openTab({
url: blockExplorerTokenLink,
});
}}
>
<IconBlockExplorer
size={16}
@ -190,24 +268,46 @@ export default function ContractDetailsModal({ onClose, address, tokenName }) {
paddingRight={4}
paddingBottom={6}
paddingLeft={4}
className="contract-details-modal__footer"
>
<Button
type="secondary"
onClick={() => {
onClose();
}}
>
{t('cancel')}
<Button type="primary" onClick={() => onClose()}>
{t('recoveryPhraseReminderConfirm')}
</Button>
<Button type="primary">{t('confirm')}</Button>
</Box>
</Popover>
);
}
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,
};

@ -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 (
<>
<Button
onClick={() => {
args.onOpenPopover();
setshowContractDetails(true);
}}
>
Verify contract details
</Button>
{showContractDetails && (
<ContractDetailsModal
onClose={() => {
args.onClosePopover();
setshowContractDetails(false);
}}
{...args}
/>
)}
</>
);
return <ContractDetailsModal {...args} />;
};
DefaultStory.storyName = 'Default';

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

@ -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 }) {
<Button
type="link"
className="contract-token-values__block-explorer__button"
onClick={() => {
const blockExplorerTokenLink = getAccountLink(
address,
chainId,
{
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
},
null,
);
global.platform.openTab({
url: blockExplorerTokenLink,
});
}}
>
<IconBlockExplorer size={24} color="var(--color-icon-muted)" />
</Button>
@ -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,
};

@ -8,7 +8,7 @@
width: 180px;
&__warning-icon {
color: var(--color-warning-default);
color: var(--color-error-default);
}
&__question-icon {

@ -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}
>
<i className="fa fa-exclamation-circle" />{' '}
{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}

@ -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 ? (
<Loading />
) : (
!process.env.TOKEN_ALLOWANCE_IMPROVEMENTS && (
if (tokenSymbol === undefined && assetName === undefined) {
return <Loading />;
}
if (process.env.TOKEN_ALLOWANCE_IMPROVEMENTS && assetStandard === ERC20) {
return (
<GasFeeContextProvider transaction={transaction}>
<ConfirmTransactionBase
toAddress={toAddress}
identiconAddress={toAddress}
showAccountInHeader
title={tokensText}
customTokenAmount={String(customPermissionAmount)}
dappProposedTokenAmount={tokenAmount}
currentTokenBalance={tokenBalance}
isApprovalOrRejection={isApprovalOrRejection}
contentComponent={
<TransactionModalContextProvider>
<ConfirmApproveContent
userAddress={userAddress}
isSetApproveForAll={isSetApproveForAll}
isApprovalOrRejection={isApprovalOrRejection}
decimals={decimals}
siteImage={siteImage}
setCustomAmount={setCustomPermissionAmount}
customTokenAmount={String(customPermissionAmount)}
tokenAmount={tokenAmount}
origin={formattedOrigin}
tokenSymbol={tokenSymbol}
tokenImage={tokenImage}
tokenBalance={tokenBalance}
tokenId={tokenId}
assetName={assetName}
assetStandard={assetStandard}
tokenAddress={tokenAddress}
showCustomizeGasModal={approveTransaction}
showEditApprovalPermissionModal={({
/* eslint-disable no-shadow */
customTokenAmount,
decimals,
origin,
setCustomAmount,
tokenAmount,
tokenBalance,
tokenSymbol,
/* eslint-enable no-shadow */
}) =>
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 && (
<EditGasPopover
onClose={closeCustomizeGasPopover}
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
transaction={transaction}
/>
)}
{supportsEIP1559V2 && (
<>
<EditGasFeePopover />
<AdvancedGasFeePopover />
</>
)}
</TransactionModalContextProvider>
}
hideSenderToRecipient
customTxParamsData={customData}
assetStandard={assetStandard}
/>
<TransactionModalContextProvider>
<TokenAllowance
origin={formattedOrigin}
siteImage={siteImage}
showCustomizeGasModal={approveTransaction}
useNonceField={useNonceField}
currentCurrency={currentCurrency}
nativeCurrency={nativeCurrency}
ethTransactionTotal={ethTransactionTotal}
fiatTransactionTotal={fiatTransactionTotal}
hexTransactionTotal={hexTransactionTotal}
txData={transaction}
isMultiLayerFeeNetwork={isMultiLayerFeeNetwork}
supportsEIP1559V2={supportsEIP1559V2}
userAddress={userAddress}
tokenAddress={tokenAddress}
data={customData || transactionData}
isSetApproveForAll={isSetApproveForAll}
isApprovalOrRejection={isApprovalOrRejection}
customTxParamsData={customData}
dappProposedTokenAmount={tokenAmount}
currentTokenBalance={tokenBalance}
toAddress={toAddress}
tokenSymbol={tokenSymbol}
/>
{showCustomizeGasPopover && !supportsEIP1559V2 && (
<EditGasPopover
onClose={closeCustomizeGasPopover}
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
transaction={transaction}
/>
)}
{supportsEIP1559V2 && (
<>
<EditGasFeePopover />
<AdvancedGasFeePopover />
</>
)}
</TransactionModalContextProvider>
</GasFeeContextProvider>
)
);
}
return (
<GasFeeContextProvider transaction={transaction}>
<ConfirmTransactionBase
toAddress={toAddress}
identiconAddress={toAddress}
showAccountInHeader
title={tokensText}
customTokenAmount={String(customPermissionAmount)}
dappProposedTokenAmount={tokenAmount}
currentTokenBalance={tokenBalance}
isApprovalOrRejection={isApprovalOrRejection}
contentComponent={
<TransactionModalContextProvider>
<ConfirmApproveContent
userAddress={userAddress}
isSetApproveForAll={isSetApproveForAll}
isApprovalOrRejection={isApprovalOrRejection}
decimals={decimals}
siteImage={siteImage}
setCustomAmount={setCustomPermissionAmount}
customTokenAmount={String(customPermissionAmount)}
tokenAmount={tokenAmount}
origin={formattedOrigin}
tokenSymbol={tokenSymbol}
tokenImage={tokenImage}
tokenBalance={tokenBalance}
tokenId={tokenId}
assetName={assetName}
assetStandard={assetStandard}
tokenAddress={tokenAddress}
showCustomizeGasModal={approveTransaction}
showEditApprovalPermissionModal={({
/* eslint-disable no-shadow */
customTokenAmount,
decimals,
origin,
setCustomAmount,
tokenAmount,
tokenBalance,
tokenSymbol,
/* eslint-enable no-shadow */
}) =>
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 && (
<EditGasPopover
onClose={closeCustomizeGasPopover}
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
transaction={transaction}
/>
)}
{supportsEIP1559V2 && (
<>
<EditGasFeePopover />
<AdvancedGasFeePopover />
</>
)}
</TransactionModalContextProvider>
}
hideSenderToRecipient
customTxParamsData={customData}
assetStandard={assetStandard}
/>
</GasFeeContextProvider>
);
}

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

@ -0,0 +1 @@
export { default } from './token-allowance';

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

@ -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 (
<Box className="token-allowance-container page-container">
<Box
paddingLeft={4}
paddingRight={4}
alignItems={ALIGN_ITEMS.CENTER}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
>
<Box>
{!isFirstPage && (
<Button type="inline" onClick={() => setIsFirstPage(true)}>
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.TEXT_MUTED}
fontWeight={FONT_WEIGHT.BOLD}
>
{'<'} {t('back')}
</Typography>
</Button>
)}
</Box>
<Box textAlign={TEXT_ALIGN.END}>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_MUTED}
fontWeight={FONT_WEIGHT.BOLD}
>
{isFirstPage ? 1 : 2} {t('ofTextNofM')} 2
</Typography>
</Box>
</Box>
<NetworkAccountBalanceHeader
networkName={networkName}
accountName={currentAccount.name}
accountBalance={currentTokenBalance}
tokenName={tokenSymbol}
accountAddress={userAddress}
/>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JUSTIFY_CONTENT.CENTER}
>
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
marginTop={6}
marginRight={12}
marginBottom={8}
marginLeft={12}
paddingTop={2}
paddingRight={4}
paddingBottom={2}
paddingLeft={2}
borderColor={COLORS.BORDER_MUTED}
borderStyle={BORDER_STYLE.SOLID}
borderWidth={1}
className="token-allowance-container__icon-display-content"
>
<UrlIcon
className="token-allowance-container__icon-display-content__siteimage-identicon"
fallbackClassName="token-allowance-container__icon-display-content__siteimage-identicon"
name={origin}
url={siteImage}
/>
<Typography
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.NORMAL}
color={COLORS.TEXT_ALTERNATIVE}
boxProps={{ marginLeft: 1, marginTop: 2 }}
>
{origin}
</Typography>
</Box>
</Box>
<Box marginBottom={5}>
<Typography
variant={TYPOGRAPHY.H3}
fontWeight={FONT_WEIGHT.BOLD}
align={TEXT_ALIGN.CENTER}
>
{isFirstPage ? t('setSpendingCap') : t('reviewSpendingCap')}
</Typography>
</Box>
<Box>
<ContractTokenValues
tokenName={tokenSymbol}
address={tokenAddress}
chainId={fullTxData.chainId}
rpcPrefs={rpcPrefs}
/>
</Box>
<Box
marginTop={1}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JUSTIFY_CONTENT.CENTER}
>
<Button
type="link"
onClick={() => setShowContractDetails(true)}
className="token-allowance-container__verify-link"
>
<Typography variant={TYPOGRAPHY.H6} color={COLORS.PRIMARY_DEFAULT}>
{t('verifyContractDetails')}
</Typography>
</Button>
</Box>
<Box margin={[4, 4, 3, 4]}>
<ReviewSpendingCap
tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)}
tokenValue={10}
onEdit={() => setIsFirstPage(true)}
/>
</Box>
{!isFirstPage && (
<Box className="token-allowance-container__card-wrapper">
<ApproveContentCard
symbol={<i className="fa fa-tag" />}
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}
/>
</Box>
)}
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JUSTIFY_CONTENT.CENTER}
>
<Button
type="link"
onClick={() => setShowFullTxDetails(!showFullTxDetails)}
className="token-allowance-container__view-details"
>
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.PRIMARY_DEFAULT}
marginRight={1}
>
{t('viewDetails')}
</Typography>
{showFullTxDetails ? (
<i className="fa fa-sm fa-angle-up" />
) : (
<i className="fa fa-sm fa-angle-down" />
)}
</Button>
</Box>
{showFullTxDetails ? (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={ALIGN_ITEMS.CENTER}
className="token-allowance-container__full-tx-content"
>
<Box className="token-allowance-container__data">
<ApproveContentCard
symbol={<i className="fa fa-file" />}
title={t('data')}
renderDataContent
noBorder
supportsEIP1559V2={supportsEIP1559V2}
isSetApproveForAll={isSetApproveForAll}
isApprovalOrRejection={isApprovalOrRejection}
data={data}
/>
</Box>
</Box>
) : null}
<PageContainerFooter
cancelText={t('reject')}
submitText={isFirstPage ? t('next') : t('approveButtonText')}
onCancel={() => handleReject()}
onSubmit={() => (isFirstPage ? setIsFirstPage(false) : handleApprove())}
/>
{showContractDetails && (
<ContractDetailsModal
tokenName={tokenSymbol}
onClose={() => setShowContractDetails(false)}
tokenAddress={tokenAddress}
toAddress={toAddress}
chainId={fullTxData.chainId}
rpcPrefs={rpcPrefs}
origin={origin}
siteImage={siteImage}
/>
)}
</Box>
);
}
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,
};

@ -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 <TokenAllowance {...args} />;
};
DefaultStory.storyName = 'Default';
Loading…
Cancel
Save