Better Offline Detection / Handling (#478)
* Change navigator.onLine to actually pinging the network. Refactor notifications to take Infinity instead of 'infinity' * Stop polling when forced offline. * Show spinners if unit display balance is null, show offline text if were actually offline. * Fix issue with typescript and connected union-prop components. * Only ping the node when navigator.onLine changes.
This commit is contained in:
parent
1eb687c670
commit
1221a73a46
|
@ -6,7 +6,7 @@ export type TShowNotification = typeof showNotification;
|
|||
export function showNotification(
|
||||
level: types.NOTIFICATION_LEVEL = 'info',
|
||||
msg: ReactElement<any> | string,
|
||||
duration?: number | types.INFINITY
|
||||
duration?: number
|
||||
): types.ShowNotificationAction {
|
||||
return {
|
||||
type: TypeKeys.SHOW_NOTIFICATION,
|
||||
|
|
|
@ -2,13 +2,12 @@ import { ReactElement } from 'react';
|
|||
import { TypeKeys } from './constants';
|
||||
/*** Shared types ***/
|
||||
export type NOTIFICATION_LEVEL = 'danger' | 'warning' | 'success' | 'info';
|
||||
export type INFINITY = 'infinity';
|
||||
|
||||
export interface Notification {
|
||||
level: NOTIFICATION_LEVEL;
|
||||
msg: ReactElement<any> | string;
|
||||
id: number;
|
||||
duration?: number | INFINITY;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/*** Close notification ***/
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fromTokenBase,
|
||||
getDecimal,
|
||||
|
@ -7,6 +8,9 @@ import {
|
|||
TokenValue
|
||||
} from 'libs/units';
|
||||
import { formatNumber as format } from 'utils/formatters';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import { getOffline } from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
|
@ -41,11 +45,11 @@ const isEthereumUnit = (param: EthProps | TokenProps): param is EthProps =>
|
|||
|
||||
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
|
||||
const { value, symbol, displayShortBalance } = params;
|
||||
let element;
|
||||
|
||||
if (!value) {
|
||||
return <span>Balance isn't available offline</span>;
|
||||
}
|
||||
|
||||
element = <Spinner size="x1" />;
|
||||
} else {
|
||||
const convertedValue = isEthereumUnit(params)
|
||||
? fromTokenBase(value, getDecimal(params.unit))
|
||||
: fromTokenBase(value, params.decimal);
|
||||
|
@ -62,12 +66,42 @@ const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
|
|||
formattedValue = convertedValue;
|
||||
}
|
||||
|
||||
return (
|
||||
element = (
|
||||
<span>
|
||||
{formattedValue}
|
||||
{symbol ? ` ${symbol}` : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <ConnectedOfflineDisplay>{element}</ConnectedOfflineDisplay>;
|
||||
};
|
||||
|
||||
export default UnitDisplay;
|
||||
|
||||
/**
|
||||
* @description Helper component for displaying alternate text when offline.
|
||||
* Circumvents typescript issue with union props on connected components.
|
||||
*/
|
||||
interface OfflineProps {
|
||||
offline: AppState['config']['offline'];
|
||||
children: React.ReactElement<string>;
|
||||
}
|
||||
|
||||
class OfflineDisplay extends React.Component<OfflineProps> {
|
||||
public render() {
|
||||
if (this.props.offline) {
|
||||
return <span>Balance isn't available offline</span>;
|
||||
} else {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToOfflineProps(state: AppState) {
|
||||
return {
|
||||
offline: getOffline(state)
|
||||
};
|
||||
}
|
||||
|
||||
const ConnectedOfflineDisplay = connect(mapStateToOfflineProps)(OfflineDisplay);
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface TxObj {
|
|||
data: string;
|
||||
}
|
||||
export interface INode {
|
||||
ping(): Promise<boolean>;
|
||||
getBalance(address: string): Promise<Wei>;
|
||||
getTokenBalance(address: string, token: Token): Promise<TokenValue>;
|
||||
getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
|
||||
|
|
|
@ -23,6 +23,13 @@ export default class RpcNode implements INode {
|
|||
this.requests = new RPCRequests();
|
||||
}
|
||||
|
||||
public ping(): Promise<boolean> {
|
||||
return this.client
|
||||
.call(this.requests.getNetVersion())
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
public sendCallRequest(txObj: TxObj): Promise<string> {
|
||||
return this.client.call(this.requests.ethCall(txObj)).then(r => {
|
||||
if (r.error) {
|
||||
|
|
|
@ -7,11 +7,15 @@ import {
|
|||
GetTokenBalanceRequest,
|
||||
GetTransactionCountRequest,
|
||||
SendRawTxRequest,
|
||||
GetCurrentBlockRequest,
|
||||
GetCurrentBlockRequest
|
||||
} from './types';
|
||||
import { hexEncodeData } from './utils';
|
||||
import { TxObj } from '../INode';
|
||||
export default class RPCRequests {
|
||||
public getNetVersion() {
|
||||
return { method: 'net_version' };
|
||||
}
|
||||
|
||||
public sendRawTx(signedTx: string): SendRawTxRequest | any {
|
||||
return {
|
||||
method: 'eth_sendRawTransaction',
|
||||
|
@ -67,7 +71,7 @@ export default class RPCRequests {
|
|||
|
||||
public getCurrentBlock(): GetCurrentBlockRequest | any {
|
||||
return {
|
||||
method: 'eth_blockNumber',
|
||||
method: 'eth_blockNumber'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ export default class Web3Node implements INode {
|
|||
this.web3 = web3;
|
||||
}
|
||||
|
||||
public ping(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
public sendCallRequest(txObj: TxObj): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.web3.eth.call(txObj, 'pending', (err, res) => {
|
||||
|
|
|
@ -16,7 +16,13 @@ import {
|
|||
getCustomNodeConfigFromId,
|
||||
makeNodeConfigFromCustomConfig
|
||||
} from 'utils/node';
|
||||
import { getNode, getNodeConfig, getCustomNodeConfigs } from 'selectors/config';
|
||||
import {
|
||||
getNode,
|
||||
getNodeConfig,
|
||||
getCustomNodeConfigs,
|
||||
getOffline,
|
||||
getForceOffline
|
||||
} from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { TypeKeys } from 'actions/config/constants';
|
||||
import {
|
||||
|
@ -38,15 +44,58 @@ import {
|
|||
|
||||
export const getConfig = (state: AppState): ConfigState => state.config;
|
||||
|
||||
let hasCheckedOnline = false;
|
||||
export function* pollOfflineStatus(): SagaIterator {
|
||||
while (true) {
|
||||
const offline = !navigator.onLine;
|
||||
const config = yield select(getConfig);
|
||||
const offlineState = config.offline;
|
||||
if (offline !== offlineState) {
|
||||
yield put(toggleOfflineConfig());
|
||||
const node = yield select(getNodeConfig);
|
||||
const isOffline = yield select(getOffline);
|
||||
const isForcedOffline = yield select(getForceOffline);
|
||||
|
||||
// If they're forcing themselves offline, exit the loop. It will be
|
||||
// kicked off again if they toggle it in handleTogglePollOfflineStatus.
|
||||
if (isForcedOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If our offline state disagrees with the browser, run a check
|
||||
// Don't check if the user is in another tab or window
|
||||
const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline;
|
||||
if (shouldPing && !document.hidden) {
|
||||
hasCheckedOnline = true;
|
||||
const { pingSucceeded } = yield race({
|
||||
pingSucceeded: call(node.lib.ping.bind(node.lib)),
|
||||
timeout: call(delay, 5000)
|
||||
});
|
||||
|
||||
if (pingSucceeded && isOffline) {
|
||||
// If we were able to ping but redux says we're offline, mark online
|
||||
yield put(
|
||||
showNotification(
|
||||
'success',
|
||||
'Your connection to the network has been restored!',
|
||||
3000
|
||||
)
|
||||
);
|
||||
yield put(toggleOfflineConfig());
|
||||
} else if (!pingSucceeded && !isOffline) {
|
||||
// If we were unable to ping but redux says we're online, mark offline
|
||||
yield put(
|
||||
showNotification(
|
||||
'danger',
|
||||
`You’ve lost your connection to the network, check your internet
|
||||
connection or try changing networks from the dropdown at the
|
||||
top right of the page.`,
|
||||
Infinity
|
||||
)
|
||||
);
|
||||
yield put(toggleOfflineConfig());
|
||||
} else {
|
||||
// If neither case was true, try again in 5s
|
||||
yield call(delay, 5000);
|
||||
}
|
||||
} else {
|
||||
yield call(delay, 1000);
|
||||
}
|
||||
yield call(delay, 250);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,6 +106,15 @@ function* handlePollOfflineStatus(): SagaIterator {
|
|||
yield cancel(pollOfflineStatusTask);
|
||||
}
|
||||
|
||||
function* handleTogglePollOfflineStatus(): SagaIterator {
|
||||
const isForcedOffline = yield select(getForceOffline);
|
||||
if (isForcedOffline) {
|
||||
yield fork(handlePollOfflineStatus);
|
||||
} else {
|
||||
yield call(handlePollOfflineStatus);
|
||||
}
|
||||
}
|
||||
|
||||
// @HACK For now we reload the app when doing a language swap to force non-connected
|
||||
// data to reload. Also the use of timeout to avoid using additional actions for now.
|
||||
function* reload(): SagaIterator {
|
||||
|
@ -164,6 +222,7 @@ export default function* configSaga(): SagaIterator {
|
|||
TypeKeys.CONFIG_POLL_OFFLINE_STATUS,
|
||||
handlePollOfflineStatus
|
||||
);
|
||||
yield takeEvery(TypeKeys.CONFIG_FORCE_OFFLINE, handleTogglePollOfflineStatus);
|
||||
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
||||
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
||||
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { closeNotification, ShowNotificationAction } from 'actions/notifications';
|
||||
import {
|
||||
closeNotification,
|
||||
ShowNotificationAction
|
||||
} from 'actions/notifications';
|
||||
import { delay, SagaIterator } from 'redux-saga';
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
function* handleNotification(action: ShowNotificationAction): SagaIterator {
|
||||
const { duration } = action.payload;
|
||||
// show forever
|
||||
if (duration === 0 || duration === 'infinity') {
|
||||
if (duration === 0 || duration === Infinity) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ export function* bityTimeRemaining(): SagaIterator {
|
|||
if (!hasShownNotification) {
|
||||
hasShownNotification = true;
|
||||
yield put(
|
||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
|
||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
@ -156,7 +156,7 @@ export function* bityTimeRemaining(): SagaIterator {
|
|||
if (!hasShownNotification) {
|
||||
hasShownNotification = true;
|
||||
yield put(
|
||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
|
||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
@ -164,7 +164,7 @@ export function* bityTimeRemaining(): SagaIterator {
|
|||
if (!hasShownNotification) {
|
||||
hasShownNotification = true;
|
||||
yield put(
|
||||
showNotification('warning', BITY_TIMEOUT_MESSAGE, 'infinity')
|
||||
showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -39,3 +39,11 @@ export function getLanguageSelection(state: AppState): string {
|
|||
export function getCustomNodeConfigs(state: AppState): CustomNodeConfig[] {
|
||||
return state.config.customNodes;
|
||||
}
|
||||
|
||||
export function getOffline(state: AppState): boolean {
|
||||
return state.config.offline;
|
||||
}
|
||||
|
||||
export function getForceOffline(state: AppState): boolean {
|
||||
return state.config.forceOffline;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue