Equivalent Values for Tokens (#366)

* Add select with tokens.

* Reformat state shape to allow multiple currency rates. Show those rates when selected.

* Add loader
This commit is contained in:
William O'Beirne 2017-11-14 22:51:09 -07:00 committed by Daniel Ternyak
parent 151b2d762f
commit 8bc1a348c7
8 changed files with 167 additions and 67 deletions

View File

@ -3,9 +3,9 @@ import { TypeKeys } from './constants';
import { fetchRates } from './actionPayloads'; import { fetchRates } from './actionPayloads';
export type TFetchCCRates = typeof fetchCCRates; export type TFetchCCRates = typeof fetchCCRates;
export function fetchCCRates(): interfaces.FetchCCRates { export function fetchCCRates(symbol: string): interfaces.FetchCCRates {
return { return {
type: TypeKeys.RATES_FETCH_CC, type: TypeKeys.RATES_FETCH_CC,
payload: fetchRates() payload: fetchRates(symbol)
}; };
} }

View File

@ -1,22 +1,31 @@
import { handleJSONResponse } from 'api/utils'; import { handleJSONResponse } from 'api/utils';
export const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP']; export const rateSymbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH'];
const symbolsURL = symbols.join(','); const rateSymbolsArg = rateSymbols.join(',');
// TODO - internationalize // TODO - internationalize
const ERROR_MESSAGE = 'Could not fetch rate data.'; const ERROR_MESSAGE = 'Could not fetch rate data.';
const CCApi = 'https://min-api.cryptocompare.com'; const CCApi = 'https://min-api.cryptocompare.com';
const CCRates = CCSymbols => `${CCApi}/data/price?fsym=ETH&tsyms=${CCSymbols}`; const CCRates = (symbol: string) => {
return `${CCApi}/data/price?fsym=${symbol}&tsyms=${rateSymbolsArg}`;
};
export interface CCResponse { export interface CCResponse {
BTC: number; symbol: string;
EUR: number; rates: {
GBP: number; BTC: number;
CHF: number; EUR: number;
REP: number; GBP: number;
CHF: number;
REP: number;
ETH: number;
};
} }
export const fetchRates = (): Promise<CCResponse> => export const fetchRates = (symbol: string): Promise<CCResponse> =>
fetch(CCRates(symbolsURL)).then(response => fetch(CCRates(symbol))
handleJSONResponse(response, ERROR_MESSAGE) .then(response => handleJSONResponse(response, ERROR_MESSAGE))
); .then(rates => ({
symbol,
rates
}));

View File

@ -1,4 +1,3 @@
import { TFetchCCRates } from 'actions/rates';
import { Identicon, UnitDisplay } from 'components/ui'; import { Identicon, UnitDisplay } from 'components/ui';
import { NetworkConfig } from 'config/data'; import { NetworkConfig } from 'config/data';
import { IWallet, Balance } from 'libs/wallet'; import { IWallet, Balance } from 'libs/wallet';
@ -11,7 +10,6 @@ interface Props {
balance: Balance; balance: Balance;
wallet: IWallet; wallet: IWallet;
network: NetworkConfig; network: NetworkConfig;
fetchCCRates: TFetchCCRates;
} }
interface State { interface State {
@ -32,7 +30,6 @@ export default class AccountInfo extends React.Component<Props, State> {
} }
public componentDidMount() { public componentDidMount() {
this.props.fetchCCRates();
this.setAddressFromWallet(); this.setAddressFromWallet();
} }

View File

@ -34,5 +34,10 @@
@include mono; @include mono;
} }
} }
&-loader {
padding: 25px 0;
text-align: center;
}
} }
} }

View File

