Wallet Loading States & Spinner Update (#334)

* Add disclaimer modal to footer

* Remove duplicate code & unnecessary styles

* Fix formatting noise

* remove un-used css style

* Fix tslint error & add media query for modals

* Nest Media Query

* Replace '???' with Spinner & update spinner

* Add loading states for wallet balances

* Update wallet test

* Remove excess data passed to wallet balance reducer & Fix wallet balance types

* Merge 'develop' into 'loading-indicator'

* Add 'light' prop to Spinner

* Only show spinners when fetching data

* Remove format diff

* Apply naming conventions

* Remove network name when offline
This commit is contained in:
James Prado 2017-11-14 22:44:55 -05:00 committed by Daniel Ternyak
parent 7e7c070abe
commit 0d5d0cea9a
23 changed files with 215 additions and 79 deletions

View File

@ -47,14 +47,28 @@ export function setWallet(value: IWallet): types.SetWalletAction {
};
}
export type TSetBalance = typeof setBalance;
export function setBalance(value: Wei): types.SetBalanceAction {
export function setBalancePending(): types.SetBalancePendingAction {
return {
type: TypeKeys.WALLET_SET_BALANCE,
type: TypeKeys.WALLET_SET_BALANCE_PENDING
};
}
export type TSetBalance = typeof setBalanceFullfilled;
export function setBalanceFullfilled(
value: Wei
): types.SetBalanceFullfilledAction {
return {
type: TypeKeys.WALLET_SET_BALANCE_FULFILLED,
payload: value
};
}
export function setBalanceRejected(): types.SetBalanceRejectedAction {
return {
type: TypeKeys.WALLET_SET_BALANCE_REJECTED
};
}
export type TSetTokenBalances = typeof setTokenBalances;
export function setTokenBalances(payload: {
[key: string]: TokenValue;

View File

@ -33,10 +33,16 @@ export interface ResetWalletAction {
}
/*** Set Balance ***/
export interface SetBalanceAction {
type: TypeKeys.WALLET_SET_BALANCE;
export interface SetBalancePendingAction {
type: TypeKeys.WALLET_SET_BALANCE_PENDING;
}
export interface SetBalanceFullfilledAction {
type: TypeKeys.WALLET_SET_BALANCE_FULFILLED;
payload: Wei;
}
export interface SetBalanceRejectedAction {
type: TypeKeys.WALLET_SET_BALANCE_REJECTED;
}
/*** Set Token Balance ***/
export interface SetTokenBalancesAction {
@ -94,7 +100,9 @@ export type WalletAction =
| UnlockPrivateKeyAction
| SetWalletAction
| ResetWalletAction
| SetBalanceAction
| SetBalancePendingAction
| SetBalanceFullfilledAction
| SetBalanceRejectedAction
| SetTokenBalancesAction
| BroadcastTxRequestedAction
| BroadcastTxFailedAction

View File

@ -4,7 +4,9 @@ export enum TypeKeys {
WALLET_UNLOCK_MNEMONIC = 'WALLET_UNLOCK_MNEMONIC',
WALLET_UNLOCK_WEB3 = 'WALLET_UNLOCK_WEB3',
WALLET_SET = 'WALLET_SET',
WALLET_SET_BALANCE = 'WALLET_SET_BALANCE',
WALLET_SET_BALANCE_PENDING = 'WALLET_SET_BALANCE_PENDING',
WALLET_SET_BALANCE_FULFILLED = 'WALLET_SET_BALANCE_FULFILLED',
WALLET_SET_BALANCE_REJECTED = 'WALLET_SET_BALANCE_REJECTED',
WALLET_SET_TOKEN_BALANCES = 'WALLET_SET_TOKEN_BALANCES',
WALLET_BROADCAST_TX_REQUESTED = 'WALLET_BROADCAST_TX_REQUESTED',
WALLET_BROADCAST_TX_FAILED = 'WALLET_BROADCAST_TX_FAILED',

View File

@ -1,14 +1,14 @@
import { TFetchCCRates } from 'actions/rates';
import { Identicon, UnitDisplay } from 'components/ui';
import { NetworkConfig } from 'config/data';
import { IWallet } from 'libs/wallet';
import { Wei } from 'libs/units';
import { IWallet, Balance } from 'libs/wallet';
import React from 'react';
import translate from 'translations';
import './AccountInfo.scss';
import Spinner from 'components/ui/Spinner';
interface Props {
balance: Wei;
balance: Balance;
wallet: IWallet;
network: NetworkConfig;
fetchCCRates: TFetchCCRates;
@ -79,13 +79,17 @@ export default class AccountInfo extends React.Component<Props, State> {
className="AccountInfo-list-item-clickable mono wrap"
onClick={this.toggleShowLongBalance}
>
<UnitDisplay
value={balance}
unit={'ether'}
displayShortBalance={!showLongBalance}
/>
{balance.isPending ? (
<Spinner />
) : (
<UnitDisplay
value={balance.wei}
unit={'ether'}
displayShortBalance={!showLongBalance}
/>
)}
</span>
{` ${network.name}`}
{balance ? `${network.name}` : null}
</li>
</ul>
</div>

View File

@ -1,5 +1,5 @@
@import "common/sass/variables";
@import "common/sass/mixins";
@import 'common/sass/variables';
@import 'common/sass/mixins';
.EquivalentValues {
&-title {
@ -25,6 +25,7 @@
}
&-label {
white-space: pre-wrap;
display: inline-block;
min-width: 36px;
}

View File

@ -1,13 +1,14 @@
import { Wei } from 'libs/units';
import React from 'react';
import translate from 'translations';
import './EquivalentValues.scss';
import { State } from 'reducers/rates';
import { symbols } from 'actions/rates';
import { UnitDisplay } from 'components/ui';
import { Balance } from 'libs/wallet';
import Spinner from 'components/ui/Spinner';
interface Props {
balance?: Wei;
balance: Balance;
rates?: State['rates'];
ratesError?: State['ratesError'];
}
@ -29,18 +30,19 @@ export default class EquivalentValues extends React.Component<Props, {}> {
return (
<li className="EquivalentValues-values-currency" key={key}>
<span className="EquivalentValues-values-currency-label">
{key}:
{key + ': '}
</span>
<span className="EquivalentValues-values-currency-value">
{' '}
{balance ? (
{balance.isPending ? (
<Spinner />
) : (
<UnitDisplay
unit={'ether'}
value={balance.muln(rates[key])}
value={
balance.wei ? balance.wei.muln(rates[key]) : null
}
displayShortBalance={2}
/>
) : (
'???'
)}
</span>
</li>

View File

@ -7,8 +7,7 @@ import {
import { showNotification, TShowNotification } from 'actions/notifications';
import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates';
import { NetworkConfig } from 'config/data';
import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet';
import { IWallet, Balance } from 'libs/wallet';
import React from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
@ -27,7 +26,7 @@ import OfflineToggle from './OfflineToggle';
interface Props {
wallet: IWallet;
balance: Wei;
balance: Balance;
network: NetworkConfig;
tokenBalances: TokenBalance[];
rates: State['rates'];

View File

@ -0,0 +1,66 @@
.Spinner {
animation: rotate 2s linear infinite;
&-x1 {
height: 1em;
width: 1em;
}
&-x2 {
height: 2em;
width: 2em;
}
&-x3 {
height: 3em;
width: 3em;
}
&-x4 {
height: 4em;
width: 4em;
}
&-x5 {
height: 5em;
width: 5em;
}
& .path {
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
&-light {
& .path {
stroke: white;
}
}
&-dark {
& .path {
stroke: #163151;
}
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}

View File

@ -1,13 +1,27 @@
import React from 'react';
import './Spinner.scss';
type Size = 'lg' | '2x' | '3x' | '4x' | '5x';
type Size = 'x1' | 'x2' | 'x3' | 'x4' | 'x5';
interface SpinnerProps {
size?: Size;
light?: boolean;
}
const Spinner = ({ size = 'fa-' }: SpinnerProps) => {
return <i className={`fa fa-spinner fa-spin fa-${size ? size : 'fw'}`} />;
const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => {
const color = light ? 'Spinner-light' : 'Spinner-dark';
return (
<svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50">
<circle
className="path"
cx="25"
cy="25"
r="20"
fill="none"
strokeWidth="5"
/>
</svg>
);
};
export default Spinner;

View File

@ -14,7 +14,7 @@ interface Props {
* @type {TokenValue | Wei}
* @memberof Props
*/
value?: TokenValue | Wei;
value?: TokenValue | Wei | null;
/**
* @description Symbol to display to the right of the value, such as 'ETH'
* @type {string}
@ -43,7 +43,7 @@ const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
const { value, symbol, displayShortBalance } = params;
if (!value) {
return <span>???</span>;
return <span>Balance isn't available offline</span>;
}
const convertedValue = isEthereumUnit(params)

View File

@ -1,5 +1,5 @@
import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet';
import { IWallet, Balance } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
import { TBroadcastTx } from 'actions/wallet';
@ -7,7 +7,7 @@ import { TShowNotification } from 'actions/notifications';
export interface Props {
wallet: IWallet;
balance: Wei;
balance: Balance;
node: NodeConfig;
nodeLib: RPCNode;
chainId: NetworkConfig['chainId'];

View File

@ -4,13 +4,13 @@ import { toWei, Wei, getDecimal } from 'libs/units';
import { connect } from 'react-redux';
import { showNotification, TShowNotification } from 'actions/notifications';
import { broadcastTx, TBroadcastTx } from 'actions/wallet';
import { IWallet } from 'libs/wallet/IWallet';
import { IWallet, Balance } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
export interface IWithTx {
wallet: IWallet;
balance: Wei;
balance: Balance;
node: NodeConfig;
nodeLib: RPCNode;
chainId: NetworkConfig['chainId'];

View File

@ -1,13 +1,13 @@
import React from 'react';
import translate, { translateRaw } from 'translations';
import UnitDropdown from './UnitDropdown';
import { Wei } from 'libs/units';
import { Balance } from 'libs/wallet';
import { UnitConverter } from 'components/renderCbs';
interface Props {
decimal: number;
unit: string;
tokens: string[];
balance: number | null | Wei;
balance: number | null | Balance;
isReadOnly: boolean;
onAmountChange(value: string, unit: string): void;
onUnitChange(unit: string): void;
@ -30,10 +30,11 @@ export default class AmountField extends React.Component {
<UnitConverter decimal={decimal} onChange={this.callWithBaseUnit}>
{({ onUserInput, convertedUnit }) => (
<input
className={`form-control ${isFinite(Number(convertedUnit)) &&
Number(convertedUnit) > 0
? 'is-valid'
: 'is-invalid'}`}
className={`form-control ${
isFinite(Number(convertedUnit)) && Number(convertedUnit) > 0
? 'is-valid'
: 'is-invalid'
}`}
type="text"
placeholder={translateRaw('SEND_amount_short')}
value={convertedUnit}

View File

@ -115,7 +115,7 @@ class ConfirmationModal extends React.Component<Props, State> {
<div className="ConfModal">
{isBroadcasting ? (
<div className="ConfModal-loading">
<Spinner size="5x" />
<Spinner size="x5" />
</div>
) : (
<div>

View File

@ -32,7 +32,7 @@ import {
import { UnitKey, Wei, getDecimal, toWei } from 'libs/units';
import { isValidETHAddress } from 'libs/validators';
// LIBS
import { IWallet, Web3Wallet } from 'libs/wallet';
import { IWallet, Balance, Web3Wallet } from 'libs/wallet';
import pickBy from 'lodash/pickBy';
import React from 'react';
// REDUX
@ -92,7 +92,7 @@ interface State {
interface Props {
wallet: IWallet;
balance: Wei;
balance: Balance;
nodeLib: RPCNode;
network: NetworkConfig;
tokens: MergedToken[];
@ -353,7 +353,7 @@ export class SendTransaction extends React.Component<Props, State> {
{generateTxProcessing && (
<div className="container">
<div className="row form-group text-center">
<Spinner size="5x" />
<Spinner size="x5" />
</div>
</div>
)}
@ -574,7 +574,7 @@ export class SendTransaction extends React.Component<Props, State> {
value = getBalanceMinusGasCosts(
bigGasLimit,
gasPrice,
balance
balance.wei
).toString();
} else {
const tokenBalance = this.props.tokenBalances.find(

View File

@ -44,14 +44,13 @@ export default class CurrentRates extends Component<Pairs, State> {
name={pair + 'Amount'}
/>
<span className="SwapRates-panel-rate-amount">
{` ${origin} = ${toFixedIfLarger(
statePair * propsPair,
6
)} ${destination}`}
{` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${
destination
}`}
</span>
</div>
) : (
<Spinner />
<Spinner size="x1" light={true} />
)}
</div>
);

View File

@ -183,11 +183,9 @@ EncodedCall:${data}`);
//TODO: parse args based on type
if (!suppliedArgs[name]) {
throw Error(
`Expected argument "${name}" of type "${type}" missing, suppliedArgs: ${JSON.stringify(
suppliedArgs,
null,
2
)}`
`Expected argument "${name}" of type "${
type
}" missing, suppliedArgs: ${JSON.stringify(suppliedArgs, null, 2)}`
);
}
const value = suppliedArgs[name];

View File

@ -0,0 +1,6 @@
import { Wei } from 'libs/units';
export interface Balance {
wei: Wei;
isPending: boolean;
}

View File

@ -1,3 +1,4 @@
export { IWallet } from './IWallet';
export { Balance } from './balance';
export * from './deterministic';
export * from './non-deterministic';

View File

@ -1,19 +1,19 @@
import { SetBalanceFullfilledAction } from 'actions/wallet/actionTypes';
import {
SetBalanceAction,
SetTokenBalancesAction,
SetWalletAction,
WalletAction,
TypeKeys
} from 'actions/wallet';
import { Wei, TokenValue } from 'libs/units';
import { TokenValue } from 'libs/units';
import { BroadcastTransactionStatus } from 'libs/transaction';
import { IWallet } from 'libs/wallet';
import { IWallet, Balance } from 'libs/wallet';
import { getTxFromBroadcastTransactionStatus } from 'selectors/wallet';
export interface State {
inst?: IWallet | null;
// in ETH
balance?: Wei | null;
balance: Balance | { wei: null };
tokens: {
[key: string]: TokenValue;
};
@ -22,18 +22,36 @@ export interface State {
export const INITIAL_STATE: State = {
inst: null,
balance: null,
balance: { isPending: false, wei: null },
tokens: {},
transactions: []
};
function setWallet(state: State, action: SetWalletAction): State {
return { ...state, inst: action.payload, balance: null, tokens: {} };
return {
...state,
inst: action.payload,
balance: INITIAL_STATE.balance,
tokens: INITIAL_STATE.tokens
};
}
function setBalance(state: State, action: SetBalanceAction): State {
const weiBalance = action.payload;
return { ...state, balance: weiBalance };
function setBalancePending(state: State): State {
return { ...state, balance: { ...state.balance, isPending: true } };
}
function setBalanceFullfilled(
state: State,
action: SetBalanceFullfilledAction
): State {
return {
...state,
balance: { wei: action.payload, isPending: false }
};
}
function setBalanceRejected(state: State): State {
return { ...state, balance: { ...state.balance, isPending: false } };
}
function setTokenBalances(state: State, action: SetTokenBalancesAction): State {
@ -111,8 +129,12 @@ export function wallet(
return setWallet(state, action);
case TypeKeys.WALLET_RESET:
return INITIAL_STATE;
case TypeKeys.WALLET_SET_BALANCE:
return setBalance(state, action);
case TypeKeys.WALLET_SET_BALANCE_PENDING:
return setBalancePending(state);
case TypeKeys.WALLET_SET_BALANCE_FULFILLED:
return setBalanceFullfilled(state, action);
case TypeKeys.WALLET_SET_BALANCE_REJECTED:
return setBalanceRejected(state);
case TypeKeys.WALLET_SET_TOKEN_BALANCES:
return setTokenBalances(state, action);
case TypeKeys.WALLET_BROADCAST_TX_REQUESTED:

View File

@ -3,7 +3,9 @@ import {
broadCastTxFailed,
BroadcastTxRequestedAction,
broadcastTxSucceded,
setBalance,
setBalanceFullfilled,
setBalancePending,
setBalanceRejected,
setTokenBalances,
setWallet,
UnlockKeystoreAction,
@ -39,6 +41,7 @@ import translate from 'translations';
function* updateAccountBalance(): SagaIterator {
try {
yield put(setBalancePending());
const wallet: null | IWallet = yield select(getWalletInst);
if (!wallet) {
return;
@ -47,9 +50,9 @@ function* updateAccountBalance(): SagaIterator {
const address = yield apply(wallet, wallet.getAddressString);
// network request
const balance: Wei = yield apply(node, node.getBalance, [address]);
yield put(setBalance(balance));
yield put(setBalanceFullfilled(balance));
} catch (error) {
yield put({ type: 'updateAccountBalance_error', error });
yield put(setBalanceRejected());
}
}
@ -191,8 +194,6 @@ function* broadcastTx(action: BroadcastTxRequestedAction): SagaIterator {
}
export default function* walletSaga(): SagaIterator {
// useful for development
yield call(updateBalances);
yield [
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
takeEvery('WALLET_UNLOCK_KEYSTORE', unlockKeystore),

View File

@ -369,7 +369,7 @@ declare module 'bn.js' {
* @description reduct
*/
modn(b: number): number; //API consistency https://github.com/indutny/bn.js/pull/130
modn(b: number): BN;
/**
* @description rounded division

View File

@ -16,9 +16,7 @@ describe('wallet reducer', () => {
expect(wallet(undefined, walletActions.setWallet(walletInstance))).toEqual({
...INITIAL_STATE,
inst: walletInstance,
balance: null,
tokens: {}
inst: walletInstance
});
});