Feature #154: Fiat Balances (#290)

* Adds DropdownCurrency
Adds redux store for currencyValues
Adds Value column on the assets table
Adds mocked currency values

* (add) base currency dropdown

* (add) dropdown styles

* Refactors data fetching of the balances list
Now uses the endpoint

* Fix column value styling

* Adds support for ECB currency values

* Fixs list overflow

* Changes endpoint url
Adds decimals for balance values

* (fix) remove inline style

* (add) currencies dropdown search field

* (fix) list items' hover color

* Implements filter search

* Fix warning on dropdown template

* Saves selected currency in localStorage

* Remove spaces on curly braces
Add alt
Renames rowItem to cellItem
Improves fetchCurrenciesRates handling

* Removes withMutations

* Removes middleware
Export style to another file for dropdownCurrency

* Adds classNames
This commit is contained in:
Agustin Pane 2019-12-13 11:35:05 -03:00 committed by GitHub
parent d69e5fca7f
commit 63c1153772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 671 additions and 43 deletions

View File

@ -42,6 +42,7 @@
"bignumber.js": "9.0.0",
"connected-react-router": "6.6.1",
"date-fns": "2.8.1",
"currency-flags": "^2.1.1",
"dotenv": "^8.2.0",
"ethereum-ens": "0.7.8",
"final-form": "4.18.6",

View File

@ -16,6 +16,7 @@ export type Column = {
custom: boolean, // If content will be rendered by user manually
width?: number,
static?: boolean, // If content can't be sorted by values in the column
style?: Object, // if you want to add some custom styling to the column
}
export const cellWidth = (width: number | typeof undefined) => {
@ -56,12 +57,15 @@ class GnoTableHead extends React.PureComponent<Props> {
sortDirection={orderBy === column.id ? order : false}
>
{column.static ? (
column.label
<div style={column.style}>
{column.label}
</div>
) : (
<TableSortLabel
active={orderBy === column.id}
direction={order}
onClick={this.changeSort(column.id, column.order)}
style={column.style}
>
{column.label}
</TableSortLabel>

View File

@ -72,3 +72,5 @@ export const getIntercomId = () =>
process.env.REACT_APP_ENV === "production"
? process.env.REACT_APP_INTERCOM_ID
: "plssl1fl"
export const getExchangeRatesUrl = () => 'https://api.exchangeratesapi.io/latest'

View File

@ -0,0 +1,19 @@
// @flow
import axios from 'axios'
import { getExchangeRatesUrl } from '~/config'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
const fetchCurrenciesRates = async (baseCurrency: AVAILABLE_CURRENCIES, targetCurrencyValue: AVAILABLE_CURRENCIES): 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
}
return rate
}
export default fetchCurrenciesRates

View File

@ -0,0 +1,15 @@
// @flow
import axios from 'axios'
import { getTxServiceHost } from '~/config'
const fetchTokenCurrenciesBalances = (safeAddress: string) => {
if (!safeAddress) {
return null
}
const apiUrl = getTxServiceHost()
const url = `${apiUrl}safes/${safeAddress}/balances/usd`
return axios.get(url)
}
export default fetchTokenCurrenciesBalances

View File

@ -0,0 +1,35 @@
// @flow
import { Dispatch as ReduxDispatch } from 'redux'
import { List } from 'immutable'
import type { GlobalState } from '~/store'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import fetchCurrenciesRates from '~/logic/currencyValues/api/fetchCurrenciesRates'
import { currencyValuesListSelector } from '~/logic/currencyValues/store/selectors'
import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
// eslint-disable-next-line max-len
const fetchCurrencySelectedValue = (currencyValueSelected: AVAILABLE_CURRENCIES) => async (dispatch: ReduxDispatch<GlobalState>, getState: Function) => {
const state = getState()
const currencyBalancesList = currencyValuesListSelector(state)
const selectedCurrencyRateInBaseCurrency = await fetchCurrenciesRates(AVAILABLE_CURRENCIES.USD, currencyValueSelected)
const newList = []
for (const currencyValue of currencyBalancesList) {
const { balanceInBaseCurrency } = currencyValue
const balanceInSelectedCurrency = balanceInBaseCurrency * selectedCurrencyRateInBaseCurrency
const updatedValue = currencyValue.merge({
currencyName: currencyValueSelected,
balanceInSelectedCurrency,
})
newList.push(updatedValue)
}
dispatch(setCurrencyBalances(List(newList)))
}
export default fetchCurrencySelectedValue

View File

@ -0,0 +1,43 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { List } from 'immutable'
import type { GlobalState } from '~/store'
import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
import { AVAILABLE_CURRENCIES, makeBalanceCurrency } from '~/logic/currencyValues/store/model/currencyValues'
import { setCurrencySelected } from '~/logic/currencyValues/store/actions/setCurrencySelected'
import fetchTokenCurrenciesBalances from '~/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { loadFromStorage } from '~/utils/storage'
import fetchCurrencySelectedValue from '~/logic/currencyValues/store/actions/fetchCurrencySelectedValue'
import { CURRENCY_SELECTED_KEY } from '~/logic/currencyValues/store/actions/saveCurrencySelected'
export const fetchCurrencyValues = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const tokensFetched = await fetchTokenCurrenciesBalances(safeAddress)
// eslint-disable-next-line max-len
const currencyList = List(tokensFetched.data.filter((currencyBalance) => currencyBalance.balanceUsd).map((currencyBalance) => {
const { balanceUsd, tokenAddress } = currencyBalance
return makeBalanceCurrency({
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null,
tokenAddress,
balanceInBaseCurrency: balanceUsd,
balanceInSelectedCurrency: balanceUsd,
})
}))
dispatch(setCurrencyBalances(currencyList))
const currencyStored = await loadFromStorage(CURRENCY_SELECTED_KEY)
if (!currencyStored) {
return dispatch(setCurrencySelected(AVAILABLE_CURRENCIES.USD))
}
const { currencyValueSelected } = currencyStored
dispatch(fetchCurrencySelectedValue(currencyValueSelected))
dispatch(setCurrencySelected(currencyValueSelected))
} catch (err) {
console.error('Error fetching tokens price list', err)
}
return Promise.resolve()
}
export default fetchCurrencyValues

