1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Add setApprovalForAll confirmation view (#15010)

* enhance setApprovalForAll confirmation flow

* cleanup

* address feedback
This commit is contained in:
Alex Donesky 2022-07-11 18:32:55 -05:00 committed by GitHub
parent 2a73dea54d
commit 4f0115fcdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 154 additions and 16 deletions

View File

@ -230,6 +230,10 @@
"alerts": {
"message": "Alerts"
},
"allOfYour": {
"message": "All of your $1",
"description": "$1 is the symbol or name of the token that the user is approving spending"
},
"allowExternalExtensionTo": {
"message": "Allow this external extension to:"
},
@ -266,6 +270,10 @@
"approve": {
"message": "Approve spend limit"
},
"approveAllTokensTitle": {
"message": "Give permission to access all of your $1?",
"description": "$1 is the symbol of the token for which the user is granting approval"
},
"approveAndInstall": {
"message": "Approve & Install"
},
@ -1287,6 +1295,9 @@
"functionApprove": {
"message": "Function: Approve"
},
"functionSetApprovalForAll": {
"message": "Function: SetApprovalForAll"
},
"functionType": {
"message": "Function Type"
},
@ -2696,6 +2707,14 @@
"revealTheSeedPhrase": {
"message": "Reveal seed phrase"
},
"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"
},
"revokeApproveForAllDescription": {
"message": "By revoking permission, the following $1 will no longer be able to access your $2",
"description": "$1 is either key 'account' or 'contract', and $2 is either a string or link of a given token symbol or name"
},
"rinkeby": {
"message": "Rinkeby Test Network"
},
@ -2878,6 +2897,13 @@
"setAdvancedPrivacySettingsDetails": {
"message": "MetaMask uses these trusted third-party services to enhance product usability and safety."
},
"setApprovalForAll": {
"message": "Set Approval for All"
},
"setApprovalForAllTitle": {
"message": "Approve $1 with no spend limit",
"description": "The token symbol that is being approved"
},
"settings": {
"message": "Settings"
},

View File

