diff --git a/common/actions/rates/actionPayloads.ts b/common/actions/rates/actionPayloads.ts index 123e1fe0..95837b1b 100644 --- a/common/actions/rates/actionPayloads.ts +++ b/common/actions/rates/actionPayloads.ts @@ -1,14 +1,56 @@ import { handleJSONResponse } from 'api/utils'; +interface IRateSymbols { + symbols: { + all: TAllSymbols; + fiat: TFiatSymbols; + coinAndToken: TCoinAndTokenSymbols; + }; + isFiat: isFiat; +} -export const rateSymbols: Symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH']; +type isFiat = (rate: string) => boolean; + +export type TAllSymbols = (keyof ISymbol)[]; +export type TFiatSymbols = (keyof IFiatSymbols)[]; +export type TCoinAndTokenSymbols = (keyof ICoinAndTokenSymbols)[]; +interface ISymbol { + USD: number; + EUR: number; + GBP: number; + CHF: number; + BTC: number; + ETH: number; + REP: number; +} +interface IFiatSymbols { + USD: number; + EUR: number; + GBP: number; + CHF: number; +} +interface ICoinAndTokenSymbols { + BTC: number; + ETH: number; + REP: number; +} + +const fiat: TFiatSymbols = ['USD', 'EUR', 'GBP', 'CHF']; +const coinAndToken: TCoinAndTokenSymbols = ['BTC', 'ETH', 'REP']; +export const rateSymbols: IRateSymbols = { + symbols: { + all: [...fiat, ...coinAndToken], + fiat, + coinAndToken + }, + isFiat: (rate: string) => (fiat as string[]).includes(rate) +}; -export type Symbols = (keyof ISymbol)[]; // TODO - internationalize const ERROR_MESSAGE = 'Could not fetch rate data.'; const CCApi = 'https://min-api.cryptocompare.com'; const CCRates = (symbols: string[]) => { - const tsyms = rateSymbols.concat(symbols as any).join(','); + const tsyms = rateSymbols.symbols.all.concat(symbols as any).join(','); return `${CCApi}/data/price?fsym=ETH&tsyms=${tsyms}`; }; @@ -16,16 +58,6 @@ export interface CCResponse { [symbol: string]: ISymbol; } -interface ISymbol { - USD: number; - EUR: number; - GBP: number; - BTC: number; - CHF: number; - REP: number; - ETH: number; -} - interface IRates extends ISymbol { Response?: 'Error'; } @@ -45,7 +77,7 @@ export const fetchRates = (symbols: string[] = []): Promise => return symbols.reduce( (eqRates, sym: keyof ISymbol) => { if (rates[sym]) { - eqRates[sym] = rateSymbols.reduce( + eqRates[sym] = rateSymbols.symbols.all.reduce( (symRates, rateSym) => { symRates[rateSym] = 1 / rates[sym] * rates[rateSym]; return symRates; @@ -60,10 +92,10 @@ export const fetchRates = (symbols: string[] = []): Promise => USD: rates.USD, EUR: rates.EUR, GBP: rates.GBP, - BTC: rates.BTC, CHF: rates.CHF, - REP: rates.REP, - ETH: 1 + BTC: rates.BTC, + ETH: 1, + REP: rates.REP } } as CCResponse ); diff --git a/common/components/BalanceSidebar/EquivalentValues.scss b/common/components/BalanceSidebar/EquivalentValues.scss index 9e4256fb..be76b0f5 100644 --- a/common/components/BalanceSidebar/EquivalentValues.scss +++ b/common/components/BalanceSidebar/EquivalentValues.scss @@ -2,43 +2,44 @@ @import 'common/sass/mixins'; .EquivalentValues { - &-title { - margin-top: 0; + &-header { + display: flex; + align-items: center; margin-bottom: $space; + + .Select { + flex-grow: 1; + } + } + + .Spinner { + display: block; + margin: auto; + } + + &-title { + margin: 0; + margin-right: 16px; } &-values { - list-style: none; - padding: 0; - @include clearfix; - + display: flex; + flex-wrap: wrap; &-currency { - float: left; width: 50%; margin-bottom: $space-xs; - - &:nth-child(odd) { - padding-right: $space-sm; - } - &:nth-child(even) { - padding-left: $space-sm; - } - &-label { white-space: pre-wrap; display: inline-block; min-width: 36px; + opacity: 0.54; + margin-right: 8px; } &-value { font-weight: 600; @include mono; } } - - &-loader { - padding: 25px 0; - text-align: center; - } } &-offline { diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index 0a3add2a..5f2f8d86 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -1,177 +1,234 @@ -import * as React from 'react'; -import BN from 'bn.js'; +import React from 'react'; import translate from 'translations'; -import { State } from 'reducers/rates'; -import { rateSymbols, TFetchCCRates } from 'actions/rates'; +import { UnitDisplay, Spinner } from 'components/ui'; +import Select from 'react-select'; +import { TFetchCCRates, rateSymbols } from 'actions/rates'; +import { chain, flatMap } from 'lodash'; +import { State as RatesState } from 'reducers/rates'; import { TokenBalance } from 'selectors/wallet'; import { Balance } from 'libs/wallet'; import { NetworkConfig } from 'config'; -import { ETH_DECIMAL, convertTokenBase } from 'libs/units'; -import Spinner from 'components/ui/Spinner'; -import UnitDisplay from 'components/ui/UnitDisplay'; import './EquivalentValues.scss'; +import { Wei } from 'libs/units'; -const ALL_OPTION = 'All'; +interface AllValue { + symbol: string; + balance: Balance['wei']; +} + +interface DefaultOption { + label: string; + value: AllValue[]; +} + +interface Option { + label: string; + value: Balance['wei'] | AllValue[]; +} + +interface State { + equivalentValues: Option; + options: Option[]; +} interface Props { - balance?: Balance; - tokenBalances?: TokenBalance[]; - rates: State['rates']; - ratesError?: State['ratesError']; + balance: Balance; + tokenBalances: TokenBalance[]; + rates: RatesState['rates']; fetchCCRates: TFetchCCRates; + ratesError: RatesState['ratesError']; network: NetworkConfig; - isOffline: boolean; + offline: boolean; } -interface CmpState { - currency: string; -} - -export default class EquivalentValues extends React.Component { - public state = { - currency: ALL_OPTION - }; - private balanceLookup: { [key: string]: Balance['wei'] | undefined } = {}; - private decimalLookup: { [key: string]: number } = {}; +class Equiv extends React.Component { private requestedCurrencies: string[] | null = null; - public constructor(props: Props) { super(props); - this.makeBalanceLookup(props); + const { balance, tokenBalances, network } = this.props; + this.state = { + equivalentValues: this.defaultOption(balance, tokenBalances, network), + options: [] + }; if (props.balance && props.tokenBalances) { this.fetchRates(props); } } + public defaultOption( + balance: Balance, + tokenBalances: TokenBalance[], + network: NetworkConfig + ): DefaultOption { + return { + label: 'All', + value: [{ symbol: network.unit, balance: balance.wei }, ...tokenBalances] + }; + } + public componentWillReceiveProps(nextProps: Props) { - const { balance, tokenBalances, isOffline } = this.props; + const { balance, tokenBalances, offline } = this.props; if ( nextProps.balance !== balance || nextProps.tokenBalances !== tokenBalances || - nextProps.isOffline !== isOffline + nextProps.offline !== offline ) { - this.makeBalanceLookup(nextProps); + const defaultOption = this.defaultOption( + nextProps.balance, + nextProps.tokenBalances, + nextProps.network + ); + const options: Option[] = [ + defaultOption, + { label: nextProps.network.unit, value: nextProps.balance.wei }, + ...Object.values(nextProps.tokenBalances).map(token => { + return { label: token.symbol, value: token.balance }; + }) + ]; + const equivalentValues = + options.find(opt => opt.label === this.state.equivalentValues.label) || defaultOption; + this.setState({ + equivalentValues, + options + }); this.fetchRates(nextProps); } } - public render() { - const { balance, tokenBalances, rates, ratesError, isOffline, network } = this.props; - const { currency } = this.state; + public selectOption = equivalentValues => { + this.setState({ equivalentValues }); + }; - // There are a bunch of reasons why the incorrect balances might be rendered - // while we have incomplete data that's being fetched. + public render(): JSX.Element { + const { balance, offline, tokenBalances, rates, network, ratesError } = this.props; + const { equivalentValues, options } = this.state; const isFetching = !balance || balance.isPending || !tokenBalances || Object.keys(rates).length === 0; - // Currency exists in rates or the all option is selected - const rateExistsOrAll = rates[currency] || currency === ALL_OPTION; - let valuesEl; - if (!isFetching && rateExistsOrAll && !network.isTestnet) { - const values = this.getEquivalentValues(currency); - valuesEl = rateSymbols.map(key => { - if (!values[key] || key === currency) { - return null; - } - - return ( -
  • - {key}:{' '} - - - -
  • - ); - }); - } else if (network.isTestnet) { - valuesEl = ( -
    -
    - On test network, equivalent values will not be displayed. -
    -
    - ); - } else if (ratesError) { - valuesEl =
    {ratesError}
    ; - } else if (tokenBalances && tokenBalances.length === 0) { - valuesEl =
    No tokens found!
    ; - } else { - valuesEl = ( -
    - -
    - ); - } + const Value = ({ rate, value }) => ( +
    + {rate}{' '} + + + +
    + ); return (
    -
    - {translate('sidebar_Equiv')} for{' '} - -
    +
    +
    {translate('sidebar_Equiv')}
    +