View File

@ -0,0 +1,18 @@
// @flow
import { Dispatch as ReduxDispatch } from 'redux'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import type { GlobalState } from '~/store'
import { saveToStorage } from '~/utils/storage'
import { setCurrencySelected } from '~/logic/currencyValues/store/actions/setCurrencySelected'
export const CURRENCY_SELECTED_KEY = 'CURRENCY_SELECTED_KEY'
const saveCurrencySelected = (currencySelected: AVAILABLE_CURRENCIES) => async (dispatch: ReduxDispatch<GlobalState>) => {
await saveToStorage(CURRENCY_SELECTED_KEY, { currencyValueSelected: currencySelected })
dispatch(setCurrencySelected(currencySelected))
}
export default saveCurrencySelected

View File

@ -0,0 +1,10 @@
// @flow
import { Map } from 'immutable'
import { createAction } from 'redux-actions'
import type { CurrencyValues, CurrencyValuesProps } from '~/logic/currencyValues/store/model/currencyValues'
export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES'
// eslint-disable-next-line max-len
export const setCurrencyBalances = createAction<string, *>(SET_CURRENCY_BALANCES, (currencyBalances: Map<string, CurrencyValues>): CurrencyValuesProps => ({ currencyBalances }))

View File

@ -0,0 +1,13 @@
// @flow
import { createAction } from 'redux-actions'
import type {
CurrencyValuesProps,
} from '~/logic/currencyValues/store/model/currencyValues'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
// eslint-disable-next-line max-len
export const setCurrencySelected = createAction<string, *>(SET_CURRENT_CURRENCY, (currencyValueSelected: AVAILABLE_CURRENCIES): CurrencyValuesProps => ({ currencyValueSelected }))

View File

