From a993509afcc195556e3d02ee62776c47ad4dc80f Mon Sep 17 00:00:00 2001 From: Filip Sekulic Date: Wed, 5 Oct 2022 21:48:35 +0200 Subject: [PATCH] Review spending cap screen (#15919) --- app/_locales/en/messages.json | 12 + ui/components/app/app-components.scss | 1 + .../approve-content-card.js | 241 ++++++++++ .../app/approve-content-card/index.js | 1 + .../app/approve-content-card/index.scss | 51 ++ .../contract-details-modal.js | 146 +++++- .../contract-details-modal.stories.js | 67 +-- .../modals/contract-details-modal/index.scss | 10 +- .../contract-token-values.js | 29 +- .../ui/review-spending-cap/index.scss | 2 +- .../review-spending-cap.js | 4 +- ui/pages/confirm-approve/confirm-approve.js | 289 +++++++----- ui/pages/pages.scss | 1 + ui/pages/token-allowance/index.js | 1 + ui/pages/token-allowance/index.scss | 41 ++ ui/pages/token-allowance/token-allowance.js | 437 ++++++++++++++++++ .../token-allowance.stories.js | 201 ++++++++ 17 files changed, 1348 insertions(+), 186 deletions(-) create mode 100644 ui/components/app/approve-content-card/approve-content-card.js create mode 100644 ui/components/app/approve-content-card/index.js create mode 100644 ui/components/app/approve-content-card/index.scss create mode 100644 ui/pages/token-allowance/index.js create mode 100644 ui/pages/token-allowance/index.scss create mode 100644 ui/pages/token-allowance/token-allowance.js create mode 100644 ui/pages/token-allowance/token-allowance.stories.js 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 }) { @@ -173,6 +238,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 ( - <> - - {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 }) { @@ -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 && ( + + )} + + + + {isFirstPage ? 1 : 2} {t('ofTextNofM')} 2 + + + + + + + + + {origin} + + + + + + {isFirstPage ? t('setSpendingCap') : t('reviewSpendingCap')} + + + + + + + + + + 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} + /> + + )} + + + + {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';