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

Display large and small numbers as decimals instead of scientific notation on token allowance confirmation screens (#16676)

Co-authored-by: VSaric <vladimir.saric@consensys.net>
Co-authored-by: Vladimir Saric <92527393+VSaric@users.noreply.github.com>
This commit is contained in:
Adnan Sahovic 2023-01-05 15:58:16 +01:00 committed by GitHub
parent f586f142be
commit fbbc1df853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 45 deletions

View File

@ -1,4 +1,5 @@
import contractMap from '@metamask/contract-metadata'; import contractMap from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js';
/** /**
* A normalized list of addresses exported as part of the contractMap in * A normalized list of addresses exported as part of the contractMap in
@ -39,3 +40,8 @@ export const STATIC_MAINNET_TOKEN_LIST = Object.keys(contractMap).reduce(
export const TOKEN_API_METASWAP_CODEFI_URL = export const TOKEN_API_METASWAP_CODEFI_URL =
'https://token-api.metaswap.codefi.network/tokens/'; 'https://token-api.metaswap.codefi.network/tokens/';
export const MAX_TOKEN_ALLOWANCE_AMOUNT = new BigNumber(2)
.pow(256)
.minus(1)
.toString(10);
export const TOKEN_ALLOWANCE_VALUE_REGEX = /^[0-9]{1,}([,.][0-9]{1,})?$/u;

View File

@ -1,6 +1,7 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import BigNumber from 'bignumber.js';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box'; import Box from '../../ui/box';
import FormField from '../../ui/form-field'; import FormField from '../../ui/form-field';
@ -20,6 +21,15 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { getCustomTokenAmount } from '../../../selectors'; import { getCustomTokenAmount } from '../../../selectors';
import { setCustomTokenAmount } from '../../../ducks/app/app'; import { setCustomTokenAmount } from '../../../ducks/app/app';
import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils';
import {
conversionGreaterThan,
conversionLTE,
} from '../../../../shared/modules/conversion.utils';
import {
MAX_TOKEN_ALLOWANCE_AMOUNT,
TOKEN_ALLOWANCE_VALUE_REGEX,
} from '../../../../shared/constants/tokens';
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip'; import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
export default function CustomSpendingCap({ export default function CustomSpendingCap({
@ -28,6 +38,7 @@ export default function CustomSpendingCap({
dappProposedValue, dappProposedValue,
siteOrigin, siteOrigin,
passTheErrorText, passTheErrorText,
decimals,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -40,8 +51,27 @@ export default function CustomSpendingCap({
); );
const inputLogicEmptyStateText = t('inputLogicEmptyState'); const inputLogicEmptyStateText = t('inputLogicEmptyState');
const replaceCommaToDot = (inputValue) => {
return inputValue.replace(/,/gu, '.');
};
const decConversionGreaterThan = (tokenValue, tokenBalance) => {
return conversionGreaterThan(
{ value: Number(replaceCommaToDot(tokenValue)), fromNumericBase: 'dec' },
{ value: Number(tokenBalance), fromNumericBase: 'dec' },
);
};
const getInputTextLogic = (inputNumber) => { const getInputTextLogic = (inputNumber) => {
if (inputNumber <= currentTokenBalance) { if (
conversionLTE(
{
value: Number(replaceCommaToDot(inputNumber)),
fromNumericBase: 'dec',
},
{ value: Number(currentTokenBalance), fromNumericBase: 'dec' },
)
) {
return { return {
className: 'custom-spending-cap__lowerValue', className: 'custom-spending-cap__lowerValue',
description: t('inputLogicEqualOrSmallerNumber', [ description: t('inputLogicEqualOrSmallerNumber', [
@ -51,11 +81,11 @@ export default function CustomSpendingCap({
fontWeight={FONT_WEIGHT.BOLD} fontWeight={FONT_WEIGHT.BOLD}
className="custom-spending-cap__input-value-and-token-name" className="custom-spending-cap__input-value-and-token-name"
> >
{inputNumber} {tokenName} {replaceCommaToDot(inputNumber)} {tokenName}
</Typography>, </Typography>,
]), ]),
}; };
} else if (inputNumber > currentTokenBalance) { } else if (decConversionGreaterThan(inputNumber, currentTokenBalance)) {
return { return {
className: 'custom-spending-cap__higherValue', className: 'custom-spending-cap__higherValue',
description: t('inputLogicHigherNumber'), description: t('inputLogicHigherNumber'),
@ -76,7 +106,7 @@ export default function CustomSpendingCap({
const inputTextLogic = getInputTextLogic(valueInput); const inputTextLogic = getInputTextLogic(valueInput);
const inputTextLogicDescription = inputTextLogic.description; const inputTextLogicDescription = inputTextLogic.description;
if (valueInput < 0 || isNaN(valueInput)) { if (valueInput && !TOKEN_ALLOWANCE_VALUE_REGEX.test(valueInput)) {
spendingCapError = t('spendingCapError'); spendingCapError = t('spendingCapError');
setCustomSpendingCapText(t('spendingCapErrorDescription', [siteOrigin])); setCustomSpendingCapText(t('spendingCapErrorDescription', [siteOrigin]));
setError(spendingCapError); setError(spendingCapError);
@ -85,6 +115,18 @@ export default function CustomSpendingCap({
setError(''); setError('');
} }
const maxTokenAmount = calcTokenAmount(
MAX_TOKEN_ALLOWANCE_AMOUNT,
decimals,
);
if (Number(valueInput.length) > 1 && Number(valueInput)) {
const customSpendLimitNumber = new BigNumber(valueInput);
if (customSpendLimitNumber.greaterThan(maxTokenAmount)) {
spendingCapError = t('spendLimitTooLarge');
setError(spendingCapError);
}
}
dispatch(setCustomTokenAmount(String(valueInput))); dispatch(setCustomTokenAmount(String(valueInput)));
}; };
@ -98,19 +140,21 @@ export default function CustomSpendingCap({
passTheErrorText(error); passTheErrorText(error);
}, [error, passTheErrorText]); }, [error, passTheErrorText]);
const chooseTooltipContentText = const chooseTooltipContentText = decConversionGreaterThan(
value > currentTokenBalance value,
? t('warningTooltipText', [ currentTokenBalance,
<Typography )
key="tooltip-text" ? t('warningTooltipText', [
variant={TYPOGRAPHY.H7} <Typography
fontWeight={FONT_WEIGHT.BOLD} key="tooltip-text"
color={COLORS.ERROR_DEFAULT} variant={TYPOGRAPHY.H7}
> fontWeight={FONT_WEIGHT.BOLD}
<i className="fa fa-exclamation-circle" /> {t('beCareful')} color={COLORS.ERROR_DEFAULT}
</Typography>, >
]) <i className="fa fa-exclamation-circle" /> {t('beCareful')}
: t('inputLogicEmptyState'); </Typography>,
])
: t('inputLogicEmptyState');
return ( return (
<> <>
@ -133,25 +177,30 @@ export default function CustomSpendingCap({
> >
<label <label
htmlFor={ htmlFor={
value > (currentTokenBalance || error) decConversionGreaterThan(value, currentTokenBalance)
? 'custom-spending-cap-input-value' ? 'custom-spending-cap-input-value'
: 'custom-spending-cap' : 'custom-spending-cap'
} }
> >
<FormField <FormField
numeric
dataTestId="custom-spending-cap-input" dataTestId="custom-spending-cap-input"
autoFocus autoFocus
wrappingLabelProps={{ as: 'div' }} wrappingLabelProps={{ as: 'div' }}
id={ id={
value > (currentTokenBalance || error) decConversionGreaterThan(value, currentTokenBalance)
? 'custom-spending-cap-input-value' ? 'custom-spending-cap-input-value'
: 'custom-spending-cap' : 'custom-spending-cap'
} }
TooltipCustomComponent={ TooltipCustomComponent={
<CustomSpendingCapTooltip <CustomSpendingCapTooltip
tooltipContentText={value ? chooseTooltipContentText : ''} tooltipContentText={
tooltipIcon={value ? value > currentTokenBalance : ''} replaceCommaToDot(value) ? chooseTooltipContentText : ''
}
tooltipIcon={
replaceCommaToDot(value)
? decConversionGreaterThan(value, currentTokenBalance)
: ''
}
/> />
} }
onChange={handleChange} onChange={handleChange}
@ -174,7 +223,6 @@ export default function CustomSpendingCap({
) )
} }
titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }} titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }}
allowDecimals
/> />
<Box <Box
width={BLOCK_SIZES.MAX} width={BLOCK_SIZES.MAX}
@ -195,11 +243,14 @@ export default function CustomSpendingCap({
</ButtonLink> </ButtonLink>
</Box> </Box>
<Typography <Typography
className="custom-spending-cap__description"
color={COLORS.TEXT_DEFAULT} color={COLORS.TEXT_DEFAULT}
variant={TYPOGRAPHY.H7} variant={TYPOGRAPHY.H7}
boxProps={{ paddingTop: 2, paddingBottom: 2 }} boxProps={{ paddingTop: 2, paddingBottom: 2 }}
> >
{value ? customSpendingCapText : inputLogicEmptyStateText} {replaceCommaToDot(value)
? customSpendingCapText
: inputLogicEmptyStateText}
</Typography> </Typography>
</label> </label>
</Box> </Box>
@ -216,11 +267,11 @@ CustomSpendingCap.propTypes = {
/** /**
* The current token balance of the token * The current token balance of the token
*/ */
currentTokenBalance: PropTypes.number, currentTokenBalance: PropTypes.string,
/** /**
* The dapp suggested amount * The dapp suggested amount
*/ */
dappProposedValue: PropTypes.number, dappProposedValue: PropTypes.string,
/** /**
* The origin of the site generally the URL * The origin of the site generally the URL
*/ */
@ -229,4 +280,8 @@ CustomSpendingCap.propTypes = {
* Parent component's callback function passed in order to get the error text * Parent component's callback function passed in order to get the error text
*/ */
passTheErrorText: PropTypes.func, passTheErrorText: PropTypes.func,
/**
* Number of decimals
*/
decimals: PropTypes.string,
}; };

