mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-02-18 05:56:54 +00:00
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(
|
export function showNotification(
|
||||||
level: types.NOTIFICATION_LEVEL = 'info',
|
level: types.NOTIFICATION_LEVEL = 'info',
|
||||||
msg: ReactElement<any> | string,
|
msg: ReactElement<any> | string,
|
||||||
duration?: number | types.INFINITY
|
duration?: number
|
||||||
): types.ShowNotificationAction {
|
): types.ShowNotificationAction {
|
||||||
return {
|
return {
|
||||||
type: TypeKeys.SHOW_NOTIFICATION,
|
type: TypeKeys.SHOW_NOTIFICATION,
|
||||||
|
@ -2,13 +2,12 @@ import { ReactElement } from 'react';
|
|||||||
import { TypeKeys } from './constants';
|
import { TypeKeys } from './constants';
|
||||||
/*** Shared types ***/
|
/*** Shared types ***/
|
||||||
export type NOTIFICATION_LEVEL = 'danger' | 'warning' | 'success' | 'info';
|
export type NOTIFICATION_LEVEL = 'danger' | 'warning' | 'success' | 'info';
|
||||||
export type INFINITY = 'infinity';
|
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
level: NOTIFICATION_LEVEL;
|
level: NOTIFICATION_LEVEL;
|
||||||
msg: ReactElement<any> | string;
|
msg: ReactElement<any> | string;
|
||||||
id: number;
|
id: number;
|
||||||
duration?: number | INFINITY;
|
duration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Close notification ***/
|
/*** Close notification ***/
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
fromTokenBase,
|
fromTokenBase,
|
||||||
getDecimal,
|
getDecimal,
|
||||||
@ -7,6 +8,9 @@ import {
|
|||||||
TokenValue
|
TokenValue
|
||||||
} from 'libs/units';
|
} from 'libs/units';
|
||||||
import { formatNumber as format } from 'utils/formatters';
|
import { formatNumber as format } from 'utils/formatters';
|
||||||
|
import Spinner from 'components/ui/Spinner';
|
||||||
|
import { getOffline } from 'selectors/config';
|
||||||
|
import { AppState } from 'reducers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@ -41,33 +45,63 @@ const isEthereumUnit = (param: EthProps | TokenProps): param is EthProps =>
|
|||||||
|
|
||||||
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
|
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
|
||||||
const { value, symbol, displayShortBalance } = params;
|
const { value, symbol, displayShortBalance } = params;
|
||||||
|
let element;
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return <span>Balance isn't available offline</span>;
|
element = <Spinner size="x1" />;
|
||||||
}
|
|
||||||
|
|
||||||
const convertedValue = isEthereumUnit(params)
|
|
||||||
? fromTokenBase(value, getDecimal(params.unit))
|
|
||||||
: fromTokenBase(value, params.decimal);
|
|
||||||
|
|
||||||
let formattedValue;
|
|
||||||
|
|
||||||
if (displayShortBalance) {
|
|
||||||
const digits =
|
|
||||||
typeof displayShortBalance === 'number' && displayShortBalance;
|
|
||||||
formattedValue = digits
|
|
||||||
? format(convertedValue, digits)
|
|
||||||
: format(convertedValue);
|
|
||||||
} else {
|
} else {
|
||||||
formattedValue = convertedValue;
|
const convertedValue = isEthereumUnit(params)
|
||||||
|
? fromTokenBase(value, getDecimal(params.unit))
|
||||||
|
: fromTokenBase(value, params.decimal);
|
||||||
|
|
||||||
|
let formattedValue;
|
||||||
|
|
||||||
|
if (displayShortBalance) {
|
||||||
|
const digits =
|
||||||
|
typeof displayShortBalance === 'number' && displayShortBalance;
|
||||||
|
formattedValue = digits
|
||||||
|
? format(convertedValue, digits)
|
||||||
|
: format(convertedValue);
|
||||||
|
} else {
|
||||||
|
formattedValue = convertedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
element = (
|
||||||
|
<span>
|
||||||
|
{formattedValue}
|
||||||
|
{symbol ? ` ${symbol}` : ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <ConnectedOfflineDisplay>{element}</ConnectedOfflineDisplay>;
|
||||||
<span>
|
|
||||||
{formattedValue}
|
|
||||||
{symbol ? ` ${symbol}` : ''}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UnitDisplay;
|
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;
|
data: string;
|
||||||
}
|
}
|
||||||
export interface INode {
|
export interface INode {
|
||||||
|
ping(): Promise<boolean>;
|
||||||
getBalance(address: string): Promise<Wei>;
|
getBalance(address: string): Promise<Wei>;
|
||||||
getTokenBalance(address: string, token: Token): Promise<TokenValue>;
|
getTokenBalance(address: string, token: Token): Promise<TokenValue>;
|
||||||
getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
|
getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
|
||||||
|
@ -23,6 +23,13 @@ export default class RpcNode implements INode {
|
|||||||
this.requests = new RPCRequests();
|
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> {
|
public sendCallRequest(txObj: TxObj): Promise<string> {
|
||||||
return this.client.call(this.requests.ethCall(txObj)).then(r => {
|
return this.client.call(this.requests.ethCall(txObj)).then(r => {
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
|
@ -7,11 +7,15 @@ import {
|
|||||||
GetTokenBalanceRequest,
|
GetTokenBalanceRequest,
|
||||||
GetTransactionCountRequest,
|
GetTransactionCountRequest,
|
||||||
SendRawTxRequest,
|
SendRawTxRequest,
|
||||||
GetCurrentBlockRequest,
|
GetCurrentBlockRequest
|
||||||
} from './types';
|
} from './types';
|
||||||
import { hexEncodeData } from './utils';
|
import { hexEncodeData } from './utils';
|
||||||
import { TxObj } from '../INode';
|
import { TxObj } from '../INode';
|
||||||
export default class RPCRequests {
|
export default class RPCRequests {
|
||||||
|
public getNetVersion() {
|
||||||
|
return { method: 'net_version' };
|
||||||
|
}
|
||||||
|
|
||||||
public sendRawTx(signedTx: string): SendRawTxRequest | any {
|
public sendRawTx(signedTx: string): SendRawTxRequest | any {
|
||||||
return {
|
return {
|
||||||
method: 'eth_sendRawTransaction',
|
method: 'eth_sendRawTransaction',
|
||||||
@ -67,7 +71,7 @@ export default class RPCRequests {
|
|||||||
|
|
||||||
public getCurrentBlock(): GetCurrentBlockRequest | any {
|
public getCurrentBlock(): GetCurrentBlockRequest | any {
|
||||||
return {
|
return {
|
||||||
method: 'eth_blockNumber',
|
method: 'eth_blockNumber'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,10 @@ export default class Web3Node implements INode {
|
|||||||
this.web3 = web3;
|
this.web3 = web3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ping(): Promise<boolean> {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
public sendCallRequest(txObj: TxObj): Promise<string> {
|
public sendCallRequest(txObj: TxObj): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.web3.eth.call(txObj, 'pending', (err, res) => {
|
this.web3.eth.call(txObj, 'pending', (err, res) => {
|
||||||
|
@ -16,7 +16,13 @@ import {
|
|||||||
getCustomNodeConfigFromId,
|
getCustomNodeConfigFromId,
|
||||||
makeNodeConfigFromCustomConfig
|
makeNodeConfigFromCustomConfig
|
||||||
} from 'utils/node';
|
} from 'utils/node';
|
||||||
import { getNode, getNodeConfig, getCustomNodeConfigs } from 'selectors/config';
|
import {
|
||||||
|
getNode,
|
||||||
|
getNodeConfig,
|
||||||
|
getCustomNodeConfigs,
|
||||||
|
getOffline,
|
||||||
|
getForceOffline
|
||||||
|
} from 'selectors/config';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { TypeKeys } from 'actions/config/constants';
|
import { TypeKeys } from 'actions/config/constants';
|
||||||
import {
|
import {
|
||||||
@ -38,15 +44,58 @@ import {
|
|||||||
|
|
||||||
export const getConfig = (state: AppState): ConfigState => state.config;
|
export const getConfig = (state: AppState): ConfigState => state.config;
|
||||||
|
|
||||||
|
let hasCheckedOnline = false;
|
||||||
export function* pollOfflineStatus(): SagaIterator {
|
export function* pollOfflineStatus(): SagaIterator {
|
||||||
while (true) {
|
while (true) {
|
||||||
const offline = !navigator.onLine;
|
const node = yield select(getNodeConfig);
|
||||||
const config = yield select(getConfig);
|
const isOffline = yield select(getOffline);
|
||||||
const offlineState = config.offline;
|
const isForcedOffline = yield select(getForceOffline);
|
||||||
if (offline !== offlineState) {
|
|
||||||
yield put(toggleOfflineConfig());
|
// 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);
|
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
|
// @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.
|
// data to reload. Also the use of timeout to avoid using additional actions for now.
|
||||||
function* reload(): SagaIterator {
|
function* reload(): SagaIterator {
|
||||||
@ -164,6 +222,7 @@ export default function* configSaga(): SagaIterator {
|
|||||||
TypeKeys.CONFIG_POLL_OFFLINE_STATUS,
|
TypeKeys.CONFIG_POLL_OFFLINE_STATUS,
|
||||||
handlePollOfflineStatus
|
handlePollOfflineStatus
|
||||||
);
|
);
|
||||||
|
yield takeEvery(TypeKeys.CONFIG_FORCE_OFFLINE, handleTogglePollOfflineStatus);
|
||||||
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
||||||
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
||||||
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
|
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 { delay, SagaIterator } from 'redux-saga';
|
||||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||||
|
|
||||||
function* handleNotification(action: ShowNotificationAction): SagaIterator {
|
function* handleNotification(action: ShowNotificationAction): SagaIterator {
|
||||||
const { duration } = action.payload;
|
const { duration } = action.payload;
|
||||||
// show forever
|
// show forever
|
||||||
if (duration === 0 || duration === 'infinity') {
|
if (duration === 0 || duration === Infinity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ export function* bityTimeRemaining(): SagaIterator {
|
|||||||
if (!hasShownNotification) {
|
if (!hasShownNotification) {
|
||||||
hasShownNotification = true;
|
hasShownNotification = true;
|
||||||
yield put(
|
yield put(
|
||||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
|
showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -156,7 +156,7 @@ export function* bityTimeRemaining(): SagaIterator {
|
|||||||
if (!hasShownNotification) {
|
if (!hasShownNotification) {
|
||||||
hasShownNotification = true;
|
hasShownNotification = true;
|
||||||
yield put(
|
yield put(
|
||||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
|
showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -164,7 +164,7 @@ export function* bityTimeRemaining(): SagaIterator {
|
|||||||
if (!hasShownNotification) {
|
if (!hasShownNotification) {
|
||||||
hasShownNotification = true;
|
hasShownNotification = true;
|
||||||
yield put(
|
yield put(
|
||||||
showNotification('warning', BITY_TIMEOUT_MESSAGE, 'infinity')
|
showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -39,3 +39,11 @@ export function getLanguageSelection(state: AppState): string {
|
|||||||
export function getCustomNodeConfigs(state: AppState): CustomNodeConfig[] {
|
export function getCustomNodeConfigs(state: AppState): CustomNodeConfig[] {
|
||||||
return state.config.customNodes;
|
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…
x
Reference in New Issue
Block a user