From f5c95860f7f75b22f999a28a1db3e3af957af103 Mon Sep 17 00:00:00 2001 From: Adnan Sahovic <63151811+adnansahovic@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:58:16 +0100 Subject: [PATCH] Display large and small numbers as decimals instead of scientific notation on token allowance confirmation screens (#16676) Co-authored-by: VSaric Co-authored-by: Vladimir Saric <92527393+VSaric@users.noreply.github.com> --- shared/constants/tokens.js | 6 + .../custom-spending-cap.js | 107 +++++++++++++----- .../custom-spending-cap.stories.js | 8 +- .../app/custom-spending-cap/index.scss | 8 ++ .../ui/review-spending-cap/index.scss | 4 + .../review-spending-cap.js | 22 ++-- .../review-spending-cap.stories.js | 4 +- ui/pages/token-allowance/token-allowance.js | 30 +++-- 8 files changed, 144 insertions(+), 45 deletions(-) diff --git a/shared/constants/tokens.js b/shared/constants/tokens.js index ef9e7f76a..658b19417 100644 --- a/shared/constants/tokens.js +++ b/shared/constants/tokens.js @@ -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; diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.js b/ui/components/app/custom-spending-cap/custom-spending-cap.js index ef2d13ae3..8e976482c 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.js @@ -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} , ]), }; - } 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', [ - - {t('beCareful')} - , - ]) - : t('inputLogicEmptyState'); + const chooseTooltipContentText = decConversionGreaterThan( + value, + currentTokenBalance, + ) + ? t('warningTooltipText', [ + + {t('beCareful')} + , + ]) + : t('inputLogicEmptyState'); return ( <> @@ -133,25 +177,30 @@ export default function CustomSpendingCap({ > @@ -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, }; diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js index 5c30a32e7..ba37c6274 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js @@ -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', }, }; diff --git a/ui/components/app/custom-spending-cap/index.scss b/ui/components/app/custom-spending-cap/index.scss index 3864e3618..2377aec1f 100644 --- a/ui/components/app/custom-spending-cap/index.scss +++ b/ui/components/app/custom-spending-cap/index.scss @@ -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; diff --git a/ui/components/ui/review-spending-cap/index.scss b/ui/components/ui/review-spending-cap/index.scss index c9000d842..aa84a9f8b 100644 --- a/ui/components/ui/review-spending-cap/index.scss +++ b/ui/components/ui/review-spending-cap/index.scss @@ -25,4 +25,8 @@ i { font-size: $font-size-h7; } + + &__value { + word-break: break-word; + } } diff --git a/ui/components/ui/review-spending-cap/review-spending-cap.js b/ui/components/ui/review-spending-cap/review-spending-cap.js index bce174345..c4948af2e 100644 --- a/ui/components/ui/review-spending-cap/review-spending-cap.js +++ b/ui/components/ui/review-spending-cap/review-spending-cap.js @@ -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 ( - {tokenValue > currentTokenBalance && + {valueIsGreaterThanBalance && t('warningTooltipText', [ , ])} - {tokenValue === 0 && t('revokeSpendingCapTooltipText')} + {Number(tokenValue) === 0 && + t('revokeSpendingCapTooltipText')} } > - {tokenValue > currentTokenBalance && ( + {valueIsGreaterThanBalance && ( )} - {tokenValue === 0 && ( + {Number(tokenValue) === 0 && ( )} @@ -105,11 +111,11 @@ export default function ReviewSpendingCap({ - + 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, }; diff --git a/ui/components/ui/review-spending-cap/review-spending-cap.stories.js b/ui/components/ui/review-spending-cap/review-spending-cap.stories.js index 83832a966..15bd1346d 100644 --- a/ui/components/ui/review-spending-cap/review-spending-cap.stories.js +++ b/ui/components/ui/review-spending-cap/review-spending-cap.stories.js @@ -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', }, }; diff --git a/ui/pages/token-allowance/token-allowance.js b/ui/pages/token-allowance/token-allowance.js index 90103116e..7c69aabdc 100644 --- a/ui/pages/token-allowance/token-allowance.js +++ b/ui/pages/token-allowance/token-allowance.js @@ -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 ? ( setErrorText(value)} + decimals={decimals} /> ) : ( handleBackClick()} />