View File

@ -12,7 +12,7 @@ export default {
control: { type: 'number' }, control: { type: 'number' },
}, },
dappProposedValue: { dappProposedValue: {
control: { type: 'number' }, control: { type: 'text' },
}, },
siteOrigin: { siteOrigin: {
control: { type: 'text' }, control: { type: 'text' },
@ -20,12 +20,16 @@ export default {
passTheErrorText: { passTheErrorText: {
action: 'passTheErrorText', action: 'passTheErrorText',
}, },
decimals: {
control: 'text',
},
}, },
args: { args: {
tokenName: 'DAI', tokenName: 'DAI',
currentTokenBalance: 200.12, currentTokenBalance: 200.12,
dappProposedValue: 7, dappProposedValue: '7',
siteOrigin: 'Uniswap.org', siteOrigin: 'Uniswap.org',
decimals: '4',
}, },
}; };

View File

@ -12,11 +12,19 @@
width: 100%; width: 100%;
} }
&__description {
word-break: break-word;
}
#custom-spending-cap-input-value { #custom-spending-cap-input-value {
color: var(--color-error-default); color: var(--color-error-default);
padding-inline-end: 60px; padding-inline-end: 60px;
} }
#custom-spending-cap {
padding-inline-end: 60px;
}
input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-inner-spin-button,
input[type='number']:hover::-webkit-inner-spin-button { input[type='number']:hover::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;

View File

@ -25,4 +25,8 @@
i { i {
font-size: $font-size-h7; font-size: $font-size-h7;
} }
&__value {
word-break: break-word;
}
} }

View File

@ -15,6 +15,7 @@ import {
TEXT_ALIGN, TEXT_ALIGN,
SIZES, SIZES,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { conversionGreaterThan } from '../../../../shared/modules/conversion.utils';
export default function ReviewSpendingCap({ export default function ReviewSpendingCap({
tokenName, tokenName,
@ -23,6 +24,10 @@ export default function ReviewSpendingCap({
onEdit, onEdit,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const valueIsGreaterThanBalance = conversionGreaterThan(
{ value: Number(tokenValue), fromNumericBase: 'dec' },
{ value: Number(currentTokenBalance), fromNumericBase: 'dec' },
);
return ( return (
<Box <Box
@ -65,7 +70,7 @@ export default function ReviewSpendingCap({
color={COLORS.TEXT_ALTERNATIVE} color={COLORS.TEXT_ALTERNATIVE}
className="review-spending-cap__heading-title__tooltip" className="review-spending-cap__heading-title__tooltip"
> >
{tokenValue > currentTokenBalance && {valueIsGreaterThanBalance &&
t('warningTooltipText', [ t('warningTooltipText', [
<Typography <Typography
key="tooltip-text" key="tooltip-text"
@ -77,14 +82,15 @@ export default function ReviewSpendingCap({
{t('beCareful')} {t('beCareful')}
</Typography>, </Typography>,
])} ])}
{tokenValue === 0 && t('revokeSpendingCapTooltipText')} {Number(tokenValue) === 0 &&
t('revokeSpendingCapTooltipText')}
</Typography> </Typography>
} }
> >
{tokenValue > currentTokenBalance && ( {valueIsGreaterThanBalance && (
<i className="fa fa-exclamation-triangle review-spending-cap__heading-title__tooltip__warning-icon" /> <i className="fa fa-exclamation-triangle review-spending-cap__heading-title__tooltip__warning-icon" />
)} )}
{tokenValue === 0 && ( {Number(tokenValue) === 0 && (
<i className="far fa-question-circle review-spending-cap__heading-title__tooltip__question-icon" /> <i className="far fa-question-circle review-spending-cap__heading-title__tooltip__question-icon" />
)} )}
</Tooltip> </Tooltip>
@ -105,11 +111,11 @@ export default function ReviewSpendingCap({
</ButtonLink> </ButtonLink>
</Box> </Box>
</Box> </Box>
<Box> <Box className="review-spending-cap__value">
<Typography <Typography
as={TYPOGRAPHY.H6} as={TYPOGRAPHY.H6}
color={ color={
tokenValue > currentTokenBalance valueIsGreaterThanBalance
? COLORS.ERROR_DEFAULT ? COLORS.ERROR_DEFAULT
: COLORS.TEXT_DEFAULT : COLORS.TEXT_DEFAULT
} }
@ -125,7 +131,7 @@ export default function ReviewSpendingCap({
ReviewSpendingCap.propTypes = { ReviewSpendingCap.propTypes = {
tokenName: PropTypes.string, tokenName: PropTypes.string,
currentTokenBalance: PropTypes.number, currentTokenBalance: PropTypes.string,
tokenValue: PropTypes.number, tokenValue: PropTypes.string,
onEdit: PropTypes.func, onEdit: PropTypes.func,
}; };

View File

@ -12,7 +12,7 @@ export default {
control: { type: 'number' }, control: { type: 'number' },
}, },
tokenValue: { tokenValue: {
control: { type: 'number' }, control: { type: 'text' },
}, },
onEdit: { onEdit: {
action: 'onEdit', action: 'onEdit',
@ -21,7 +21,7 @@ export default {
args: { args: {
tokenName: 'DAI', tokenName: 'DAI',
currentTokenBalance: 200.12, currentTokenBalance: 200.12,
tokenValue: 7, tokenValue: '7',
}, },
}; };

View File

@ -2,6 +2,7 @@ import React, { useState, useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import BigNumber from 'bignumber.js';
import Box from '../../components/ui/box/box'; import Box from '../../components/ui/box/box';
import NetworkAccountBalanceHeader from '../../components/app/network-account-balance-header/network-account-balance-header'; import NetworkAccountBalanceHeader from '../../components/app/network-account-balance-header/network-account-balance-header';
import UrlIcon from '../../components/ui/url-icon/url-icon'; import UrlIcon from '../../components/ui/url-icon/url-icon';
@ -50,6 +51,8 @@ import { useGasFeeContext } from '../../contexts/gasFee';
import { getCustomTxParamsData } from '../confirm-approve/confirm-approve.util'; import { getCustomTxParamsData } from '../confirm-approve/confirm-approve.util';
import { setCustomTokenAmount } from '../../ducks/app/app'; import { setCustomTokenAmount } from '../../ducks/app/app';
import { valuesFor } from '../../helpers/utils/util'; import { valuesFor } from '../../helpers/utils/util';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
import { MAX_TOKEN_ALLOWANCE_AMOUNT } from '../../../shared/constants/tokens';
export default function TokenAllowance({ export default function TokenAllowance({
origin, origin,
@ -94,9 +97,21 @@ export default function TokenAllowance({
const unapprovedTxCount = useSelector(getUnapprovedTxCount); const unapprovedTxCount = useSelector(getUnapprovedTxCount);
const unapprovedTxs = useSelector(getUnapprovedTransactions); const unapprovedTxs = useSelector(getUnapprovedTransactions);
const customPermissionAmount = customTokenAmount.toString(); const replaceCommaToDot = (inputValue) => {
return inputValue.replace(/,/gu, '.');
};
const customTxParamsData = customTokenAmount let customPermissionAmount = replaceCommaToDot(customTokenAmount).toString();
const maxTokenAmount = calcTokenAmount(MAX_TOKEN_ALLOWANCE_AMOUNT, decimals);
if (customTokenAmount.length > 1 && Number(customTokenAmount)) {
const customSpendLimitNumber = new BigNumber(customTokenAmount);
if (customSpendLimitNumber.greaterThan(maxTokenAmount)) {
customPermissionAmount = 0;
}
}
const customTxParamsData = customPermissionAmount
? getCustomTxParamsData(data, { ? getCustomTxParamsData(data, {
customPermissionAmount, customPermissionAmount,
decimals, decimals,
@ -339,19 +354,20 @@ export default function TokenAllowance({
{isFirstPage ? ( {isFirstPage ? (
<CustomSpendingCap <CustomSpendingCap
tokenName={tokenSymbol} tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)} currentTokenBalance={currentTokenBalance}
dappProposedValue={parseFloat(dappProposedTokenAmount)} dappProposedValue={dappProposedTokenAmount}
siteOrigin={origin} siteOrigin={origin}
passTheErrorText={(value) => setErrorText(value)} passTheErrorText={(value) => setErrorText(value)}
decimals={decimals}
/> />
) : ( ) : (
<ReviewSpendingCap <ReviewSpendingCap
tokenName={tokenSymbol} tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)} currentTokenBalance={currentTokenBalance}
tokenValue={ tokenValue={
isNaN(parseFloat(customTokenAmount)) isNaN(parseFloat(customTokenAmount))
? parseFloat(dappProposedTokenAmount) ? dappProposedTokenAmount
: parseFloat(customTokenAmount) : replaceCommaToDot(customTokenAmount)
} }
onEdit={() => handleBackClick()} onEdit={() => handleBackClick()}
/> />