@ -0,0 +1,61 @@
// @flow
import type { RecordOf } from 'immutable'
import { Record } from 'immutable'
export const AVAILABLE_CURRENCIES = {
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',
CNY: 'CNY',
NOK: 'NOK',
NZD: 'NZD',
ZAR: 'ZAR',
MXN: 'MXN',
ILS: 'ILS',
GBP: 'GBP',
KRW: 'KRW',
MYR: 'MYR',
}
export type BalanceCurrencyType = {
currencyName: AVAILABLE_CURRENCIES;
tokenAddress: string,
balanceInBaseCurrency: string,
balanceInSelectedCurrency: string,
}
export const makeBalanceCurrency = Record({
currencyName: '',
tokenAddress: '',
balanceInBaseCurrency: '',
balanceInSelectedCurrency: '',
})
export type CurrencyValuesProps = {
currencyValueSelected: AVAILABLE_CURRENCIES;
currencyValuesList: BalanceCurrencyType[]
}
export type CurrencyValues = RecordOf<CurrencyValuesProps>

View File

@ -0,0 +1,28 @@
// @flow
import { Map } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import { SET_CURRENCY_BALANCES } from '../actions/setCurrencyBalances'
import type { State } from '~/logic/tokens/store/reducer/tokens'
import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setCurrencySelected'
export const CURRENCY_VALUES_KEY = 'currencyValues'
export default handleActions<State, *>(
{
[SET_CURRENCY_BALANCES]: (state: State, action: ActionType<Function>): State => {
const { currencyBalances } = action.payload
const newState = state.set('currencyBalances', currencyBalances)
return newState
},
[SET_CURRENT_CURRENCY]: (state: State, action: ActionType<Function>): State => {
const { currencyValueSelected } = action.payload
const newState = state.set('currencyValueSelected', currencyValueSelected)
return newState
},
},
Map(),
)

View File

@ -0,0 +1,9 @@
// @flow
import { List } from 'immutable'
import { type GlobalState } from '~/store'
import { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
export const currencyValuesListSelector = (state: GlobalState) => (state[CURRENCY_VALUES_KEY].get('currencyBalances') ? state[CURRENCY_VALUES_KEY].get('currencyBalances') : List([]))
export const currentCurrencySelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyValueSelected')

View File

@ -2,7 +2,6 @@
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type Operation } from '~/logic/safe/transactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
export const CALL = 0
export const TX_TYPE_EXECUTION = 'execution'

View File

