Token allowance screen updated (#16157)

feature/default_network_editable
Filip Sekulic 2 years ago committed by GitHub
parent 0336a3c006
commit 17c1fef9be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/_locales/en/messages.json
  2. 63
      ui/components/app/approve-content-card/approve-content-card.js
  3. 196
      ui/components/app/approve-content-card/approve-content-card.stories.js
  4. 64
      ui/components/app/custom-spending-cap/custom-spending-cap.js
  5. 4
      ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
  6. 7
      ui/components/app/custom-spending-cap/index.scss
  7. 10
      ui/ducks/app/app.js
  8. 4
      ui/pages/confirm-approve/confirm-approve.js
  9. 96
      ui/pages/token-allowance/token-allowance.js
  10. 6
      ui/pages/token-allowance/token-allowance.stories.js
  11. 4
      ui/selectors/selectors.js
  12. 3
      ui/store/actionConstants.js

@ -2931,6 +2931,9 @@
"message": "By revoking permission, the following $1 will no longer be able to access your $2",
"description": "$1 is either key 'account' or 'contract', and $2 is either a string or link of a given token symbol or name"
},
"revokeSpendingCap": {
"message": "Revoke spending cap for your"
},
"revokeSpendingCapTooltipText": {
"message": "This contract will be unable to spend any more of your current or future tokens."
},

@ -217,25 +217,88 @@ export default function ApproveContentCard({
}
ApproveContentCard.propTypes = {
/**
* Whether to show header including icon, transaction fee text and edit button
*/
showHeader: PropTypes.bool,
/**
* Symbol icon
*/
symbol: PropTypes.node,
/**
* Title to be included in the header
*/
title: PropTypes.string,
/**
* Whether to show edit button or not
*/
showEdit: PropTypes.bool,
/**
* Whether to show advanced gas fee options or not
*/
showAdvanceGasFeeOptions: PropTypes.bool,
/**
* Should open customize gas modal when edit button is clicked
*/
onEditClick: PropTypes.func,
/**
* Footer to be shown
*/
footer: PropTypes.node,
/**
* Whether to include border-bottom or not
*/
noBorder: PropTypes.bool,
/**
* Is enhanced gas fee enabled or not
*/
supportsEIP1559V2: PropTypes.bool,
/**
* Whether to render transaction details content or not
*/
renderTransactionDetailsContent: PropTypes.bool,
/**
* Whether to render data content or not
*/
renderDataContent: PropTypes.bool,
/**
* Is multi-layer fee network or not
*/
isMultiLayerFeeNetwork: PropTypes.bool,
/**
* Total sum of the transaction in native currency
*/
ethTransactionTotal: PropTypes.string,
/**
* Current native currency
*/
nativeCurrency: PropTypes.string,
/**
* Current transaction
*/
fullTxData: PropTypes.object,
/**
* Total sum of the transaction converted to hex value
*/
hexTransactionTotal: PropTypes.string,
/**
* Total sum of the transaction in fiat currency
*/
fiatTransactionTotal: PropTypes.string,
/**
* Current fiat currency
*/
currentCurrency: PropTypes.string,
/**
* Is set approve for all or not
*/
isSetApproveForAll: PropTypes.bool,
/**
* Whether a current set approval for all transaction will approve or revoke access
*/
isApprovalOrRejection: PropTypes.bool,
/**
* Current transaction data
*/
data: PropTypes.string,
};

@ -0,0 +1,196 @@
import React from 'react';
import ApproveContentCard from './approve-content-card';
export default {
title: 'Components/App/ApproveContentCard',
id: __filename,
argTypes: {
showHeader: {
control: 'boolean',
},
symbol: {
control: 'array',
},
title: {
control: 'text',
},
showEdit: {
control: 'boolean',
},
showAdvanceGasFeeOptions: {
control: 'boolean',
},
footer: {
control: 'array',
},
noBorder: {
control: 'boolean',
},
supportsEIP1559V2: {
control: 'boolean',
},
renderTransactionDetailsContent: {
control: 'boolean',
},
renderDataContent: {
control: 'boolean',
},
isMultiLayerFeeNetwork: {
control: 'boolean',
},
ethTransactionTotal: {
control: 'text',
},
nativeCurrency: {
control: 'text',
},
fullTxData: {
control: 'object',
},
hexTransactionTotal: {
control: 'text',
},
fiatTransactionTotal: {
control: 'text',
},
currentCurrency: {
control: 'text',
},
isSetApproveForAll: {
control: 'boolean',
},
isApprovalOrRejection: {
control: 'boolean',
},
data: {
control: 'text',
},
onEditClick: {
control: 'onEditClick',
},
},
args: {
showHeader: true,
symbol: <i className="fa fa-tag" />,
title: 'Transaction fee',
showEdit: true,
showAdvanceGasFeeOptions: true,
noBorder: true,
supportsEIP1559V2: false,
renderTransactionDetailsContent: true,
renderDataContent: false,
isMultiLayerFeeNetwork: false,
ethTransactionTotal: '0.0012',
nativeCurrency: 'GoerliETH',
hexTransactionTotal: '0x44364c5bb0000',
fiatTransactionTotal: '1.54',
currentCurrency: 'usd',
isSetApproveForAll: false,
isApprovalOrRejection: false,
data: '',
fullTxData: {
id: 3049568294499567,
time: 1664449552289,
status: 'unapproved',
metamaskNetworkId: '3',
originalGasEstimate: '0xea60',
userEditedGasLimit: false,
chainId: '0x3',
loadingDefaults: false,
dappSuggestedGasFees: {
gasPrice: '0x4a817c800',
gas: '0xea60',
},
sendFlowHistory: [],
txParams: {
from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
value: '0x0',
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
gas: '0xea60',
maxFeePerGas: '0x4a817c800',
maxPriorityFeePerGas: '0x4a817c800',
},
origin: 'https://metamask.github.io',
type: 'approve',
history: [
{
id: 3049568294499567,
time: 1664449552289,
status: 'unapproved',
metamaskNetworkId: '3',
originalGasEstimate: '0xea60',
userEditedGasLimit: false,
chainId: '0x3',
loadingDefaults: true,
dappSuggestedGasFees: {
gasPrice: '0x4a817c800',
gas: '0xea60',
},
sendFlowHistory: [],
txParams: {
from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
value: '0x0',
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
gas: '0xea60',
gasPrice: '0x4a817c800',
},
origin: 'https://metamask.github.io',
type: 'approve',
},
[
{
op: 'remove',
path: '/txParams/gasPrice',
note: 'Added new unapproved transaction.',
timestamp: 1664449553939,
},
{
op: 'add',
path: '/txParams/maxFeePerGas',
value: '0x4a817c800',
},
{
op: 'add',
path: '/txParams/maxPriorityFeePerGas',
value: '0x4a817c800',
},
{
op: 'replace',
path: '/loadingDefaults',
value: false,
},
{
op: 'add',
path: '/userFeeLevel',
value: 'custom',
},
{
op: 'add',
path: '/defaultGasEstimates',
value: {
estimateType: 'custom',
gas: '0xea60',
maxFeePerGas: '0x4a817c800',
maxPriorityFeePerGas: '0x4a817c800',
},
},
],
],
userFeeLevel: 'custom',
defaultGasEstimates: {
estimateType: 'custom',
gas: '0xea60',
maxFeePerGas: '0x4a817c800',
maxPriorityFeePerGas: '0x4a817c800',
},
},
},
};
export const DefaultStory = (args) => {
return <ApproveContentCard {...args} />;
};
DefaultStory.storyName = 'Default';

@ -1,4 +1,5 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box';
@ -15,6 +16,8 @@ import {
JUSTIFY_CONTENT,
SIZES,
} from '../../../helpers/constants/design-system';
import { getCustomTokenAmount } from '../../../selectors';
import { setCustomTokenAmount } from '../../../ducks/app/app';
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
export default function CustomSpendingCap({
@ -22,12 +25,17 @@ export default function CustomSpendingCap({
currentTokenBalance,
dappProposedValue,
siteOrigin,
onEdit,
passTheErrorText,
}) {
const t = useContext(I18nContext);
const [value, setValue] = useState('');
const [customSpendingCapText, setCustomSpendingCapText] = useState('');
const dispatch = useDispatch();
const value = useSelector(getCustomTokenAmount);
const [error, setError] = useState('');
const [showUseDefaultButton, setShowUseDefaultButton] = useState(
value !== String(dappProposedValue) && true,
);
const inputLogicEmptyStateText = t('inputLogicEmptyState');
const getInputTextLogic = (inputNumber) => {
@ -57,6 +65,10 @@ export default function CustomSpendingCap({
};
};
const [customSpendingCapText, setCustomSpendingCapText] = useState(
getInputTextLogic(value).description,
);
const handleChange = (valueInput) => {
let spendingCapError = '';
const inputTextLogic = getInputTextLogic(valueInput);
@ -71,9 +83,19 @@ export default function CustomSpendingCap({
setError('');
}
setValue(valueInput);
dispatch(setCustomTokenAmount(String(valueInput)));
};
useEffect(() => {
if (value !== String(dappProposedValue)) {
setShowUseDefaultButton(true);
}
}, [value, dappProposedValue]);
useEffect(() => {
passTheErrorText(error);
}, [error, passTheErrorText]);
const chooseTooltipContentText =
value > currentTokenBalance
? t('warningTooltipText', [
@ -100,7 +122,6 @@ export default function CustomSpendingCap({
onClick={(e) => {
e.preventDefault();
handleChange(currentTokenBalance);
setValue(currentTokenBalance);
}}
>
{t('max')}
@ -131,6 +152,7 @@ export default function CustomSpendingCap({
}
>
<FormField
numeric
dataTestId="custom-spending-cap-input"
autoFocus
wrappingLabelProps={{ as: 'div' }}
@ -151,21 +173,19 @@ export default function CustomSpendingCap({
error={error}
value={value}
titleDetail={
<button
className="custom-spending-cap__input--button"
type="link"
onClick={(e) => {
e.preventDefault();
if (value <= currentTokenBalance || error) {
showUseDefaultButton && (
<button
className="custom-spending-cap__input--button"
type="link"
onClick={(e) => {
e.preventDefault();
setShowUseDefaultButton(false);
handleChange(dappProposedValue);
setValue(dappProposedValue);
} else {
onEdit();
}
}}
>
{value > currentTokenBalance ? t('edit') : t('useDefault')}
</button>
}}
>
{t('useDefault')}
</button>
)
}
titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }}
allowDecimals
@ -202,7 +222,7 @@ CustomSpendingCap.propTypes = {
*/
siteOrigin: PropTypes.string,
/**
* onClick handler for the Edit link
* Parent component's callback function passed in order to get the error text
*/
onEdit: PropTypes.func,
passTheErrorText: PropTypes.func,
};

@ -17,8 +17,8 @@ export default {
siteOrigin: {
control: { type: 'text' },
},
onEdit: {
action: 'onEdit',
passTheErrorText: {
action: 'passTheErrorText',
},
},
args: {

@ -21,6 +21,7 @@
position: absolute;
margin-top: 55px;
margin-inline-start: -75px;
z-index: 1;
}
}
@ -28,4 +29,10 @@
color: var(--color-error-default);
padding-inline-end: 60px;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
}

@ -58,6 +58,7 @@ export default function reduceApp(state = {}, action) {
newTokensImported: '',
newCustomNetworkAdded: {},
onboardedInThisUISession: false,
customTokenAmount: '',
...state,
};
@ -401,6 +402,11 @@ export default function reduceApp(state = {}, action) {
...appState,
onboardedInThisUISession: action.value,
};
case actionConstants.SET_CUSTOM_TOKEN_AMOUNT:
return {
...appState,
customTokenAmount: action.value,
};
default:
return appState;
}
@ -463,3 +469,7 @@ export function setNewCustomNetworkAdded(value) {
export function setOnBoardedInThisUISession(value) {
return { type: actionConstants.ONBOARDED_IN_THIS_UI_SESSION, value };
}
export function setCustomTokenAmount(value) {
return { type: actionConstants.SET_CUSTOM_TOKEN_AMOUNT, value };
}

@ -180,14 +180,14 @@ export default function ConfirmApprove({
supportsEIP1559V2={supportsEIP1559V2}
userAddress={userAddress}
tokenAddress={tokenAddress}
data={customData || transactionData}
data={transactionData}
isSetApproveForAll={isSetApproveForAll}
isApprovalOrRejection={isApprovalOrRejection}
customTxParamsData={customData}
dappProposedTokenAmount={tokenAmount}
currentTokenBalance={tokenBalance}
toAddress={toAddress}
tokenSymbol={tokenSymbol}
decimals={decimals}
/>
{showCustomizeGasPopover && !supportsEIP1559V2 && (
<EditGasPopover

@ -29,6 +29,7 @@ import {
transactionFeeSelector,
getKnownMethodData,
getRpcPrefsForCurrentProvider,
getCustomTokenAmount,
} from '../../selectors';
import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
import {
@ -39,6 +40,10 @@ import {
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';
import CustomSpendingCap from '../../components/app/custom-spending-cap/custom-spending-cap';
import Dialog from '../../components/ui/dialog';
import { useGasFeeContext } from '../../contexts/gasFee';
import { getCustomTxParamsData } from '../confirm-approve/confirm-approve.util';
export default function TokenAllowance({
origin,
@ -58,7 +63,7 @@ export default function TokenAllowance({
data,
isSetApproveForAll,
isApprovalOrRejection,
customTxParamsData,
decimals,
dappProposedTokenAmount,
currentTokenBalance,
toAddress,
@ -71,11 +76,22 @@ export default function TokenAllowance({
const [showContractDetails, setShowContractDetails] = useState(false);
const [showFullTxDetails, setShowFullTxDetails] = useState(false);
const [isFirstPage, setIsFirstPage] = useState(false);
const [isFirstPage, setIsFirstPage] = useState(true);
const [errorText, setErrorText] = useState('');
const currentAccount = useSelector(getCurrentAccountWithSendEtherInfo);
const networkIdentifier = useSelector(getNetworkIdentifier);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const customTokenAmount = useSelector(getCustomTokenAmount);
const customPermissionAmount = customTokenAmount.toString();
const customTxParamsData = customTokenAmount
? getCustomTxParamsData(data, {
customPermissionAmount,
decimals,
})
: null;
let fullTxData = { ...txData };
@ -92,6 +108,13 @@ export default function TokenAllowance({
const fee = useSelector((state) => transactionFeeSelector(state, fullTxData));
const methodData = useSelector((state) => getKnownMethodData(state, data));
const { balanceError } = useGasFeeContext();
const disableNextButton =
isFirstPage && (customTokenAmount === '' || errorText !== '');
const disableApproveButton = !isFirstPage && balanceError;
const networkName =
NETWORK_TO_NAME_MAP[fullTxData.chainId] || networkIdentifier;
@ -105,9 +128,10 @@ export default function TokenAllowance({
: transactionData;
const handleReject = () => {
dispatch(updateCustomNonce(''));
dispatch(cancelTx(fullTxData)).then(() => {
dispatch(clearConfirmTransaction());
dispatch(updateCustomNonce(''));
history.push(mostRecentOverviewPage);
});
};
@ -128,17 +152,35 @@ export default function TokenAllowance({
fullTxData.originalApprovalAmount = dappProposedTokenAmount;
}
if (customTokenAmount) {
fullTxData.customTokenAmount = customTokenAmount;
fullTxData.finalApprovalAmount = customTokenAmount;
} else if (dappProposedTokenAmount !== undefined) {
fullTxData.finalApprovalAmount = dappProposedTokenAmount;
}
if (currentTokenBalance) {
fullTxData.currentTokenBalance = currentTokenBalance;
}
dispatch(updateCustomNonce(''));
dispatch(updateAndApproveTx(customNonceMerge(fullTxData))).then(() => {
dispatch(clearConfirmTransaction());
dispatch(updateCustomNonce(''));
history.push(mostRecentOverviewPage);
});
};
const handleNextClick = () => {
setShowFullTxDetails(false);
setIsFirstPage(false);
};
const handleBackClick = () => {
setShowFullTxDetails(false);
setIsFirstPage(true);
};
return (
<Box className="token-allowance-container page-container">
<Box
@ -151,7 +193,7 @@ export default function TokenAllowance({
>
<Box>
{!isFirstPage && (
<Button type="inline" onClick={() => setIsFirstPage(true)}>
<Button type="inline" onClick={() => handleBackClick()}>
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.TEXT_MUTED}
@ -217,13 +259,17 @@ export default function TokenAllowance({
</Typography>
</Box>
</Box>
<Box marginBottom={5}>
<Box marginBottom={5} marginLeft={4} marginRight={4}>
<Typography
variant={TYPOGRAPHY.H3}
fontWeight={FONT_WEIGHT.BOLD}
align={TEXT_ALIGN.CENTER}
>
{isFirstPage ? t('setSpendingCap') : t('reviewSpendingCap')}
{isFirstPage && t('setSpendingCap')}
{!isFirstPage &&
(customTokenAmount === 0
? t('revokeSpendingCap')
: t('reviewSpendingCap'))}
</Typography>
</Box>
<Box>
@ -251,13 +297,28 @@ export default function TokenAllowance({
</Button>
</Box>
<Box margin={[4, 4, 3, 4]}>
<ReviewSpendingCap
tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)}
tokenValue={10}
onEdit={() => setIsFirstPage(true)}
/>
{isFirstPage ? (
<CustomSpendingCap
tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)}
dappProposedValue={parseFloat(dappProposedTokenAmount)}
siteOrigin={origin}
passTheErrorText={(value) => setErrorText(value)}
/>
) : (
<ReviewSpendingCap
tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)}
tokenValue={parseFloat(customTokenAmount)}
onEdit={() => handleBackClick()}
/>
)}
</Box>
{!isFirstPage && balanceError && (
<Dialog type="error" className="send__error-dialog">
{t('insufficientFundsForGas')}
</Dialog>
)}
{!isFirstPage && (
<Box className="token-allowance-container__card-wrapper">
<ApproveContentCard
@ -319,7 +380,7 @@ export default function TokenAllowance({
supportsEIP1559V2={supportsEIP1559V2}
isSetApproveForAll={isSetApproveForAll}
isApprovalOrRejection={isApprovalOrRejection}
data={data}
data={customTxParamsData || data}
/>
</Box>
</Box>
@ -328,7 +389,8 @@ export default function TokenAllowance({
cancelText={t('reject')}
submitText={isFirstPage ? t('next') : t('approveButtonText')}
onCancel={() => handleReject()}
onSubmit={() => (isFirstPage ? setIsFirstPage(false) : handleApprove())}
onSubmit={() => (isFirstPage ? handleNextClick() : handleApprove())}
disabled={disableNextButton || disableApproveButton}
/>
{showContractDetails && (
<ContractDetailsModal
@ -416,9 +478,9 @@ TokenAllowance.propTypes = {
*/
isApprovalOrRejection: PropTypes.bool,
/**
* Custom transaction parameters data made by the user (fees)
* Number of decimals
*/
customTxParamsData: PropTypes.object,
decimals: PropTypes.string,
/**
* Token amount proposed by the Dapp
*/

@ -53,8 +53,8 @@ export default {
setApproveForAllArg: {
control: 'boolean',
},
customTxParamsData: {
control: 'object',
decimals: {
control: 'text',
},
dappProposedTokenAmount: {
control: 'text',
@ -88,7 +88,7 @@ export default {
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
isSetApproveForAll: false,
setApproveForAllArg: false,
customTxParamsData: {},
decimals: '4',
dappProposedTokenAmount: '7',
currentTokenBalance: '10',
toAddress: '0x9bc5baf874d2da8d216ae9f137804184ee5afef4',

@ -1307,3 +1307,7 @@ export function getShouldShowSeedPhraseReminder(state) {
dismissSeedBackUpReminder === false
);
}
export function getCustomTokenAmount(state) {
return state.appState.customTokenAmount;
}

@ -120,3 +120,6 @@ export const TOGGLE_CURRENCY_INPUT_SWITCH = 'TOGGLE_CURRENCY_INPUT_SWITCH';
// Token detection v2
export const SET_NEW_TOKENS_IMPORTED = 'SET_NEW_TOKENS_IMPORTED';
// Token allowance
export const SET_CUSTOM_TOKEN_AMOUNT = 'SET_CUSTOM_TOKEN_AMOUNT';

Loading…
Cancel
Save