@ -1,56 +1,139 @@
import React from 'react'; import * as React from 'react';
import translate from 'translations'; import translate from 'translations';
import './EquivalentValues.scss';
import { State } from 'reducers/rates'; import { State } from 'reducers/rates';
import { symbols } from 'actions/rates'; import { rateSymbols, TFetchCCRates } from 'actions/rates';
import { UnitDisplay } from 'components/ui'; import { TokenBalance } from 'selectors/wallet';
import { Balance } from 'libs/wallet'; import { Balance } from 'libs/wallet';
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import UnitDisplay from 'components/ui/UnitDisplay';
import './EquivalentValues.scss';
interface Props { interface Props {
balance: Balance; balance?: Balance;
rates?: State['rates']; tokenBalances?: TokenBalance[];
rates: State['rates'];
ratesError?: State['ratesError']; ratesError?: State['ratesError'];
fetchCCRates: TFetchCCRates;
} }
export default class EquivalentValues extends React.Component<Props, {}> { interface CmpState {
currency: string;
}
export default class EquivalentValues extends React.Component<Props, CmpState> {
public state = {
currency: 'ETH'
};
private balanceLookup = {};
public constructor(props) {
super(props);
this.makeBalanceLookup(props);
}
public componentDidMount() {
this.props.fetchCCRates(this.state.currency);
}
public componentWillReceiveProps(nextProps) {
const { balance, tokenBalances } = this.props;
if (
nextProps.balance !== balance ||
nextProps.tokenBalances !== tokenBalances
) {
this.makeBalanceLookup(nextProps);
}
}
public render() { public render() {
const { balance, rates, ratesError } = this.props; const { tokenBalances, rates, ratesError } = this.props;
const { currency } = this.state;
const balance = this.balanceLookup[currency];
let values;
if (balance && rates && rates[currency]) {
values = rateSymbols.map(key => {
if (!rates[currency][key] || key === currency) {
return null;
}
return (
<li className="EquivalentValues-values-currency" key={key}>
<span className="EquivalentValues-values-currency-label">
{key}:
</span>{' '}
<span className="EquivalentValues-values-currency-value">
{balance.isPending ? (
<Spinner />
) : (
<UnitDisplay
unit={'ether'}
value={balance ? balance.muln(rates[currency][key]) : null}
displayShortBalance={2}
/>
)}
</span>
</li>
);
});
} else if (ratesError) {
values = <h5>{ratesError}</h5>;
} else {
values = (
<div className="EquivalentValues-values-loader">
<Spinner size="x3" />
</div>
);
}
return ( return (
<div className="EquivalentValues"> <div className="EquivalentValues">
<h5 className="EquivalentValues-title">{translate('sidebar_Equiv')}</h5> <h5 className="EquivalentValues-title">
{translate('sidebar_Equiv')} for{' '}
<ul className="EquivalentValues-values"> <select
{rates className="EquivalentValues-title-symbol"
? symbols.map(key => { onChange={this.changeCurrency}
if (!rates[key]) { value={currency}
return null; >
<option value="ETH">ETH</option>
{tokenBalances &&
tokenBalances.map(tk => {
if (!tk.balance || tk.balance.isZero()) {
return;
} }
const sym = tk.symbol;
return ( return (
<li className="EquivalentValues-values-currency" key={key}> <option key={sym} value={sym}>
<span className="EquivalentValues-values-currency-label"> {sym}
{key + ': '} </option>
</span>
<span className="EquivalentValues-values-currency-value">
{balance.isPending ? (
<Spinner />
) : (
<UnitDisplay
unit={'ether'}
value={
balance.wei ? balance.wei.muln(rates[key]) : null
}
displayShortBalance={2}
/>
)}
</span>
</li>
); );
}) })}
: ratesError && <h5>{ratesError}</h5>} </select>
</ul> </h5>
<ul className="EquivalentValues-values">{values}</ul>
</div> </div>
); );
} }
private changeCurrency = (ev: React.FormEvent<HTMLSelectElement>) => {
const currency = ev.currentTarget.value;
this.setState({ currency });
if (!this.props.rates || !this.props.rates[currency]) {
this.props.fetchCCRates(currency);
}
};
private makeBalanceLookup(props: Props) {
const tokenBalances = props.tokenBalances || [];
this.balanceLookup = tokenBalances.reduce(
(prev, tk) => {
return {
...prev,
[tk.symbol]: tk.balance
};
},
{ ETH: props.balance && props.balance.wei }
);
}
} }

View File

@ -66,12 +66,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
{ {
name: 'Account Info', name: 'Account Info',
content: ( content: (
<AccountInfo <AccountInfo wallet={wallet} balance={balance} network={network} />
wallet={wallet}
balance={balance}
network={network}
fetchCCRates={fetchCCRates}
/>
) )
}, },
{ {
@ -94,8 +89,10 @@ export class BalanceSidebar extends React.Component<Props, {}> {
content: ( content: (
<EquivalentValues <EquivalentValues
balance={balance} balance={balance}
tokenBalances={tokenBalances}
rates={rates} rates={rates}
ratesError={ratesError} ratesError={ratesError}
fetchCCRates={fetchCCRates}
/> />
) )
} }

View File

@ -66,7 +66,9 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{/*Amount to send*/} {/*Amount to send*/}
{!this.isExpanded() && ( {!this.isExpanded() && (
<div className={this.computedClass()}> <div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">{` ${originAmount} ${originKind}`}</h3> <h3 className="SwapInfo-details-block-value">{` ${originAmount} ${
originKind
}`}</h3>
<p className="SwapInfo-details-block-label"> <p className="SwapInfo-details-block-label">
{translate('SEND_amount')} {translate('SEND_amount')}
</p> </p>
@ -109,9 +111,9 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
<div className={this.computedClass()}> <div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value"> <h3 className="SwapInfo-details-block-value">
{`${computedOriginDestinationRatio && {`${computedOriginDestinationRatio &&
toFixedIfLarger( toFixedIfLarger(computedOriginDestinationRatio)} ${
computedOriginDestinationRatio destinationKind
)} ${destinationKind}/${originKind}`} }/${originKind}`}
</h3> </h3>
<p className="SwapInfo-details-block-label"> <p className="SwapInfo-details-block-label">
{translate('SWAP_your_rate')} {translate('SWAP_your_rate')}

View File

@ -1,20 +1,27 @@
import { FetchCCRatesSucceeded, RatesAction, CCResponse } from 'actions/rates'; import { FetchCCRatesSucceeded, RatesAction, CCResponse } from 'actions/rates';
import { TypeKeys } from 'actions/rates/constants'; import { TypeKeys } from 'actions/rates/constants';
import { Optional } from 'utils/types';
// SYMBOL -> PRICE TO BUY 1 ETH // SYMBOL -> PRICE TO BUY 1 ETH
export interface State { export interface State {
rates?: Optional<CCResponse>; rates: { [symbol: string]: CCResponse['rates'] };
ratesError?: string | null; ratesError?: string | null;
} }
export const INITIAL_STATE: State = {}; export const INITIAL_STATE: State = {
rates: {}
};
function fetchCCRatesSucceeded( function fetchCCRatesSucceeded(
state: State, state: State,
action: FetchCCRatesSucceeded action: FetchCCRatesSucceeded
): State { ): State {
return { ...state, rates: action.payload }; return {
...state,
rates: {
...state.rates,
[action.payload.symbol]: action.payload.rates
}
};
} }
function fetchCCRatesFailed(state: State): State { function fetchCCRatesFailed(state: State): State {