Alerting and UX Improvements. (#185)
* Remove unused imports. * Create and use .toPrecision forwarding method for `Unit` * Error handling when unlocking trezor devices. * Use translateRaw to fulfill string req; * - Refactor rates actions and action creators to use standard network request state pattern (REQUESTED / SUCCE - Only Request Rates once AccountInfo Component has mounted, instead of upon saga instantiation (uneeded overhead). This allows also us to issue subsequent fiat rates requests to update the "equivalent values" should the users session persist. - Show '???' as account balance when balance is null - Wallet initial state with balance as null instead of 0. We don't actually know what the balance is, and we shouldn't have 0 as a default as this may confuse users and doesn't accurately reflect their balance. * - Display 'No rates were loaded.' in EquivalentValues when rates are null, instead of nothing. - Remove unneeded imports. * Remove unneeded imports and reformat. * Fix error messaging (show error message instead of error Object) * remove console.log * inform flow how silly it is being * fix wallet test to reflect balance being null by default * figure out way to have flow understand that rates will not be undefined * open external links in new tab * handle case where balance is null in equivalanet values
This commit is contained in:
parent
f34811546a
commit
f3b85b2aae
|
@ -1,17 +1,29 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
export type FiatRequestedRatesAction = {
|
||||||
|
type: 'RATES_FIAT_REQUESTED'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fiatRequestedRates() {
|
||||||
|
return {
|
||||||
|
type: 'RATES_FIAT_REQUESTED'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/*** Set rates ***/
|
/*** Set rates ***/
|
||||||
export type SetRatesAction = {
|
export type FiatSucceededRatesAction = {
|
||||||
type: 'RATES_SET',
|
type: 'RATES_FIAT_SUCCEEDED',
|
||||||
payload: { [string]: number }
|
payload: { [string]: number }
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setRates(payload: { [string]: number }): SetRatesAction {
|
export function fiatSucceededRates(payload: {
|
||||||
|
[string]: number
|
||||||
|
}): FiatSucceededRatesAction {
|
||||||
return {
|
return {
|
||||||
type: 'RATES_SET',
|
type: 'RATES_FIAT_SUCCEEDED',
|
||||||
payload
|
payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Union Type ***/
|
/*** Union Type ***/
|
||||||
export type RatesAction = SetRatesAction;
|
export type RatesAction = FiatSucceededRatesAction | FiatRequestedRatesAction;
|
||||||
|
|
|
@ -7,11 +7,13 @@ import { formatNumber } from 'utils/formatters';
|
||||||
import type { IWallet } from 'libs/wallet';
|
import type { IWallet } from 'libs/wallet';
|
||||||
import type { NetworkConfig } from 'config/data';
|
import type { NetworkConfig } from 'config/data';
|
||||||
import { Ether } from 'libs/units';
|
import { Ether } from 'libs/units';
|
||||||
|
import type { FiatRequestedRatesAction } from 'actions/rates';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
balance: Ether,
|
balance: Ether,
|
||||||
wallet: IWallet,
|
wallet: IWallet,
|
||||||
network: NetworkConfig
|
network: NetworkConfig,
|
||||||
|
fiatRequestedRates: () => FiatRequestedRatesAction
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class AccountInfo extends React.Component {
|
export default class AccountInfo extends React.Component {
|
||||||
|
@ -23,6 +25,7 @@ export default class AccountInfo extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.props.fiatRequestedRates();
|
||||||
this.props.wallet.getAddress().then(addr => {
|
this.props.wallet.getAddress().then(addr => {
|
||||||
this.setState({ address: addr });
|
this.setState({ address: addr });
|
||||||
});
|
});
|
||||||
|
@ -67,11 +70,10 @@ export default class AccountInfo extends React.Component {
|
||||||
<span
|
<span
|
||||||
className="AccountInfo-list-item-clickable mono wrap"
|
className="AccountInfo-list-item-clickable mono wrap"
|
||||||
onClick={this.toggleShowLongBalance}
|
onClick={this.toggleShowLongBalance}
|
||||||
title={`${balance.toString()}`}
|
|
||||||
>
|
>
|
||||||
{this.state.showLongBalance
|
{this.state.showLongBalance
|
||||||
? balance.toString()
|
? balance ? balance.toString() : '???'
|
||||||
: formatNumber(balance.amount)}
|
: balance ? formatNumber(balance.amount) : '???'}
|
||||||
</span>
|
</span>
|
||||||
{` ${network.name}`}
|
{` ${network.name}`}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -2,16 +2,14 @@
|
||||||
import './EquivalentValues.scss';
|
import './EquivalentValues.scss';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { formatNumber } from 'utils/formatters';
|
import { formatNumber } from 'utils/formatters';
|
||||||
import type Big from 'bignumber.js';
|
|
||||||
import { Ether } from 'libs/units';
|
import { Ether } from 'libs/units';
|
||||||
|
|
||||||
const ratesKeys = ['BTC', 'REP', 'EUR', 'USD', 'GBP', 'CHF'];
|
const ratesKeys = ['BTC', 'REP', 'EUR', 'USD', 'GBP', 'CHF'];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
balance: Ether,
|
balance: ?Ether,
|
||||||
rates: { [string]: number }
|
rates: ?{ [string]: number }
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class EquivalentValues extends React.Component {
|
export default class EquivalentValues extends React.Component {
|
||||||
|
@ -27,19 +25,23 @@ export default class EquivalentValues extends React.Component {
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<ul className="EquivalentValues-values">
|
<ul className="EquivalentValues-values">
|
||||||
{ratesKeys.map(key => {
|
{rates
|
||||||
if (!rates[key]) return null;
|
? ratesKeys.map(key => {
|
||||||
return (
|
if (!rates[key]) return null;
|
||||||
<li className="EquivalentValues-values-currency" key={key}>
|
return (
|
||||||
<span className="EquivalentValues-values-currency-label">
|
<li className="EquivalentValues-values-currency" key={key}>
|
||||||
{key}:
|
<span className="EquivalentValues-values-currency-label">
|
||||||
</span>
|
{key}:
|
||||||
<span className="EquivalentValues-values-currency-value">
|
</span>
|
||||||
{' '}{formatNumber(balance.amount.times(rates[key]))}
|
<span className="EquivalentValues-values-currency-value">
|
||||||
</span>
|
{' '}{balance
|
||||||
</li>
|
? formatNumber(balance.amount.times(rates[key]))
|
||||||
);
|
: '???'}
|
||||||
})}
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: <h5>No rates were loaded.</h5>}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,6 +66,7 @@ export default class Promos extends React.Component {
|
||||||
? <a
|
? <a
|
||||||
className="Promos-promo"
|
className="Promos-promo"
|
||||||
key={promo.href}
|
key={promo.href}
|
||||||
|
target="_blank"
|
||||||
href={promo.href}
|
href={promo.href}
|
||||||
style={{ backgroundColor: promo.color }}
|
style={{ backgroundColor: promo.color }}
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import type { TokenBalance } from 'selectors/wallet';
|
||||||
import { getNetworkConfig } from 'selectors/config';
|
import { getNetworkConfig } from 'selectors/config';
|
||||||
import * as customTokenActions from 'actions/customTokens';
|
import * as customTokenActions from 'actions/customTokens';
|
||||||
import { showNotification } from 'actions/notifications';
|
import { showNotification } from 'actions/notifications';
|
||||||
|
import { fiatRequestedRates } from 'actions/rates';
|
||||||
|
import type { FiatRequestedRatesAction } from 'actions/rates';
|
||||||
|
|
||||||
import AccountInfo from './AccountInfo';
|
import AccountInfo from './AccountInfo';
|
||||||
import Promos from './Promos';
|
import Promos from './Promos';
|
||||||
|
@ -24,14 +26,22 @@ type Props = {
|
||||||
rates: { [string]: number },
|
rates: { [string]: number },
|
||||||
showNotification: Function,
|
showNotification: Function,
|
||||||
addCustomToken: typeof customTokenActions.addCustomToken,
|
addCustomToken: typeof customTokenActions.addCustomToken,
|
||||||
removeCustomToken: typeof customTokenActions.removeCustomToken
|
removeCustomToken: typeof customTokenActions.removeCustomToken,
|
||||||
|
fiatRequestedRates: () => FiatRequestedRatesAction
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BalanceSidebar extends React.Component {
|
export class BalanceSidebar extends React.Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { wallet, balance, network, tokenBalances, rates } = this.props;
|
const {
|
||||||
|
wallet,
|
||||||
|
balance,
|
||||||
|
network,
|
||||||
|
tokenBalances,
|
||||||
|
rates,
|
||||||
|
fiatRequestedRates
|
||||||
|
} = this.props;
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +50,12 @@ export class BalanceSidebar extends React.Component {
|
||||||
{
|
{
|
||||||
name: 'Account Info',
|
name: 'Account Info',
|
||||||
content: (
|
content: (
|
||||||
<AccountInfo wallet={wallet} balance={balance} network={network} />
|
<AccountInfo
|
||||||
|
wallet={wallet}
|
||||||
|
balance={balance}
|
||||||
|
network={network}
|
||||||
|
fiatRequestedRates={fiatRequestedRates}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -91,5 +106,6 @@ function mapStateToProps(state: State) {
|
||||||
|
|
||||||
export default connect(mapStateToProps, {
|
export default connect(mapStateToProps, {
|
||||||
...customTokenActions,
|
...customTokenActions,
|
||||||
showNotification
|
showNotification,
|
||||||
|
fiatRequestedRates
|
||||||
})(BalanceSidebar);
|
})(BalanceSidebar);
|
||||||
|
|
|
@ -7,11 +7,9 @@ import {
|
||||||
getDeterministicWallets,
|
getDeterministicWallets,
|
||||||
setDesiredToken
|
setDesiredToken
|
||||||
} from 'actions/deterministicWallets';
|
} from 'actions/deterministicWallets';
|
||||||
import { toUnit } from 'libs/units';
|
|
||||||
import { getNetworkConfig } from 'selectors/config';
|
import { getNetworkConfig } from 'selectors/config';
|
||||||
import { getTokens } from 'selectors/wallet';
|
import { getTokens } from 'selectors/wallet';
|
||||||
import { isValidPath } from 'libs/validators';
|
import { isValidPath } from 'libs/validators';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DeterministicWalletData,
|
DeterministicWalletData,
|
||||||
GetDeterministicWalletsArgs,
|
GetDeterministicWalletsArgs,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { SetRatesAction, RatesAction } from 'actions/rates';
|
import type { FiatSucceededRatesAction, RatesAction } from 'actions/rates';
|
||||||
|
|
||||||
// SYMBOL -> PRICE TO BUY 1 ETH
|
// SYMBOL -> PRICE TO BUY 1 ETH
|
||||||
export type State = {
|
export type State = {
|
||||||
|
@ -8,7 +8,10 @@ export type State = {
|
||||||
|
|
||||||
export const INITIAL_STATE: State = {};
|
export const INITIAL_STATE: State = {};
|
||||||
|
|
||||||
function setRates(state: State, action: SetRatesAction): State {
|
function fiatSucceededRates(
|
||||||
|
state: State,
|
||||||
|
action: FiatSucceededRatesAction
|
||||||
|
): State {
|
||||||
return action.payload;
|
return action.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,8 +20,8 @@ export function rates(
|
||||||
action: RatesAction
|
action: RatesAction
|
||||||
): State {
|
): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'RATES_SET':
|
case 'RATES_FIAT_SUCCEEDED':
|
||||||
return setRates(state, action);
|
return fiatSucceededRates(state, action);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { Ether } from 'libs/units';
|
||||||
export type State = {
|
export type State = {
|
||||||
inst: ?IWallet,
|
inst: ?IWallet,
|
||||||
// in ETH
|
// in ETH
|
||||||
balance: Ether,
|
balance: ?Ether,
|
||||||
tokens: {
|
tokens: {
|
||||||
[string]: Big
|
[string]: Big
|
||||||
},
|
},
|
||||||
|
@ -23,14 +23,14 @@ export type State = {
|
||||||
|
|
||||||
export const INITIAL_STATE: State = {
|
export const INITIAL_STATE: State = {
|
||||||
inst: null,
|
inst: null,
|
||||||
balance: new Ether(0),
|
balance: null,
|
||||||
tokens: {},
|
tokens: {},
|
||||||
isBroadcasting: false,
|
isBroadcasting: false,
|
||||||
transactions: []
|
transactions: []
|
||||||
};
|
};
|
||||||
|
|
||||||
function setWallet(state: State, action: SetWalletAction): State {
|
function setWallet(state: State, action: SetWalletAction): State {
|
||||||
return { ...state, inst: action.payload, balance: new Ether(0), tokens: {} };
|
return { ...state, inst: action.payload, balance: null, tokens: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBalance(state: State, action: SetBalanceAction): State {
|
function setBalance(state: State, action: SetBalanceAction): State {
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { put, call } from 'redux-saga/effects';
|
import { put, call, takeLatest } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { handleJSONResponse } from 'api/utils';
|
import { handleJSONResponse } from 'api/utils';
|
||||||
|
import { fiatSucceededRates } from 'actions/rates';
|
||||||
import { setRates } from 'actions/rates';
|
|
||||||
import { showNotification } from 'actions/notifications';
|
|
||||||
|
|
||||||
import type { Yield, Return, Next } from 'sagas/types';
|
import type { Yield, Return, Next } from 'sagas/types';
|
||||||
|
|
||||||
const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP'];
|
const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP'];
|
||||||
const symbolsURL = symbols.join(',');
|
const symbolsURL = symbols.join(',');
|
||||||
|
// TODO - internationalize
|
||||||
|
const ERROR_MESSAGE = 'Could not fetch rate data.';
|
||||||
|
|
||||||
const fetchRates = () =>
|
const fetchRates = () =>
|
||||||
fetch(
|
fetch(
|
||||||
`https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=${symbolsURL}`
|
`https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=${symbolsURL}`
|
||||||
).then(response =>
|
).then(response => handleJSONResponse(response, ERROR_MESSAGE));
|
||||||
handleJSONResponse(response, 'Could not fetch rate data.')
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function* ratesSaga(): Generator<Yield, Return, Next> {
|
export function* handleRatesRequest(): Generator<Yield, Return, Next> {
|
||||||
try {
|
try {
|
||||||
const rates = yield call(fetchRates);
|
const rates = yield call(fetchRates);
|
||||||
yield put(setRates(rates));
|
yield put(fiatSucceededRates(rates));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put(showNotification('danger', error));
|
yield put({ type: 'RATES_FIAT_FAILED', payload: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function* ratesSaga(): Generator<Yield, Return, Next> {
|
||||||
|
yield takeLatest('RATES_FIAT_REQUESTED', handleRatesRequest);
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function* loadBityRates(_action?: any): Generator<Yield, Return, Next> {
|
||||||
const data = yield call(getAllRates);
|
const data = yield call(getAllRates);
|
||||||
yield put(loadBityRatesSucceededSwap(data));
|
yield put(loadBityRatesSucceededSwap(data));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put(yield showNotification('danger', error));
|
yield put(yield showNotification('danger', error.message));
|
||||||
}
|
}
|
||||||
yield call(delay, 5000);
|
yield call(delay, 5000);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ describe('wallet reducer', () => {
|
||||||
expect(wallet(undefined, walletActions.setWallet(walletInstance))).toEqual({
|
expect(wallet(undefined, walletActions.setWallet(walletInstance))).toEqual({
|
||||||
...INITIAL_STATE,
|
...INITIAL_STATE,
|
||||||
inst: walletInstance,
|
inst: walletInstance,
|
||||||
balance: new Ether(0),
|
balance: null,
|
||||||
tokens: {}
|
tokens: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue