diff --git a/src/components/Table/sorting.ts b/src/components/Table/sorting.ts index b15a7b33..cdff017a 100644 --- a/src/components/Table/sorting.ts +++ b/src/components/Table/sorting.ts @@ -2,7 +2,7 @@ import { List } from 'immutable' export const FIXED = 'fixed' -export const buildOrderFieldFrom = (attr) => `${attr}Order` +export const buildOrderFieldFrom = (attr: string): string => `${attr}Order` const desc = (a, b, orderBy, orderProp) => { const order = orderProp ? buildOrderFieldFrom(orderBy) : orderBy @@ -38,5 +38,10 @@ export const stableSort = (dataArray, cmp, fixed) => { return fixedElems.concat(sortedElems) } -export const getSorting = (order, orderBy, orderProp) => - order === 'desc' ? (a, b) => desc(a, b, orderBy, orderProp) : (a, b) => -desc(a, b, orderBy, orderProp) +export const getSorting = ( + order: string, + orderBy?: string, + orderProp?: boolean, +): ((a: unknown, b: unknown) => number) => { + return order === 'desc' ? (a, b) => desc(a, b, orderBy, orderProp) : (a, b) => -desc(a, b, orderBy, orderProp) +} diff --git a/src/config/index.ts b/src/config/index.ts index 8409d2b8..dd522ab2 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -87,6 +87,8 @@ export const getIntercomId = () => export const getExchangeRatesUrl = () => 'https://api.exchangeratesapi.io/latest' +export const getExchangeRatesUrlFallback = () => 'https://api.coinbase.com/v2/exchange-rates' + export const getSafeLastVersion = () => process.env.REACT_APP_LATEST_SAFE_VERSION || '1.1.1' export const buildSafeCreationTxUrl = (safeAddress) => { diff --git a/src/logic/currencyValues/api/fetchCurrenciesRates.ts b/src/logic/currencyValues/api/fetchCurrenciesRates.ts index 46986651..00e2c48a 100644 --- a/src/logic/currencyValues/api/fetchCurrenciesRates.ts +++ b/src/logic/currencyValues/api/fetchCurrenciesRates.ts @@ -2,20 +2,38 @@ import axios from 'axios' import { getExchangeRatesUrl } from 'src/config' import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues' +import fetchTokenCurrenciesBalances from './fetchTokenCurrenciesBalances' +import BigNumber from 'bignumber.js' const fetchCurrenciesRates = async ( baseCurrency: AVAILABLE_CURRENCIES, targetCurrencyValue: AVAILABLE_CURRENCIES, + safeAddress: string, ): Promise => { let rate = 0 - const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}` - const result = await axios.get(url) - if (result && result.data) { - const { rates } = result.data - rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0 + if (targetCurrencyValue === AVAILABLE_CURRENCIES.ETH) { + try { + const result = await fetchTokenCurrenciesBalances(safeAddress) + if (result?.data?.length) { + rate = new BigNumber(1).div(result.data[0].usdConversion).toNumber() + } + } catch (error) { + console.error('Fetching ETH data from the relayer errored', error) + } + return rate } + try { + const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}` + const result = await axios.get(url) + if (result?.data) { + const { rates } = result.data + rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0 + } + } catch (error) { + console.error('Fetching data from getExchangeRatesUrl errored', error) + } return rate } diff --git a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts index fe4a293f..5c4fc541 100644 --- a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts +++ b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts @@ -1,8 +1,17 @@ -import axios from 'axios' +import axios, { AxiosResponse } from 'axios' import { getTxServiceHost } from 'src/config' +import { TokenProps } from '../../tokens/store/model/token' -const fetchTokenCurrenciesBalances = (safeAddress) => { +type BalanceEndpoint = { + balance: string + balanceUsd: string + tokenAddress?: string + token?: TokenProps + usdConversion: string +} + +const fetchTokenCurrenciesBalances = (safeAddress?: string): Promise> => { if (!safeAddress) { return null } diff --git a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts b/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts index 34a0dfbf..72f668c6 100644 --- a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts +++ b/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts @@ -1,8 +1,11 @@ import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates' import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate' import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' +import { Dispatch } from 'redux' -const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => async (dispatch) => { +const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => async ( + dispatch: Dispatch, +): Promise => { if (AVAILABLE_CURRENCIES.USD === selectedCurrency) { return dispatch(setCurrencyRate(safeAddress, 1)) } @@ -10,6 +13,7 @@ const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURR const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates( AVAILABLE_CURRENCIES.USD, selectedCurrency, + safeAddress, ) dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency)) diff --git a/src/logic/currencyValues/store/actions/fetchCurrencyValues.ts b/src/logic/currencyValues/store/actions/fetchCurrencyValues.ts index 2c098cbf..b109f93e 100644 --- a/src/logic/currencyValues/store/actions/fetchCurrencyValues.ts +++ b/src/logic/currencyValues/store/actions/fetchCurrencyValues.ts @@ -6,10 +6,13 @@ import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurre import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' import { AVAILABLE_CURRENCIES, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues' import { loadCurrencyValues } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' +import { Dispatch } from 'redux' -export const fetchCurrencyValues = (safeAddress: string) => async (dispatch) => { +export const fetchCurrencyValues = (safeAddress: string) => async ( + dispatch: Dispatch, +): Promise => { try { - const storedCurrencies: Map | any = await loadCurrencyValues() + const storedCurrencies: Map | unknown = await loadCurrencyValues() const storedCurrency = storedCurrencies[safeAddress] if (!storedCurrency) { return batch(() => { diff --git a/src/logic/currencyValues/store/model/currencyValues.ts b/src/logic/currencyValues/store/model/currencyValues.ts index dcfefb47..8a0c0786 100644 --- a/src/logic/currencyValues/store/model/currencyValues.ts +++ b/src/logic/currencyValues/store/model/currencyValues.ts @@ -1,42 +1,43 @@ -import { Record } from 'immutable' +import { List, Record, RecordOf } from 'immutable' export enum AVAILABLE_CURRENCIES { + ETH = 'ETH', USD = 'USD', EUR = 'EUR', - CAD = 'CAD', - HKD = 'HKD', - ISK = 'ISK', - PHP = 'PHP', - DKK = 'DKK', - HUF = 'HUF', - CZK = 'CZK', AUD = 'AUD', - RON = 'RON', - SEK = 'SEK', - IDR = 'IDR', - INR = 'INR', - BRL = 'BRL', - RUB = 'RUB', - HRK = 'HRK', - JPY = 'JPY', - THB = 'THB', - CHF = 'CHF', - SGD = 'SGD', - PLN = 'PLN', BGN = 'BGN', - TRY = 'TRY', + BRL = 'BRL', + CAD = 'CAD', + CHF = 'CHF', CNY = 'CNY', + CZK = 'CZK', + DKK = 'DKK', + GBP = 'GBP', + HKD = 'HKD', + HRK = 'HRK', + HUF = 'HUF', + IDR = 'IDR', + ILS = 'ILS', + INR = 'INR', + ISK = 'ISK', + JPY = 'JPY', + KRW = 'KRW', + MXN = 'MXN', + MYR = 'MYR', NOK = 'NOK', NZD = 'NZD', + PHP = 'PHP', + PLN = 'PLN', + RON = 'RON', + RUB = 'RUB', + SEK = 'SEK', + SGD = 'SGD', + THB = 'THB', + TRY = 'TRY', ZAR = 'ZAR', - MXN = 'MXN', - ILS = 'ILS', - GBP = 'GBP', - KRW = 'KRW', - MYR = 'MYR', } -type BalanceCurrencyRecord = { +export type BalanceCurrencyRecord = { currencyName?: string tokenAddress?: string balanceInBaseCurrency: string @@ -46,9 +47,11 @@ type BalanceCurrencyRecord = { export type CurrencyRateValue = { currencyRate?: number selectedCurrency?: AVAILABLE_CURRENCIES - currencyBalances?: BalanceCurrencyRecord[] + currencyBalances?: List } +export type CurrencyRateValueRecord = RecordOf + export const makeBalanceCurrency = Record({ currencyName: '', tokenAddress: '', diff --git a/src/logic/currencyValues/store/selectors/index.ts b/src/logic/currencyValues/store/selectors/index.ts index 8594f1e1..9b278f44 100644 --- a/src/logic/currencyValues/store/selectors/index.ts +++ b/src/logic/currencyValues/store/selectors/index.ts @@ -1,29 +1,58 @@ -import { List } from 'immutable' +import { List, Map, RecordOf } from 'immutable' import { createSelector } from 'reselect' import { CURRENCY_VALUES_KEY } from 'src/logic/currencyValues/store/reducer/currencyValues' import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' +import { AppReduxState } from 'src/store/index' +import { + AVAILABLE_CURRENCIES, + BalanceCurrencyRecord, + CurrencyRateValue, + CurrencyRateValueRecord, +} from 'src/logic/currencyValues/store/model/currencyValues' +import { BigNumber } from 'bignumber.js' -export const currencyValuesSelector = (state) => state[CURRENCY_VALUES_KEY] +export const currencyValuesSelector = (state: AppReduxState): Map> => + state[CURRENCY_VALUES_KEY] export const safeFiatBalancesSelector = createSelector( currencyValuesSelector, safeParamAddressFromStateSelector, - (currencyValues, safeAddress) => { + (currencyValues, safeAddress): CurrencyRateValueRecord => { if (!currencyValues) return return currencyValues.get(safeAddress) }, ) -export const safeFiatBalancesListSelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap) => { - if (!currencyValuesMap) return - return currencyValuesMap.get('currencyBalances') ? currencyValuesMap.get('currencyBalances') : List([]) -}) - -export const currentCurrencySelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap) => - currencyValuesMap ? currencyValuesMap.get('selectedCurrency') : null, +export const safeFiatBalancesListSelector = createSelector( + safeFiatBalancesSelector, + (currencyValuesMap): List => { + if (!currencyValuesMap) return + return currencyValuesMap.get('currencyBalances') ? currencyValuesMap.get('currencyBalances') : List([]) + }, ) -export const currencyRateSelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap) => +export const currentCurrencySelector = createSelector( + safeFiatBalancesSelector, + (currencyValuesMap): AVAILABLE_CURRENCIES | null => + currencyValuesMap ? currencyValuesMap.get('selectedCurrency') : null, +) + +export const currencyRateSelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap): number | null => currencyValuesMap ? currencyValuesMap.get('currencyRate') : null, ) + +export const safeFiatBalancesTotalSelector = createSelector( + safeFiatBalancesListSelector, + currencyRateSelector, + (currencyBalances, currencyRate): string | null => { + if (!currencyBalances) return '0' + if (!currencyRate) return null + + const totalInBaseCurrency = currencyBalances.reduce((total, balanceCurrencyRecord) => { + return total.plus(balanceCurrencyRecord.balanceInBaseCurrency) + }, new BigNumber(0)) + + return totalInBaseCurrency.times(currencyRate).toFixed(2) + }, +) diff --git a/src/logic/currencyValues/store/utils/currencyValuesStorage.ts b/src/logic/currencyValues/store/utils/currencyValuesStorage.ts index 002c8884..6a265fc8 100644 --- a/src/logic/currencyValues/store/utils/currencyValuesStorage.ts +++ b/src/logic/currencyValues/store/utils/currencyValuesStorage.ts @@ -3,7 +3,7 @@ import { CurrencyRateValue } from '../model/currencyValues' import { Map } from 'immutable' const CURRENCY_VALUES_STORAGE_KEY = 'CURRENCY_VALUES_STORAGE_KEY' -export const saveCurrencyValues = async (currencyValues: Map) => { +export const saveCurrencyValues = async (currencyValues: Map): Promise => { try { await saveToStorage(CURRENCY_VALUES_STORAGE_KEY, currencyValues) } catch (err) { @@ -11,6 +11,6 @@ export const saveCurrencyValues = async (currencyValues: Map | any> => { +export const loadCurrencyValues = async (): Promise | unknown> => { return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {} } diff --git a/src/logic/tokens/utils/formatAmount.ts b/src/logic/tokens/utils/formatAmount.ts index 033822a8..77ba6091 100644 --- a/src/logic/tokens/utils/formatAmount.ts +++ b/src/logic/tokens/utils/formatAmount.ts @@ -12,8 +12,8 @@ const lt100mFormatter = new Intl.NumberFormat([], { maximumFractionDigits: 0 }) // same format for billions and trillions const lt1000tFormatter = new Intl.NumberFormat([], { maximumFractionDigits: 3, notation: 'compact' } as any) -export const formatAmount = (number) => { - let numberFloat: any = parseFloat(number) +export const formatAmount = (number: string): string => { + let numberFloat: number | string = parseFloat(number) if (numberFloat === 0) { numberFloat = '0' @@ -39,3 +39,11 @@ export const formatAmount = (number) => { return numberFloat } + +const options = { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 8 } +const usNumberFormatter = new Intl.NumberFormat('en-US', options) + +export const formatAmountInUsFormat = (amount: string): string => { + const numberFloat: number = parseFloat(amount) + return usNumberFormatter.format(numberFloat).replace('$', '') +} diff --git a/src/routes/safe/components/Balances/Coins/index.tsx b/src/routes/safe/components/Balances/Coins/index.tsx index 50ba30fd..2eb79fdf 100644 --- a/src/routes/safe/components/Balances/Coins/index.tsx +++ b/src/routes/safe/components/Balances/Coins/index.tsx @@ -2,9 +2,6 @@ import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' import { makeStyles } from '@material-ui/core/styles' -//import CallMade from '@material-ui/icons/CallMade' -//import CallReceived from '@material-ui/icons/CallReceived' -//import classNames from 'classnames/bind' import { List } from 'immutable' import React from 'react' import { useSelector } from 'react-redux' @@ -30,10 +27,29 @@ import { getBalanceData, } from 'src/routes/safe/components/Balances/dataFetcher' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' +import { Skeleton } from '@material-ui/lab' const useStyles = makeStyles(styles as any) -const Coins = (props) => { +type Props = { + showReceiveFunds: () => void + showSendFunds: (tokenAddress: string) => void +} + +export type BalanceDataRow = List<{ + asset: { + name: string + address: string + logoUri: string + } + assetOrder: string + balance: string + balanceOrder: number + fixed: boolean + value: string +}> + +const Coins = (props: Props): React.ReactElement => { const { showReceiveFunds, showSendFunds } = props const classes = useStyles() const columns = generateColumns() @@ -43,7 +59,7 @@ const Coins = (props) => { const activeTokens = useSelector(extendedSafeTokensSelector) const currencyValues = useSelector(safeFiatBalancesListSelector) const granted = useSelector(grantedSelector) - const [filteredData, setFilteredData] = React.useState(List()) + const [filteredData, setFilteredData] = React.useState(List()) React.useMemo(() => { setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate)) @@ -64,7 +80,7 @@ const Coins = (props) => { sortedData.map((row, index) => ( {autoColumns.map((column) => { - const { align, id, width }: any = column + const { align, id, width } = column let cellItem switch (id) { case BALANCE_TABLE_ASSET_ID: { @@ -76,7 +92,16 @@ const Coins = (props) => { break } case BALANCE_TABLE_VALUE_ID: { - cellItem =
{row[id]}
+ // If there are no values for that row but we have balances, we display as '0.00 {CurrencySelected}' + // In case we don't have balances, we display a skeleton + const showCurrencyValueRow = row[id] || row[BALANCE_TABLE_BALANCE_ID] + + cellItem = + showCurrencyValueRow && selectedCurrency ? ( +
{row[id] ? row[id] : `0.00 ${selectedCurrency}`}
+ ) : ( + + ) break } default: { diff --git a/src/routes/safe/components/Balances/dataFetcher.ts b/src/routes/safe/components/Balances/dataFetcher.ts index 087e8ae7..fb0e311e 100644 --- a/src/routes/safe/components/Balances/dataFetcher.ts +++ b/src/routes/safe/components/Balances/dataFetcher.ts @@ -1,17 +1,24 @@ import { BigNumber } from 'bignumber.js' import { List } from 'immutable' -import { FIXED, buildOrderFieldFrom } from 'src/components/Table/sorting' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { FIXED } from 'src/components/Table/sorting' +import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' import { TableColumn } from 'src/components/Table/types' +import { AVAILABLE_CURRENCIES, BalanceCurrencyRecord } from 'src/logic/currencyValues/store/model/currencyValues' +import { Token } from 'src/logic/tokens/store/model/token' +import { BalanceDataRow } from './Coins' export const BALANCE_TABLE_ASSET_ID = 'asset' export const BALANCE_TABLE_BALANCE_ID = 'balance' export const BALANCE_TABLE_VALUE_ID = 'value' -// eslint-disable-next-line max-len -const getTokenPriceInCurrency = (token, currencySelected, currencyValues, currencyRate) => { +const getTokenPriceInCurrency = ( + token: Token, + currencySelected: AVAILABLE_CURRENCIES, + currencyValues: List, + currencyRate: number | null, +): string => { if (!currencySelected) { return '' } @@ -31,26 +38,28 @@ const getTokenPriceInCurrency = (token, currencySelected, currencyValues, curren const { balanceInBaseCurrency } = currencyValue const balance = new BigNumber(balanceInBaseCurrency).times(currencyRate).toFixed(2) - return `${balance} ${currencySelected}` + return `${formatAmountInUsFormat(balance)} ${currencySelected}` } -// eslint-disable-next-line max-len -export const getBalanceData = (activeTokens, currencySelected, currencyValues, currencyRate) => { - const rows = activeTokens.map((token) => ({ +export const getBalanceData = ( + activeTokens: List, + currencySelected: AVAILABLE_CURRENCIES, + currencyValues: List, + currencyRate: number, +): BalanceDataRow => { + return activeTokens.map((token) => ({ [BALANCE_TABLE_ASSET_ID]: { name: token.name, logoUri: token.logoUri, address: token.address, symbol: token.symbol, }, - [buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name, - [BALANCE_TABLE_BALANCE_ID]: `${formatAmount(token.balance)} ${token.symbol}`, - [buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.balance), - [FIXED]: token.get('symbol') === 'ETH', + assetOrder: token.name, + [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance.toString())} ${token.symbol}`, + balanceOrder: Number(token.balance), + [FIXED]: token.symbol === 'ETH', [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate), })) - - return rows } export const generateColumns = (): List => { @@ -102,7 +111,3 @@ export const generateColumns = (): List => { return List([assetColumn, balanceColumn, value, actions]) } - -// eslint-disable-next-line max-len -export const filterByZero = (data, hideZero) => - data.filter((row) => (hideZero ? row[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)] !== 0 : true)) diff --git a/src/routes/safe/components/Balances/index.tsx b/src/routes/safe/components/Balances/index.tsx index 52763fea..391b3d29 100644 --- a/src/routes/safe/components/Balances/index.tsx +++ b/src/routes/safe/components/Balances/index.tsx @@ -1,4 +1,4 @@ -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' @@ -41,7 +41,10 @@ const INITIAL_STATE = { export const COINS_LOCATION_REGEX = /\/balances\/?$/ export const COLLECTIBLES_LOCATION_REGEX = /\/balances\/collectibles$/ -const Balances = (props) => { +const useStyles = makeStyles(styles) + +const Balances = (): React.ReactElement => { + const classes = useStyles() const [state, setState] = useState(INITIAL_STATE) const address = useSelector(safeParamAddressFromStateSelector) @@ -66,7 +69,7 @@ const Balances = (props) => { setState((prevState) => ({ ...prevState, [`show${action}`]: false })) } - const showSendFunds = (tokenAddress) => { + const showSendFunds = (tokenAddress: string): void => { setState((prevState) => ({ ...prevState, sendFunds: { @@ -95,7 +98,7 @@ const Balances = (props) => { manageTokensButton, receiveModal, tokenControls, - } = props.classes + } = classes const { erc721Enabled, sendFunds, showManageCollectibleModal, showReceive, showToken } = state return ( @@ -227,4 +230,4 @@ const Balances = (props) => { ) } -export default withStyles(styles as any)(Balances) +export default Balances diff --git a/src/routes/safe/components/Balances/style.ts b/src/routes/safe/components/Balances/style.ts index 259533a0..bcdaf782 100644 --- a/src/routes/safe/components/Balances/style.ts +++ b/src/routes/safe/components/Balances/style.ts @@ -1,6 +1,7 @@ import { md, screenSm, secondary, xs } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ controls: { alignItems: 'center', boxSizing: 'border-box', @@ -10,7 +11,7 @@ export const styles = () => ({ assetTabs: { alignItems: 'center', display: 'flex', - order: '2', + order: 2, [`@media (min-width: ${screenSm}px)`]: { order: '1', @@ -41,7 +42,7 @@ export const styles = () => ({ alignItems: 'center', display: 'flex', justifyContent: 'space-between', - order: '1', + order: 1, padding: '0 0 10px', [`@media (min-width: ${screenSm}px)`]: { diff --git a/src/routes/safe/components/CurrencyDropdown/index.tsx b/src/routes/safe/components/CurrencyDropdown/index.tsx index 41945040..0bca0438 100644 --- a/src/routes/safe/components/CurrencyDropdown/index.tsx +++ b/src/routes/safe/components/CurrencyDropdown/index.tsx @@ -18,8 +18,11 @@ import { currentCurrencySelector } from 'src/logic/currencyValues/store/selector import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style' import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import { DropdownListTheme } from 'src/theme/mui' +import { setImageToPlaceholder } from '../Balances/utils' +import Img from 'src/components/layout/Img/index' +import etherIcon from 'src/assets/icons/icon_etherTokens.svg' -const CurrencyDropdown = () => { +const CurrencyDropdown = (): React.ReactElement => { const currenciesList = Object.values(AVAILABLE_CURRENCIES) const safeAddress = useSelector(safeParamAddressFromStateSelector) const dispatch = useDispatch() @@ -96,14 +99,23 @@ const CurrencyDropdown = () => { value={currencyName} > -
+ {currencyName === AVAILABLE_CURRENCIES.ETH ? ( + ether + ) : ( +
+ )} {currencyName === selectedCurrency ? ( diff --git a/src/routes/safe/components/CurrencyDropdown/style.ts b/src/routes/safe/components/CurrencyDropdown/style.ts index 2756ef70..67bac5e3 100644 --- a/src/routes/safe/components/CurrencyDropdown/style.ts +++ b/src/routes/safe/components/CurrencyDropdown/style.ts @@ -18,6 +18,13 @@ export const useDropdownStyles = makeStyles({ height: '20px !important', width: '26px !important', }, + etherFlag: { + backgroundPosition: '50% 50%', + backgroundRepeat: 'no-repeat', + backgroundSize: 'contain', + width: '26px', + height: '26px', + }, iconLeft: { marginRight: '10px', }, diff --git a/src/routes/safe/components/Layout/Header/index.tsx b/src/routes/safe/components/Layout/Header/index.tsx index cedb0bd6..5d0c07bc 100644 --- a/src/routes/safe/components/Layout/Header/index.tsx +++ b/src/routes/safe/components/Layout/Header/index.tsx @@ -1,4 +1,4 @@ -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import CallMade from '@material-ui/icons/CallMade' import CallReceived from '@material-ui/icons/CallReceived' import classNames from 'classnames/bind' @@ -19,13 +19,28 @@ import { SAFE_VIEW_NAME_HEADING_TEST_ID } from 'src/routes/safe/components/Layou import { grantedSelector } from 'src/routes/safe/container/selector' import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' -const LayoutHeader = (props) => { - const { classes, onShow, showSendFunds } = props +import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors/index' +import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' + +const useStyles = makeStyles(styles) + +type Props = { + onShow: (modalName: string) => void + showSendFunds: (modalName: string) => void +} + +const LayoutHeader = (props: Props): React.ReactElement => { + const { onShow, showSendFunds } = props + const classes = useStyles(styles) const address = useSelector(safeParamAddressFromStateSelector) const granted = useSelector(grantedSelector) const name = useSelector(safeNameSelector) + const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector) + const currentCurrency = useSelector(currentCurrencySelector) if (!address) return null + const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : '' + return ( @@ -34,6 +49,12 @@ const LayoutHeader = (props) => { {name} + {!!formattedTotalBalance && !!currentCurrency && ( + + {' '} + | {formattedTotalBalance} {currentCurrency} + + )} {!granted && Read Only} @@ -87,4 +108,4 @@ const LayoutHeader = (props) => { ) } -export default withStyles(styles as any)(LayoutHeader) +export default LayoutHeader diff --git a/src/routes/safe/components/Layout/Header/style.ts b/src/routes/safe/components/Layout/Header/style.ts index 991952ff..a3a300b1 100644 --- a/src/routes/safe/components/Layout/Header/style.ts +++ b/src/routes/safe/components/Layout/Header/style.ts @@ -1,6 +1,7 @@ -import { screenSm, secondaryText, sm, smallFontSize, xs } from 'src/theme/variables' +import { screenSm, secondaryText, sm, smallFontSize, xs, disabled, fontSizeHeadingSm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core/styles' -export const styles = () => ({ +export const styles = createStyles({ container: { alignItems: 'center', display: 'flex', @@ -90,4 +91,8 @@ export const styles = () => ({ wordBreak: 'break-word', whiteSpace: 'normal', }, + totalBalance: { + color: disabled, + fontSize: fontSizeHeadingSm, + }, }) diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index d9f1f765..9c5179bd 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -112,10 +112,10 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr diff --git a/src/store/index.ts b/src/store/index.ts index 97fa7ced..13650bb1 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,4 +1,3 @@ -import { Map } from 'immutable' import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router' import { createHashHistory } from 'history' import { applyMiddleware, combineReducers, compose, createStore, CombinedState } from 'redux' @@ -31,6 +30,8 @@ import incomingTransactions, { import safe, { SAFE_REDUCER_ID, SafeReducerMap } from 'src/routes/safe/store/reducer/safe' import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions' import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea' +import { Map } from 'immutable' +import { CurrencyRateValueRecord } from 'src/logic/currencyValues/store/model/currencyValues' export const history = createHashHistory() @@ -75,7 +76,7 @@ export type AppReduxState = CombinedState<{ [CANCELLATION_TRANSACTIONS_REDUCER_ID]: Map [INCOMING_TRANSACTIONS_REDUCER_ID]: Map [NOTIFICATIONS_REDUCER_ID]: Map - [CURRENCY_VALUES_KEY]: Map + [CURRENCY_VALUES_KEY]: Map [COOKIES_REDUCER_ID]: Map [ADDRESS_BOOK_REDUCER_ID]: Map [CURRENT_SESSION_REDUCER_ID]: Map diff --git a/src/test/logic/token/utils/formatAmount.test.ts b/src/test/logic/token/utils/formatAmount.test.ts new file mode 100644 index 00000000..94fc60c9 --- /dev/null +++ b/src/test/logic/token/utils/formatAmount.test.ts @@ -0,0 +1,179 @@ +import { formatAmount, formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' + + +describe('formatAmount', () => { + it('Given 0 returns 0', () => { + // given + const input = '0' + const expectedResult = '0' + + // when + const result = formatAmount(input) + + // then + expect(result).toBe(expectedResult) + }) + it('Given 1 returns 1', () => { + // given + const input = '1' + const expectedResult = '1' + + // when + const result = formatAmount(input) + + // then + expect(result).toBe(expectedResult) + }) + it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { + // given + const input = '19797.899' + const expectedResult = '19,797.899' + + // when + const result = formatAmount(input) + + // then + expect(result).toBe(expectedResult) + }) + it('Given number > 0.001 && < 1000 returns the same number as string', () => { + // given + const input = 999 + const expectedResult = '999' + + // when + const result = formatAmount(input.toString()) + // then + expect(result).toBe(expectedResult) + }) + it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => { + // given + const input = 9999 + const expectedResult = '9,999' + + // when + const result = formatAmount(input.toString()) + // then + expect(result).toBe(expectedResult) + }) + it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => { + // given + const input = 99999 + const expectedResult = '99,999' + + // when + const result = formatAmount(input.toString()) + // then + expect(result).toBe(expectedResult) + }) + it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => { + // given + const input = 999999 + const expectedResult = '999,999' + + // when + const result = formatAmount(input.toString()) + // then + expect(result).toBe(expectedResult) + }) + it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => { + // given + const input = 9999999 + const expectedResult = '9,999,999' + + // when + const result = formatAmount(input.toString()) + // then + expect(result).toBe(expectedResult) + }) + it('Given number < 0.001 returns < 0.001', () => { + // given + const input = 0.000001 + const expectedResult = '< 0.001' + + // when + const result = formatAmount(input.toString()) + // then + expect(result).toBe(expectedResult) + }) + it('Given number > 10 ** 15 returns > 1000T', () => { + // given + const input = 10 ** 15 * 2 + const expectedResult = '> 1000T' + + // when + const result = formatAmount(input.toString()) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('FormatsAmountsInUsFormat', () => { + it('Given 0 returns 0.00', () => { + // given + const input = 0 + const expectedResult = '0.00' + + // when + const result = formatAmountInUsFormat(input.toString()) + + // then + expect(result).toBe(expectedResult) + }) + it('Given 1 returns 1.00', () => { + // given + const input = 1 + const expectedResult = '1.00' + + // when + const result = formatAmountInUsFormat(input.toString()) + + // then + expect(result).toBe(expectedResult) + }) + it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => { + // given + const input = 311137.30 + const expectedResult = '311,137.30' + + // when + const result = formatAmountInUsFormat(input.toString()) + + // then + expect(result).toBe(expectedResult) + }) + it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { + // given + const input = 19797.899 + const expectedResult = '19,797.899' + + // when + const result = formatAmountInUsFormat(input.toString()) + + // then + expect(result).toBe(expectedResult) + }) + it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => { + // given + const input = 19797899.479 + const expectedResult = '19,797,899.479' + + // when + const result = formatAmountInUsFormat(input.toString()) + + // then + expect(result).toBe(expectedResult) + }) + it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => { + // given + const input = 19797899479.999 + const expectedResult = '19,797,899,479.999' + + // when + const result = formatAmountInUsFormat(input.toString()) + + // then + expect(result).toBe(expectedResult) + }) +}) +