From 931a19105330f108d8b4605235b42b4f4b4f275c Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 18 Sep 2023 15:27:21 -0500 Subject: [PATCH] UX: Display total fiat balance on home screen --- shared/modules/conversion.utils.ts | 12 ++- ui/components/app/asset-list/asset-list.js | 79 ++++++++++++++++++- ui/components/app/token-list/token-list.js | 29 ++----- .../balance-overview/balance-overview.js | 72 +++++------------ 4 files changed, 115 insertions(+), 77 deletions(-) diff --git a/shared/modules/conversion.utils.ts b/shared/modules/conversion.utils.ts index e2466bcabb1a..13e5e2c7b34e 100644 --- a/shared/modules/conversion.utils.ts +++ b/shared/modules/conversion.utils.ts @@ -125,7 +125,7 @@ export function getValueFromWeiHex({ fromCurrency = EtherDenomination.ETH, toCurrency, conversionRate, - numberOfDecimals, + numberOfDecimals, // 18 toDenomination = EtherDenomination.ETH, }: { value: NumericValue; @@ -156,6 +156,16 @@ export function sumHexes(first: string, ...args: string[]) { return total.toPrefixedHexString(); } +export function sumDecimals(first: string, ...args: string[]) { + const firstValue = new Numeric(first, 10); + const total = args.reduce( + (acc, hexAmount) => acc.add(new Numeric(hexAmount, 10)), + firstValue, + ); + + return total; +} + export function hexWEIToDecGWEI(value: number | string | BigNumber | BN) { return new Numeric(value, 16, EtherDenomination.WEI) .toBase(10) diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 0cb8e636ced3..7b43b4cdf6f7 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -1,6 +1,7 @@ import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import { isEqual } from 'lodash'; import TokenList from '../token-list'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'; @@ -10,8 +11,15 @@ import { getNativeCurrencyImage, getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, + getShouldHideZeroBalanceTokens, + getTokenExchangeRates, + getCurrentCurrency, } from '../../../selectors'; -import { getNativeCurrency } from '../../../ducks/metamask/metamask'; +import { + getConversionRate, + getNativeCurrency, + getTokens, +} from '../../../ducks/metamask/metamask'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import Box from '../../ui/box/box'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -26,6 +34,15 @@ import { ImportTokenLink, BalanceOverview, } from '../../multichain'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; +import { getTokenFiatAmount } from '../../../helpers/utils/token-util'; +import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'; +import { + getValueFromWeiHex, + sumDecimals, +} from '../../../../shared/modules/conversion.utils'; +import { useTokenTracker } from '../../../hooks/useTokenTracker'; +import { EtherDenomination } from '../../../../shared/constants/common'; const AssetList = ({ onClickAsset }) => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); @@ -66,9 +83,63 @@ const AssetList = ({ onClickAsset }) => { getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, ); + const contractExchangeRates = useSelector( + getTokenExchangeRates, + shallowEqual, + ); + const conversionRate = useSelector(getConversionRate); + const currentCurrency = useSelector(getCurrentCurrency); + + const nativeFiat = getValueFromWeiHex({ + value: balance, + toCurrency: currentCurrency, + conversionRate, + numberOfDecimals: 2, + }); + + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + // use `isEqual` comparison function because the token array is serialized + // from the background so it has a new reference with each background update, + // even if the tokens haven't changed + const tokens = useSelector(getTokens, isEqual); + const { loading, tokensWithBalances } = useTokenTracker( + tokens, + true, + shouldHideZeroBalanceTokens, + ); + + const dollarBalances = tokensWithBalances.map((token) => { + const contractExchangeTokenKey = Object.keys(contractExchangeRates).find( + (key) => isEqualCaseInsensitive(key, token.address), + ); + const tokenExchangeRate = + (contractExchangeTokenKey && + contractExchangeRates[contractExchangeTokenKey]) ?? + 0; + + const fiat = getTokenFiatAmount( + tokenExchangeRate, + conversionRate, + currentCurrency, + token.string, + token.symbol, + false, + false, + ); + + return fiat; + }); + + const totalFiat = formatCurrency( + sumDecimals(nativeFiat, ...dollarBalances).toString(10), + currentCurrency, + ); + return ( <> - {process.env.MULTICHAIN ? : null} + {process.env.MULTICHAIN ? : null} onClickAsset(nativeCurrency)} title={nativeCurrency} @@ -80,6 +151,8 @@ const AssetList = ({ onClickAsset }) => { tokenImage={balanceIsLoading ? null : primaryTokenImage} /> { onClickAsset(tokenAddress); trackEvent({ diff --git a/ui/components/app/token-list/token-list.js b/ui/components/app/token-list/token-list.js index 44d801cef4b3..c584eb4e184b 100644 --- a/ui/components/app/token-list/token-list.js +++ b/ui/components/app/token-list/token-list.js @@ -1,13 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { isEqual } from 'lodash'; - -import { useSelector } from 'react-redux'; import TokenCell from '../token-cell'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { getShouldHideZeroBalanceTokens } from '../../../selectors'; -import { getTokens } from '../../../ducks/metamask/metamask'; import { Box } from '../../component-library'; import { AlignItems, @@ -15,20 +9,9 @@ import { JustifyContent, } from '../../../helpers/constants/design-system'; -export default function TokenList({ onTokenClick }) { +export default function TokenList({ onTokenClick, tokens, loading = false }) { const t = useI18nContext(); - const shouldHideZeroBalanceTokens = useSelector( - getShouldHideZeroBalanceTokens, - ); - // use `isEqual` comparison function because the token array is serialized - // from the background so it has a new reference with each background update, - // even if the tokens haven't changed - const tokens = useSelector(getTokens, isEqual); - const { loading, tokensWithBalances } = useTokenTracker( - tokens, - true, - shouldHideZeroBalanceTokens, - ); + if (loading) { return ( - {tokensWithBalances.map((tokenData, index) => { - return ; - })} + {tokens.map((tokenData, index) => ( + + ))} ); } TokenList.propTypes = { onTokenClick: PropTypes.func.isRequired, + tokens: PropTypes.array.isRequired, + loading: PropTypes.bool, }; diff --git a/ui/components/multichain/balance-overview/balance-overview.js b/ui/components/multichain/balance-overview/balance-overview.js index 7f7a5f912e63..46962d7854b2 100644 --- a/ui/components/multichain/balance-overview/balance-overview.js +++ b/ui/components/multichain/balance-overview/balance-overview.js @@ -1,13 +1,14 @@ import React, { useContext } from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; +import PropTypes from 'prop-types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { Box, ButtonSecondary, IconName } from '../../component-library'; +import { Box, ButtonSecondary, IconName, Text } from '../../component-library'; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) import { getMmiPortfolioEnabled, @@ -22,12 +23,9 @@ import { getCurrentChainId, getMetaMetricsId, ///: END:ONLY_INCLUDE_IN - getSelectedAccountCachedBalance, isBalanceCached, } from '../../../selectors'; import Spinner from '../../ui/spinner'; -import UserPreferencedCurrencyDisplay from '../../app/user-preferenced-currency-display'; -import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { AlignItems, Display, @@ -39,7 +37,7 @@ import { import { CURRENCY_SYMBOLS } from '../../../../shared/constants/network'; ///: END:ONLY_INCLUDE_IN -export const BalanceOverview = () => { +export const BalanceOverview = ({ balance }) => { const trackEvent = useContext(MetaMetricsContext); const t = useI18nContext(); ///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask) @@ -47,7 +45,6 @@ export const BalanceOverview = () => { const chainId = useSelector(getCurrentChainId); ///: END:ONLY_INCLUDE_IN const balanceIsCached = useSelector(isBalanceCached); - const balance = useSelector(getSelectedAccountCachedBalance); ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) const mmiPortfolioEnabled = useSelector(getMmiPortfolioEnabled); @@ -85,56 +82,25 @@ export const BalanceOverview = () => { > {balance ? ( - + {balance} + + ) : ( + + )} + + {balanceIsCached ? ( + * ) : null} - - {balance ? ( - - ) : ( - - )} - {balanceIsCached ? ( - * - ) : null} - { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) @@ -172,3 +138,7 @@ export const BalanceOverview = () => { ); }; + +BalanceOverview.propTypes = { + balance: PropTypes.string.isRequired, +};