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:
parent
f586f142be
commit
fbbc1df853
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -25,4 +25,8 @@
|
||||
i {
|
||||
font-size: $font-size-h7;
|
||||
}
|
||||
|
||||
&__value {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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()}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user