(Feature) Balances enhancements (#1122)

* Fix ts error
Add type return on fetchCurrencyValues

* Add skeleton for loading balance value

* Fix texts in uppercase

* Adds ETH Icon in currencyValues dropdown

* Adds getExchangeRatesUrlFallback
Adds support for ETH as currency

* Alphabetically sort currencies

* Add types

* Type formatAmount

* Adds formatAmountInUsFormat util function

* Add types
Uses formatAmountInUsFormat for BALANCE_TABLE_BALANCE_ID

* Updates max and min fraction digits on formatAmountInUsFormat
Add tests

* Updates max and min fraction digits on formatAmountInUsFormat
Add tests

* Add types

* Fix currencyValues types

* Adds safeFiatBalancesTotalSelector

* Adds total balance to safe header

* Fix types

* Adds currentCurrency on header

* Adds types to getTokenPriceInCurrency

* Fix balance currency rate conversion

* Add guards for modules

* Add guards for modules

* Uses console error for api

* Remove anys

* Redefine CurrencyRateValue types into CurrencyRateValueRecord

* Redefine test texts

* Use absolute imports

* Add types to dispatch

* Add guard for no balance value

* Fix ESLINT warning

* Add types

* Fix no balance case

* Use optional chaining

* Absolute paths

* Adds return types
Uses BigNumber in safeFiatBalancesTotalSelector

* Remove number as type for formatAmountInUsFormat

* Uses createStyles to remove any types

* Improve total balances display

* Fix balances value column

* formatAmountInUsFormat feedback

* Force boolean evaluation

* Fix totalBalance heading styles

* Add types

* Add types to fetchTokenCurrenciesBalances endpoint

* Replaces coinbase dependency by backend for ETH price in USD

* Absolute paths

* Replaces RecordOf<TokenProps> with Token

* Feedback

* Trigger buid

* Types

* Fix tests order

* Renames numberFormat to usNumberFormatter

Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>
This commit is contained in:
Agustin Pane 2020-07-28 12:29:26 -03:00 committed by GitHub
parent 8a6b219781
commit bbfa7d8166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 448 additions and 108 deletions

View File

@ -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)
}

View File

@ -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) => {

View File

@ -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<number> => {
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
}

View File

@ -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<AxiosResponse<BalanceEndpoint[]>> => {
if (!safeAddress) {
return null
}

View File

@ -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<typeof setCurrencyRate>,
): Promise<void> => {
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))

View File

@ -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<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
): Promise<void> => {
try {
const storedCurrencies: Map<string, CurrencyRateValue> | any = await loadCurrencyValues()
const storedCurrencies: Map<string, CurrencyRateValue> | unknown = await loadCurrencyValues()
const storedCurrency = storedCurrencies[safeAddress]
if (!storedCurrency) {
return batch(() => {

View File

@ -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<BalanceCurrencyRecord>
}
export type CurrencyRateValueRecord = RecordOf<CurrencyRateValue>
export const makeBalanceCurrency = Record({
currencyName: '',
tokenAddress: '',

View File

@ -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<string, RecordOf<CurrencyRateValue>> =>
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<BalanceCurrencyRecord> => {
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)
},
)

View File

@ -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<string, CurrencyRateValue>) => {
export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyRateValue>): Promise<void> => {
try {
await saveToStorage(CURRENCY_VALUES_STORAGE_KEY, currencyValues)
} catch (err) {
@ -11,6 +11,6 @@ export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyRat
}
}
export const loadCurrencyValues = async (): Promise<Map<string, CurrencyRateValue> | any> => {
export const loadCurrencyValues = async (): Promise<Map<string, CurrencyRateValue> | unknown> => {
return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {}
}

View File

@ -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('$', '')
}

View File

@ -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<BalanceDataRow>(List())
React.useMemo(() => {
setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate))
@ -64,7 +80,7 @@ const Coins = (props) => {
sortedData.map((row, index) => (
<TableRow className={classes.hide} data-testid={BALANCE_ROW_TEST_ID} key={index} tabIndex={-1}>
{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 = <div className={classes.currencyValueRow}>{row[id]}</div>
// 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 ? (
<div className={classes.currencyValueRow}>{row[id] ? row[id] : `0.00 ${selectedCurrency}`}</div>
) : (
<Skeleton animation="wave" />
)
break
}
default: {

View File

@ -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<BalanceCurrencyRecord>,
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<Token>,
currencySelected: AVAILABLE_CURRENCIES,
currencyValues: List<BalanceCurrencyRecord>,
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<TableColumn> => {
@ -102,7 +111,3 @@ export const generateColumns = (): List<TableColumn> => {
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))

View File

@ -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

View File

@ -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)`]: {

View File

@ -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}
>
<ListItemIcon className={classes.iconLeft}>
<div
className={classNames(
classes.localFlag,
'currency-flag',
'currency-flag-lg',
`currency-flag-${currencyName.toLowerCase()}`,
)}
/>
{currencyName === AVAILABLE_CURRENCIES.ETH ? (
<Img
alt="ether"
onError={setImageToPlaceholder}
src={etherIcon}
className={classNames(classes.etherFlag)}
/>
) : (
<div
className={classNames(
classes.localFlag,
'currency-flag',
'currency-flag-lg',
`currency-flag-${currencyName.toLowerCase()}`,
)}
/>
)}
</ListItemIcon>
<ListItemText primary={currencyName} />
{currencyName === selectedCurrency ? (

View File

@ -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',
},

View File

@ -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 (
<Block className={classes.container} margin="xl">
<Row className={classes.userInfo}>
@ -34,6 +49,12 @@ const LayoutHeader = (props) => {
<Row>
<Heading className={classes.nameText} color="primary" tag="h2" testId={SAFE_VIEW_NAME_HEADING_TEST_ID}>
{name}
{!!formattedTotalBalance && !!currentCurrency && (
<span className={classes.totalBalance}>
{' '}
| {formattedTotalBalance} {currentCurrency}
</span>
)}
</Heading>
{!granted && <Block className={classes.readonly}>Read Only</Block>}
</Row>
@ -87,4 +108,4 @@ const LayoutHeader = (props) => {
</Block>
)
}
export default withStyles(styles as any)(LayoutHeader)
export default LayoutHeader

View File

@ -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,
},
})

View File

@ -112,10 +112,10 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onClose}>
BACK
Back
</Button>
<Button color="primary" minWidth={140} type="submit" variant="contained">
CHANGE
Change
</Button>
</Row>
</>

View File

@ -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<string, any>
[INCOMING_TRANSACTIONS_REDUCER_ID]: Map<string, any>
[NOTIFICATIONS_REDUCER_ID]: Map<string, any>
[CURRENCY_VALUES_KEY]: Map<string, any>
[CURRENCY_VALUES_KEY]: Map<string, CurrencyRateValueRecord>
[COOKIES_REDUCER_ID]: Map<string, any>
[ADDRESS_BOOK_REDUCER_ID]: Map<string, any>
[CURRENT_SESSION_REDUCER_ID]: Map<string, any>

View File

@ -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)
})
})