* 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:
parent
d69e5fca7f
commit
63c1153772
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 }))
|
|
@ -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 }))
|
|
@ -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>
|
|
@ -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(),
|
||||
)
|
|
@ -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')
|
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -63,4 +63,8 @@ export const styles = (theme: Object) => ({
|
|||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
currencyValueRow: {
|
||||
maxWidth: '125px',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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 |
|
@ -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
|
|
@ -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%',
|
||||
},
|
||||
})
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue