1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +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 BigNumber from 'bignumber.js';
/**
* 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 =
'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 { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import BigNumber from 'bignumber.js';
import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box';
import FormField from '../../ui/form-field';
@ -20,6 +21,15 @@ import {
} from '../../../helpers/constants/design-system';
import { getCustomTokenAmount } from '../../../selectors';
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';
export default function CustomSpendingCap({
@ -28,6 +38,7 @@ export default function CustomSpendingCap({
dappProposedValue,
siteOrigin,
passTheErrorText,
decimals,
}) {
const t = useContext(I18nContext);
const dispatch = useDispatch();
@ -40,8 +51,27 @@ export default function CustomSpendingCap({
);
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) => {
if (inputNumber <= currentTokenBalance) {
if (
conversionLTE(
{
value: Number(replaceCommaToDot(inputNumber)),
fromNumericBase: 'dec',
},
{ value: Number(currentTokenBalance), fromNumericBase: 'dec' },
)
) {
return {
className: 'custom-spending-cap__lowerValue',
description: t('inputLogicEqualOrSmallerNumber', [
@ -51,11 +81,11 @@ export default function CustomSpendingCap({
fontWeight={FONT_WEIGHT.BOLD}
className="custom-spending-cap__input-value-and-token-name"
>
{inputNumber} {tokenName}
{replaceCommaToDot(inputNumber)} {tokenName}
</Typography>,
]),
};
} else if (inputNumber > currentTokenBalance) {
} else if (decConversionGreaterThan(inputNumber, currentTokenBalance)) {
return {
className: 'custom-spending-cap__higherValue',
description: t('inputLogicHigherNumber'),
@ -76,7 +106,7 @@ export default function CustomSpendingCap({
const inputTextLogic = getInputTextLogic(valueInput);
const inputTextLogicDescription = inputTextLogic.description;
if (valueInput < 0 || isNaN(valueInput)) {
if (valueInput && !TOKEN_ALLOWANCE_VALUE_REGEX.test(valueInput)) {
spendingCapError = t('spendingCapError');
setCustomSpendingCapText(t('spendingCapErrorDescription', [siteOrigin]));
setError(spendingCapError);
@ -85,6 +115,18 @@ export default function CustomSpendingCap({
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)));
};
@ -98,19 +140,21 @@ export default function CustomSpendingCap({
passTheErrorText(error);
}, [error, passTheErrorText]);
const chooseTooltipContentText =
value > currentTokenBalance
? t('warningTooltipText', [
<Typography
key="tooltip-text"
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.BOLD}
color={COLORS.ERROR_DEFAULT}
>
<i className="fa fa-exclamation-circle" /> {t('beCareful')}
</Typography>,
])
: t('inputLogicEmptyState');
const chooseTooltipContentText = decConversionGreaterThan(
value,
currentTokenBalance,
)
? t('warningTooltipText', [
<Typography
key="tooltip-text"
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.BOLD}
color={COLORS.ERROR_DEFAULT}
>
<i className="fa fa-exclamation-circle" /> {t('beCareful')}
</Typography>,
])
: t('inputLogicEmptyState');
return (
<>
@ -133,25 +177,30 @@ export default function CustomSpendingCap({
>
<label
htmlFor={
value > (currentTokenBalance || error)
decConversionGreaterThan(value, currentTokenBalance)
? 'custom-spending-cap-input-value'
: 'custom-spending-cap'
}
>
<FormField
numeric
dataTestId="custom-spending-cap-input"
autoFocus
wrappingLabelProps={{ as: 'div' }}
id={
value > (currentTokenBalance || error)
decConversionGreaterThan(value, currentTokenBalance)
? 'custom-spending-cap-input-value'
: 'custom-spending-cap'
}
TooltipCustomComponent={
<CustomSpendingCapTooltip
tooltipContentText={value ? chooseTooltipContentText : ''}
tooltipIcon={value ? value > currentTokenBalance : ''}
tooltipContentText={
replaceCommaToDot(value) ? chooseTooltipContentText : ''
}
tooltipIcon={
replaceCommaToDot(value)
? decConversionGreaterThan(value, currentTokenBalance)
: ''
}
/>
}
onChange={handleChange}
@ -174,7 +223,6 @@ export default function CustomSpendingCap({
)
}
titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }}
allowDecimals
/>
<Box
width={BLOCK_SIZES.MAX}
@ -195,11 +243,14 @@ export default function CustomSpendingCap({
</ButtonLink>
</Box>
<Typography
className="custom-spending-cap__description"
color={COLORS.TEXT_DEFAULT}
variant={TYPOGRAPHY.H7}
boxProps={{ paddingTop: 2, paddingBottom: 2 }}
>
{value ? customSpendingCapText : inputLogicEmptyStateText}
{replaceCommaToDot(value)
? customSpendingCapText
: inputLogicEmptyStateText}
</Typography>
</label>
</Box>
@ -216,11 +267,11 @@ CustomSpendingCap.propTypes = {
/**
* The current token balance of the token
*/
currentTokenBalance: PropTypes.number,
currentTokenBalance: PropTypes.string,
/**
* The dapp suggested amount
*/
dappProposedValue: PropTypes.number,
dappProposedValue: PropTypes.string,
/**
* 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
*/
passTheErrorText: PropTypes.func,
/**
* Number of decimals
*/
decimals: PropTypes.string,
};

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ export default {
control: { type: 'number' },
},
tokenValue: {
control: { type: 'number' },
control: { type: 'text' },
},
onEdit: {
action: 'onEdit',
@ -21,7 +21,7 @@ export default {
args: {
tokenName: 'DAI',
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 { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import BigNumber from 'bignumber.js';
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';
@ -50,6 +51,8 @@ import { useGasFeeContext } from '../../contexts/gasFee';
import { getCustomTxParamsData } from '../confirm-approve/confirm-approve.util';
import { setCustomTokenAmount } from '../../ducks/app/app';
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({
origin,
@ -94,9 +97,21 @@ export default function TokenAllowance({
const unapprovedTxCount = useSelector(getUnapprovedTxCount);
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, {
customPermissionAmount,
decimals,
@ -339,19 +354,20 @@ export default function TokenAllowance({
{isFirstPage ? (
<CustomSpendingCap
tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)}
dappProposedValue={parseFloat(dappProposedTokenAmount)}
currentTokenBalance={currentTokenBalance}
dappProposedValue={dappProposedTokenAmount}
siteOrigin={origin}
passTheErrorText={(value) => setErrorText(value)}
decimals={decimals}
/>
) : (
<ReviewSpendingCap
tokenName={tokenSymbol}
currentTokenBalance={parseFloat(currentTokenBalance)}
currentTokenBalance={currentTokenBalance}
tokenValue={
isNaN(parseFloat(customTokenAmount))
? parseFloat(dappProposedTokenAmount)
: parseFloat(customTokenAmount)
? dappProposedTokenAmount
: replaceCommaToDot(customTokenAmount)
}
onEdit={() => handleBackClick()}
/>