@ -4,6 +4,8 @@ import { type Token } from '~/logic/tokens/store/model/token'
import { buildOrderFieldFrom, FIXED, type SortRow } from '~/components/Table/sorting'
import { type Column } from '~/components/Table/TableHead'
import { formatAmount } from '~/logic/tokens/utils/formatAmount'
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
export const BALANCE_TABLE_ASSET_ID = 'asset'
export const BALANCE_TABLE_BALANCE_ID = 'balance'
@ -16,13 +18,29 @@ type BalanceData = {
export type BalanceRow = SortRow<BalanceData>
export const getBalanceData = (activeTokens: List<Token>): List<BalanceRow> => {
// eslint-disable-next-line max-len
const getTokenPriceInCurrency = (token: Token, currencySelected: AVAILABLE_CURRENCIES, currencyValues: List<BalanceCurrencyType>): string => {
// eslint-disable-next-line no-restricted-syntax
for (const tokenPriceIterator of currencyValues) {
const { tokenAddress, balanceInSelectedCurrency, currencyName } = tokenPriceIterator
if (token.address === tokenAddress && currencySelected === currencyName) {
const balance = balanceInSelectedCurrency ? parseFloat(balanceInSelectedCurrency, 10).toFixed(2) : balanceInSelectedCurrency
return `${balance} ${currencySelected}`
}
}
return null
}
// eslint-disable-next-line max-len
export const getBalanceData = (activeTokens: List<Token>, currencySelected: string, currencyValues: List<BalanceCurrencyType>): List<BalanceRow> => {
const rows = activeTokens.map((token: Token) => ({
[BALANCE_TABLE_ASSET_ID]: { name: token.name, logoUri: token.logoUri, address: token.address },
[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',
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues),
}))
return rows
@ -56,5 +74,26 @@ export const generateColumns = () => {
static: true,
}
return List([assetColumn, balanceColumn, actions])
const value: Column = {
id: BALANCE_TABLE_VALUE_ID,
order: false,
label: 'Value',
custom: false,
static: true,
style: {
fontSize: '11px',
color: '#5d6d74',
borderBottomWidth: '2px',
width: '125px',
fontFamily: 'Averta',
fontWeight: 'normal',
fontStyle: 'normal',
textAlign: 'right',
},
}
return List([assetColumn, balanceColumn, value, actions])
}
// eslint-disable-next-line max-len
export const filterByZero = (data: List<BalanceRow>, hideZero: boolean): List<BalanceRow> => data.filter((row: BalanceRow) => (hideZero ? row[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)] !== 0 : true))

View File

@ -23,6 +23,9 @@ import Tokens from './Tokens'
import SendModal from './SendModal'
import Receive from './Receive'
import { styles } from './style'
import DropdownCurrency from '~/routes/safe/components/DropdownCurrency'
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
import { BALANCE_TABLE_BALANCE_ID, BALANCE_TABLE_VALUE_ID } from '~/routes/safe/components/Balances/dataFetcher'
export const MANAGE_TOKENS_BUTTON_TEST_ID = 'manage-tokens-btn'
export const BALANCE_ROW_TEST_ID = 'balance-row'
@ -45,6 +48,9 @@ type Props = {
safeName: string,
ethBalance: string,
createTransaction: Function,
currencySelected: string,
fetchCurrencyValues: Function,
currencyValues: BalanceCurrencyType[],
}
type Action = 'Token' | 'Send' | 'Receive'
@ -64,7 +70,8 @@ class Balances extends React.Component<Props, State> {
}
componentDidMount(): void {
const { activateTokensByBalance, safeAddress } = this.props
const { safeAddress, fetchCurrencyValues, activateTokensByBalance } = this.props
fetchCurrencyValues(safeAddress)
activateTokensByBalance(safeAddress)
}
@ -108,17 +115,20 @@ class Balances extends React.Component<Props, State> {
safeName,
ethBalance,
createTransaction,
currencySelected,
currencyValues,
} = this.props
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const filteredData = getBalanceData(activeTokens)
const filteredData = getBalanceData(activeTokens, currencySelected, currencyValues)
return (
<>
<Row align="center" className={classes.message}>
<Col xs={12} end="sm">
<DropdownCurrency />
<ButtonLink size="lg" onClick={this.onShow('Token')} testId="manage-tokens-btn">
Manage List
</ButtonLink>
@ -149,11 +159,42 @@ class Balances extends React.Component<Props, State> {
>
{(sortedData: Array<BalanceRow>) => sortedData.map((row: any, index: number) => (
<TableRow tabIndex={-1} key={index} className={classes.hide} data-testid={BALANCE_ROW_TEST_ID}>
{autoColumns.map((column: Column) => (
<TableCell key={column.id} style={cellWidth(column.width)} align={column.align} component="td">
{column.id === BALANCE_TABLE_ASSET_ID ? <AssetTableCell asset={row[column.id]} /> : row[column.id]}
</TableCell>
))}
{autoColumns.map((column: Column) => {
const { id, width, align } = column
let cellItem
switch (id) {
case BALANCE_TABLE_ASSET_ID: {
cellItem = <AssetTableCell asset={row[id]} />
break
}
case BALANCE_TABLE_BALANCE_ID: {
cellItem = (
<div>
{row[id]}
</div>
)
break
}
case BALANCE_TABLE_VALUE_ID: {
cellItem = <div className={classes.currencyValueRow}>{row[id]}</div>
break
}
default: {
cellItem = null
break
}
}
return (
<TableCell
key={id}
style={cellWidth(width)}
align={align}
component="td"
>
{cellItem}
</TableCell>
)
})}
<TableCell component="td">
<Row align="end" className={classes.actions}>
{granted && (

View File

@ -63,4 +63,8 @@ export const styles = (theme: Object) => ({
cursor: 'pointer',
},
},
currencyValueRow: {
maxWidth: '125px',
textAlign: 'right',
},
})

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 18 14">
<path fill="#008C73" fill-rule="evenodd" d="M5.6 10.6L1.4 6.4 0 7.8l5.6 5.6 12-12L16.2 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@ -0,0 +1,124 @@
// @flow
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem'
import React, { useState } from 'react'
import style from 'currency-flags/dist/currency-flags.min.css'
import { MuiThemeProvider } from '@material-ui/core/styles'
import { useDispatch, useSelector } from 'react-redux'
import SearchIcon from '@material-ui/icons/Search'
import InputBase from '@material-ui/core/InputBase'
import classNames from 'classnames'
import { DropdownListTheme } from '~/theme/mui'
import CheckIcon from './img/check.svg'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import fetchCurrencySelectedValue from '~/logic/currencyValues/store/actions/fetchCurrencySelectedValue'
import { currentCurrencySelector } from '~/logic/currencyValues/store/selectors'
import { useDropdownStyles } from '~/routes/safe/components/DropdownCurrency/style'
import saveCurrencySelected from '~/logic/currencyValues/store/actions/saveCurrencySelected'
const DropdownCurrency = () => {
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
const dispatch = useDispatch()
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const currencyValueSelected = useSelector(currentCurrencySelector)
const [searchParams, setSearchParams] = useState('')
const classes = useDropdownStyles()
const currenciesListFiltered = currenciesList.filter((currency) => currency.toLowerCase().includes(searchParams.toLowerCase()))
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const onCurrentCurrencyChangedHandler = (newCurrencySelectedName: AVAILABLE_CURRENCIES) => {
dispatch(fetchCurrencySelectedValue(newCurrencySelectedName))
dispatch(saveCurrencySelected(newCurrencySelectedName))
handleClose()
}
return (
!currencyValueSelected ? null
: (
<MuiThemeProvider theme={DropdownListTheme}>
<>
<button
className={classes.button}
onClick={handleClick}
type="button"
>
<span className={classNames(classes.buttonInner, anchorEl && classes.openMenuButton)}>
{currencyValueSelected}
</span>
</button>
<Menu
anchorEl={anchorEl}
elevation={0}
getContentAnchorEl={null}
id="customizedMenu"
keepMounted
onClose={handleClose}
open={Boolean(anchorEl)}
rounded={0}
anchorOrigin={{
horizontal: 'center',
vertical: 'bottom',
}}
transformOrigin={{
horizontal: 'center',
vertical: 'top',
}}
>
<MenuItem
className={classes.listItemSearch}
key="0"
>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
inputProps={{ 'aria-label': 'search' }}
onChange={(event) => setSearchParams(event.target.value)}
value={searchParams}
/>
</div>
</MenuItem>
<div className={classes.dropdownItemsScrollWrapper}>
{currenciesListFiltered.map((currencyName) => (
<MenuItem
className={classes.listItem}
key={currencyName}
value={currencyName}
onClick={() => onCurrentCurrencyChangedHandler(currencyName)}
>
<ListItemIcon className={classes.iconLeft}>
<div
className={classNames(classes.localFlag, style['currency-flag'], style['currency-flag-lg'], style[`currency-flag-${currencyName.toLowerCase()}`])}
/>
</ListItemIcon>
<ListItemText primary={currencyName} />
{currencyName === currencyValueSelected
? <ListItemIcon className={classes.iconRight}><img src={CheckIcon} alt="checked" /></ListItemIcon> : null}
</MenuItem>
))}
</div>
</Menu>
</>
</MuiThemeProvider>
)
)
}
export default DropdownCurrency

View File

@ -0,0 +1,119 @@
// @flow
import { makeStyles } from '@material-ui/core/styles'
const buttonWidth = '140px'
export const useDropdownStyles = makeStyles({
listItem: {
maxWidth: buttonWidth,
boxSizing: 'border-box',
},
listItemSearch: {
maxWidth: buttonWidth,
padding: '0',
boxSizing: 'border-box',
},
localFlag: {
backgroundPosition: '50% 50%',
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',
height: '20px !important',
width: '26px !important',
},
iconLeft: {
marginRight: '10px',
},
iconRight: {
marginLeft: '18px',
},
button: {
backgroundColor: '#e8e7e6',
border: 'none',
borderRadius: '3px',
boxSizing: 'border-box',
color: '#5d6d74',
cursor: 'pointer',
fontSize: '12px',
fontWeight: 'normal',
height: '24px',
lineHeight: '1.33',
marginRight: '20px',
minWidth: buttonWidth,
outline: 'none',
padding: '0',
textAlign: 'left',
'&:active': {
opacity: '0.8',
},
},
buttonInner: {
boxSizing: 'border-box',
display: 'block',
height: '100%',
lineHeight: '24px',
padding: '0 22px 0 8px',
position: 'relative',
width: '100%',
'&::after': {
borderLeft: '5px solid transparent',
borderRight: '5px solid transparent',
borderTop: '5px solid #5d6d74',
content: '""',
height: '0',
position: 'absolute',
right: '8px',
top: '9px',
width: '0',
},
},
openMenuButton: {
'&::after': {
borderBottom: '5px solid #5d6d74',
borderLeft: '5px solid transparent',
borderRight: '5px solid transparent',
borderTop: 'none',
},
},
dropdownItemsScrollWrapper: {
maxHeight: '280px',
overflow: 'auto',
},
search: {
position: 'relative',
borderRadius: '0',
backgroundColor: '#fff',
'&:hover': {
backgroundColor: '#fff',
},
marginRight: 0,
width: '100%',
},
searchIcon: {
alignItems: 'center',
display: 'flex',
height: '100%',
justifyContent: 'center',
left: '12px',
margin: '0',
pointerEvents: 'none',
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
width: '18px',
'& path': {
fill: '#b2b5b2',
},
},
inputRoot: {
color: '#5d6d74',
fontSize: '14px',
fontWeight: 'normal',
lineHeight: '1.43',
width: '100%',
},
inputInput: {
boxSizing: 'border-box',
height: '44px',
padding: '12px 12px 12px 40px',
width: '100%',
},
})

View File

@ -49,6 +49,7 @@ type Props = SelectorProps &
match: Object,
location: Object,
history: Object,
fetchCurrencyValues: Function,
}
const Layout = (props: Props) => {
@ -76,6 +77,9 @@ const Layout = (props: Props) => {
hideSendFunds,
match,
location,
currencySelected,
fetchCurrencyValues,
currencyValues,
} = props
const handleCallToRouter = (_, value) => {
@ -165,6 +169,9 @@ const Layout = (props: Props) => {
fetchTokens={fetchTokens}
safeName={name}
createTransaction={createTransaction}
currencySelected={currencySelected}
fetchCurrencyValues={fetchCurrencyValues}
currencyValues={currencyValues}
/>
)}
/>

View File

@ -7,6 +7,7 @@ import processTransaction from '~/routes/safe/store/actions/processTransaction'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
import updateSafe from '~/routes/safe/store/actions/updateSafe'
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
import fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance'
export type Actions = {
@ -19,7 +20,8 @@ export type Actions = {
processTransaction: typeof processTransaction,
fetchEtherBalance: typeof fetchEtherBalance,
activateTokensByBalance: typeof activateTokensByBalance,
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe,
fetchCurrencyValues: typeof fetchCurrencyValues
}
export default {
@ -32,5 +34,6 @@ export default {
activateTokensByBalance,
updateSafe,
fetchEtherBalance,
fetchCurrencyValues,
checkAndUpdateSafeOwners: checkAndUpdateSafe,
}

View File

@ -34,7 +34,7 @@ class SafeView extends React.Component<Props, State> {
componentDidMount() {
const {
fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens, fetchTransactions,
fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens, fetchTransactions, fetchCurrencyValues,
} = this.props
fetchSafe(safeUrl).then(() => {
@ -44,6 +44,7 @@ class SafeView extends React.Component<Props, State> {
fetchTokenBalances(safeUrl, activeTokens)
// fetch tokens there to get symbols for tokens in TXs list
fetchTokens()
fetchCurrencyValues(safeUrl)
this.intervalId = setInterval(() => {
this.checkForUpdates()
@ -125,6 +126,9 @@ class SafeView extends React.Component<Props, State> {
fetchTokens,
updateSafe,
transactions,
currencySelected,
fetchCurrencyValues,
currencyValues,
} = this.props
return (
@ -150,6 +154,9 @@ class SafeView extends React.Component<Props, State> {
onHide={this.onHide}
showSendFunds={this.showSendFunds}
hideSendFunds={this.hideSendFunds}
currencySelected={currencySelected}
fetchCurrencyValues={fetchCurrencyValues}
currencyValues={currencyValues}
/>
</Page>
)

View File

@ -21,6 +21,8 @@ import { type Token } from '~/logic/tokens/store/model/token'
import { type Transaction, type TransactionStatus } from '~/routes/safe/store/models/transaction'
import { safeParamAddressSelector } from '../store/selectors'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
import { currencyValuesListSelector, currentCurrencySelector } from '~/logic/currencyValues/store/selectors'
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
export type SelectorProps = {
@ -32,6 +34,8 @@ export type SelectorProps = {
userAddress: string,
network: string,
safeUrl: string,
currencySelected: string,
currencyValues: BalanceCurrencyType[],
transactions: List<Transaction | IncomingTransaction>,
}
@ -154,4 +158,6 @@ export default createStructuredSelector<Object, *>({
network: networkSelector,
safeUrl: safeParamAddressSelector,
transactions: extendedTransactionsSelector,
currencySelected: currentCurrencySelector,
currencyValues: currencyValuesListSelector,
})

View File

@ -102,35 +102,13 @@ const createTransaction = ({
try {
if (isExecution) {
tx = await getExecutionTransaction(
safeInstance,
to,
valueInWei,
txData,
CALL,
nonce,
0,
0,
0,
ZERO_ADDRESS,
ZERO_ADDRESS,
from,
sigs,
safeInstance, to, valueInWei, txData, CALL, nonce,
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs,
)
} else {
tx = await getApprovalTransaction(
safeInstance,
to,
valueInWei,
txData,
CALL,
nonce,
0,
0,
0,
ZERO_ADDRESS,
ZERO_ADDRESS,
from,
sigs,
safeInstance, to, valueInWei, txData, CALL, nonce,
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs,
)
}

View File

@ -119,9 +119,7 @@ export default handleActions<SafeReducerState, *>(
[UPDATE_SAFE_THRESHOLD]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
const { safeAddress, threshold } = action.payload
return state.updateIn(['safes', safeAddress], (prevSafe) => {
return prevSafe.set('threshold', threshold)
})
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set('threshold', threshold))
},
[SET_DEFAULT_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => state.set('defaultSafe', action.payload),
},

View File

@ -22,6 +22,7 @@ import notifications, {
NOTIFICATIONS_REDUCER_ID,
type NotificationReducerState as NotificationsState,
} from '~/logic/notifications/store/reducer/notifications'
import currencyValues, { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
import cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies'
import notificationsMiddleware from '~/routes/safe/store/middleware/notificationsMiddleware'
@ -53,6 +54,7 @@ const reducers: Reducer<GlobalState> = combineReducers({
[TRANSACTIONS_REDUCER_ID]: transactions,
[INCOMING_TRANSACTIONS_REDUCER_ID]: incomingTransactions,
[NOTIFICATIONS_REDUCER_ID]: notifications,
[CURRENCY_VALUES_KEY]: currencyValues,
[COOKIES_REDUCER_ID]: cookies,
})

View File

@ -43,7 +43,7 @@ const palette = {
// see https://material-ui-next.com/customization/themes/
// see https://github.com/mui-org/material-ui/blob/v1-beta/src/styles/createMuiTheme.js
export default createMuiTheme({
const theme = createMuiTheme({
typography: {
fontFamily: mainFontFamily,
useNextVariants: true,
@ -342,3 +342,43 @@ export default createMuiTheme({
},
palette,
})
export default theme
export const DropdownListTheme = {
...theme,
overrides: {
...theme.overrides,
MuiPaper: {
root: {
marginTop: '10px',
},
elevation0: {
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
},
rounded: {
borderRadius: '4px',
},
},
MuiList: {
padding: {
paddingBottom: '0',
paddingTop: '0',
},
},
MuiListItem: {
root: {
borderBottom: '2px solid #e8e7e6',
'&:last-child': {
borderBottom: 'none',
},
boxSizing: 'border-box',
},
button: {
'&:hover': {
backgroundColor: '#fff3e2',
},
},
},
},
}