@ -15,6 +15,8 @@ import { MESSAGE_TYPE } from './app';
* to ensure that the receiver is an address capable of handling with the token being sent.
* @property {'approve'} TOKEN_METHOD_APPROVE - A token transaction requesting an
* allowance of the token to spend on behalf of the user
* @property {'setapprovalforall'} TOKEN_METHOD_SET_APPROVAL_FOR_ALL - A token transaction requesting an
* allowance of all of a user's token to spend on behalf of the user
* @property {'incoming'} INCOMING - An incoming (deposit) transaction
* @property {'simpleSend'} SIMPLE_SEND - A transaction sending a network's native asset to a recipient
* @property {'contractInteraction'} CONTRACT_INTERACTION - A transaction that is
@ -66,6 +68,7 @@ export const TRANSACTION_TYPES = {
TOKEN_METHOD_SAFE_TRANSFER_FROM: 'safetransferfrom',
TOKEN_METHOD_TRANSFER: 'transfer',
TOKEN_METHOD_TRANSFER_FROM: 'transferfrom',
TOKEN_METHOD_SET_APPROVAL_FOR_ALL: 'setapprovalforall',
};
/**

View File

@ -8,7 +8,7 @@ import { readAddressAsContract } from './contract-utils';
import { isEqualCaseInsensitive } from './string-utils';
/**
* @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes
* @typedef { 'transfer' | 'approve' | 'setapprovalforall' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes
*/
/**
@ -150,6 +150,7 @@ export async function determineTransactionType(txParams, query) {
const tokenMethodName = [
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM,
@ -181,6 +182,7 @@ export async function determineTransactionType(txParams, query) {
const INFERRABLE_TRANSACTION_TYPES = [
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
TRANSACTION_TYPES.CONTRACT_INTERACTION,
@ -220,6 +222,7 @@ export async function determineTransactionAssetType(
// method to get the asset type.
const isTokenMethod = [
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
].find((methodName) => methodName === inferrableType);

View File

@ -53,7 +53,8 @@ const ConfirmPageContainerSummary = (props) => {
contractAddress =
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER ||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM ||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM ||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL
? tokenAddress
: toAddress;
}

View File

@ -251,7 +251,10 @@ export default class TransactionListItemDetails extends PureComponent {
<div className="transaction-list-item-details__cards-container">
<TransactionBreakdown
nonce={transactionGroup.initialTransaction.txParams.nonce}
isTokenApprove={type === TRANSACTION_TYPES.TOKEN_METHOD_APPROVE}
isTokenApprove={
type === TRANSACTION_TYPES.TOKEN_METHOD_APPROVE ||
type === TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL
}
transaction={transaction}
primaryCurrency={primaryCurrency}
className="transaction-list-item-details__transaction-breakdown"

View File

@ -90,6 +90,7 @@ const CONFIRM_SEND_ETHER_PATH = '/send-ether';
const CONFIRM_SEND_TOKEN_PATH = '/send-token';
const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract';
const CONFIRM_APPROVE_PATH = '/approve';
const CONFIRM_SET_APPROVAL_FOR_ALL_PATH = '/set-approval-for-all';
const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from';
const CONFIRM_SAFE_TRANSFER_FROM_PATH = '/safe-transfer-from';
const CONFIRM_TOKEN_METHOD_PATH = '/token-method';
@ -145,6 +146,7 @@ const PATH_NAME_MAP = {
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_TOKEN_PATH}`]: 'Confirm Send Token Transaction Page',
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_DEPLOY_CONTRACT_PATH}`]: 'Confirm Deploy Contract Transaction Page',
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_APPROVE_PATH}`]: 'Confirm Approve Transaction Page',
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`]: 'Confirm Set Approval For All Transaction Page',
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TRANSFER_FROM_PATH}`]: 'Confirm Transfer From Transaction Page',
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SAFE_TRANSFER_FROM_PATH}`]: 'Confirm Safe Transfer From Transaction Page',
[`${CONFIRM_TRANSACTION_ROUTE}/:id${SIGNATURE_REQUEST_PATH}`]: 'Signature Request Page',
@ -206,6 +208,7 @@ export {
CONFIRM_SEND_TOKEN_PATH,
CONFIRM_DEPLOY_CONTRACT_PATH,
CONFIRM_APPROVE_PATH,
CONFIRM_SET_APPROVAL_FOR_ALL_PATH,
CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_SAFE_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH,

View File

@ -17,6 +17,7 @@ export const PRIORITY_STATUS_HASH = {
export const TOKEN_CATEGORY_HASH = {
[TRANSACTION_TYPES.TOKEN_METHOD_APPROVE]: true,
[TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL]: true,
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER]: true,
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM]: true,
};

View File

@ -151,6 +151,10 @@ export function getTokenValueParam(tokenData = {}) {
return tokenData?.args?._value?.toString();
}
export function getTokenApprovedParam(tokenData = {}) {
return tokenData?.args?._approved;
}
export function getTokenValue(tokenParams = []) {
const valueData = tokenParams.find((param) => param.name === '_value');
return valueData && valueData.value;

View File

@ -116,6 +116,7 @@ export function isTokenMethodAction(type) {
return [
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM,
].includes(type);
@ -217,6 +218,9 @@ export function getTransactionTypeTitle(t, type, nativeCurrency = 'ETH') {
case TRANSACTION_TYPES.TOKEN_METHOD_APPROVE: {
return t('approve');
}
case TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL: {
return t('setApprovalForAll');
}
case TRANSACTION_TYPES.SIMPLE_SEND: {
return t('sendingNativeAsset', [nativeCurrency]);
}

View File

@ -222,6 +222,12 @@ export function useTransactionDisplayData(transactionGroup) {
title = t('approveSpendLimit', [token?.symbol || t('token')]);
subtitle = origin;
subtitleContainsOrigin = true;
} else if (type === TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL) {
category = TRANSACTION_GROUP_CATEGORIES.APPROVAL;
prefix = '';
title = t('setApprovalForAllTitle', [token?.symbol || t('token')]);
subtitle = origin;
subtitleContainsOrigin = true;
} else if (type === TRANSACTION_TYPES.CONTRACT_INTERACTION) {
category = TRANSACTION_GROUP_CATEGORIES.INTERACTION;
const transactionTypeTitle = getTransactionTypeTitle(t, type);

View File

@ -71,6 +71,8 @@ export default class ConfirmApproveContent extends Component {
assetName: PropTypes.string,
tokenId: PropTypes.string,
assetStandard: PropTypes.string,
isSetApproveForAll: PropTypes.bool,
setApproveForAllArg: PropTypes.bool,
};
state = {
@ -184,7 +186,7 @@ export default class ConfirmApproveContent extends Component {
renderERC721OrERC1155PermissionContent() {
const { t } = this.context;
const { origin, toAddress, isContract } = this.props;
const { origin, toAddress, isContract, isSetApproveForAll } = this.props;
const titleTokenDescription = this.getTitleTokenDescription();
@ -201,7 +203,9 @@ export default class ConfirmApproveContent extends Component {
{t('approvedAsset')}:
</div>
<div className="confirm-approve-content__medium-text">
{titleTokenDescription}
{isSetApproveForAll
? `${t('allOfYour', [titleTokenDescription])} `
: titleTokenDescription}
</div>
</div>
<div className="flex-row">
@ -299,12 +303,19 @@ export default class ConfirmApproveContent extends Component {
renderDataContent() {
const { t } = this.context;
const { data } = this.props;
const { data, isSetApproveForAll, setApproveForAllArg } = this.props;
return (
<div className="flex-column">
<div className="confirm-approve-content__small-text">
{t('functionApprove')}
{isSetApproveForAll
? t('functionSetApprovalForAll')
: t('functionApprove')}
</div>
{isSetApproveForAll && setApproveForAllArg !== undefined ? (
<div className="confirm-approve-content__small-text">
{t('parameters')}: {setApproveForAllArg}
</div>
) : null}
<div className="confirm-approve-content__small-text confirm-approve-content__data__data-block">
{data}
</div>
@ -509,6 +520,41 @@ export default class ConfirmApproveContent extends Component {
return titleTokenDescription;
}
renderTitle() {
const { t } = this.context;
const { isSetApproveForAll, setApproveForAllArg } = this.props;
const titleTokenDescription = this.getTitleTokenDescription();
let title;
if (isSetApproveForAll) {
title = t('approveAllTokensTitle', [titleTokenDescription]);
if (setApproveForAllArg === false) {
title = t('revokeAllTokensTitle', [titleTokenDescription]);
}
}
return title || t('allowSpendToken', [titleTokenDescription]);
}
renderDescription() {
const { t } = this.context;
const { isContract, isSetApproveForAll, setApproveForAllArg } = this.props;
const grantee = isContract
? t('contract').toLowerCase()
: t('account').toLowerCase();
let description = t('trustSiteApprovePermission', [grantee]);
if (isSetApproveForAll && setApproveForAllArg === false) {
description = t('revokeApproveForAllDescription', [
grantee,
this.getTitleTokenDescription(),
]);
}
return description;
}
render() {
const { t } = this.context;
const {
@ -534,8 +580,6 @@ export default class ConfirmApproveContent extends Component {
} = this.props;
const { showFullTxDetails } = this.state;
const titleTokenDescription = this.getTitleTokenDescription();
return (
<div
className={classnames('confirm-approve-content', {
@ -575,14 +619,10 @@ export default class ConfirmApproveContent extends Component {
</Box>
</Box>
<div className="confirm-approve-content__title">
{t('allowSpendToken', [titleTokenDescription])}
{this.renderTitle()}
</div>
<div className="confirm-approve-content__description">
{t('trustSiteApprovePermission', [
isContract
? t('contract').toLowerCase()
: t('account').toLowerCase(),
])}
{this.renderDescription()}
</div>
<Box className="confirm-approve-content__address-display-content">
<Box display={DISPLAY.FLEX}>

View File

@ -8,7 +8,10 @@ import {
updateCustomNonce,
getNextNonce,
} from '../../store/actions';
import { calcTokenAmount } from '../../helpers/utils/token-util';
import {
calcTokenAmount,
getTokenApprovedParam,
} from '../../helpers/utils/token-util';
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
import { GasFeeContextProvider } from '../../contexts/gasFee';
import { TransactionModalContextProvider } from '../../contexts/transaction-modal';
@ -34,6 +37,7 @@ import EditGasFeePopover from '../../components/app/edit-gas-fee-popover';
import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component';
import Loading from '../../components/ui/loading-screen';
import { ERC20, ERC1155, ERC721 } from '../../helpers/constants/common';
import { parseStandardTokenTransactionData } from '../../../shared/modules/transaction.utils';
import { getCustomTxParamsData } from './confirm-approve.util';
import ConfirmApproveContent from './confirm-approve-content';
@ -57,6 +61,7 @@ export default function ConfirmApprove({
ethTransactionTotal,
fiatTransactionTotal,
hexTransactionTotal,
isSetApproveForAll,
}) {
const dispatch = useDispatch();
const { txParams: { data: transactionData } = {} } = transaction;
@ -150,6 +155,11 @@ export default function ConfirmApprove({
})
: null;
const parsedTransactionData = parseStandardTokenTransactionData(
transactionData,
);
const setApproveForAllArg = getTokenApprovedParam(parsedTransactionData);
return tokenSymbol === undefined && assetName === undefined ? (
<Loading />
) : (
@ -162,6 +172,8 @@ export default function ConfirmApprove({
contentComponent={
<TransactionModalContextProvider>
<ConfirmApproveContent
isSetApproveForAll={isSetApproveForAll}
setApproveForAllArg={setApproveForAllArg}
decimals={decimals}
siteImage={siteImage}
setCustomAmount={setCustomPermissionAmount}
@ -290,4 +302,5 @@ ConfirmApprove.propTypes = {
ethTransactionTotal: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
hexTransactionTotal: PropTypes.string,
isSetApproveForAll: PropTypes.bool,
};

View File

@ -338,6 +338,7 @@ export default class ConfirmTransactionBase extends Component {
};
const hasSimulationError = Boolean(txData.simulationFails);
const renderSimulationFailureWarning =
hasSimulationError && !userAcknowledgedGasMissing;
const networkName = NETWORK_TO_NAME_MAP[txData.chainId];

View File

@ -14,6 +14,7 @@ import {
DECRYPT_MESSAGE_REQUEST_PATH,
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
CONFIRM_SAFE_TRANSFER_FROM_PATH,
CONFIRM_SET_APPROVAL_FOR_ALL_PATH,
} from '../../helpers/constants/routes';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction';
@ -47,6 +48,10 @@ export default class ConfirmTransactionSwitch extends Component {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`;
return <Redirect to={{ pathname }} />;
}
case TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL: {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`;
return <Redirect to={{ pathname }} />;
}
case TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM: {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`;
return <Redirect to={{ pathname }} />;

View File

@ -6,6 +6,7 @@ import {
CONFIRM_APPROVE_PATH,
CONFIRM_SAFE_TRANSFER_FROM_PATH,
CONFIRM_SEND_TOKEN_PATH,
CONFIRM_SET_APPROVAL_FOR_ALL_PATH,
CONFIRM_TRANSACTION_ROUTE,
CONFIRM_TRANSFER_FROM_PATH,
} from '../../helpers/constants/routes';
@ -66,6 +67,30 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) {
/>
)}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`}
render={() => (
<ConfirmApprove
isSetApproveForAll
assetStandard={assetStandard}
assetName={assetName}
userBalance={userBalance}
tokenSymbol={tokenSymbol}
decimals={decimals}
tokenImage={tokenImage}
tokenAmount={tokenAmount}
tokenId={tokenId}
userAddress={userAddress}
tokenAddress={tokenAddress}
toAddress={toAddress}
transaction={transaction}
ethTransactionTotal={ethTransactionTotal}
fiatTransactionTotal={fiatTransactionTotal}
hexTransactionTotal={hexTransactionTotal}
/>
)}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}