Revamp Offline Status Checker (#1917)
* Revamp app status to be event listener based * Update lockfile * Update snapshot * Show invalid only if .has-blurred * revert yarn.lock changes * Clean up input classes and types (#1925) * Show invalid for Nonce Field when empty (#1930)
This commit is contained in:
parent
c21d520422
commit
59d2b73fad
|
@ -19,7 +19,6 @@ import OnboardModal from 'containers/OnboardModal';
|
|||
import WelcomeModal from 'components/WelcomeModal';
|
||||
import NewAppReleaseModal from 'components/NewAppReleaseModal';
|
||||
import { Store } from 'redux';
|
||||
import { pollOfflineStatus, TPollOfflineStatus } from 'actions/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { RouteNotFound } from 'components/RouteNotFound';
|
||||
import { RedirectWithQuery } from 'components/RedirectWithQuery';
|
||||
|
@ -36,7 +35,6 @@ interface StateProps {
|
|||
}
|
||||
|
||||
interface DispatchProps {
|
||||
pollOfflineStatus: TPollOfflineStatus;
|
||||
setUnitMeta: TSetUnitMeta;
|
||||
}
|
||||
|
||||
|
@ -52,7 +50,6 @@ class RootClass extends Component<Props, State> {
|
|||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.pollOfflineStatus();
|
||||
this.props.setUnitMeta(this.props.networkUnit);
|
||||
this.addBodyClasses();
|
||||
}
|
||||
|
@ -190,6 +187,5 @@ const mapStateToProps = (state: AppState) => {
|
|||
};
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
pollOfflineStatus,
|
||||
setUnitMeta
|
||||
})(RootClass);
|
||||
|
|
|
@ -28,13 +28,6 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
|
|||
};
|
||||
}
|
||||
|
||||
export type TPollOfflineStatus = typeof pollOfflineStatus;
|
||||
export function pollOfflineStatus(): interfaces.PollOfflineStatus {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_POLL_OFFLINE_STATUS
|
||||
};
|
||||
}
|
||||
|
||||
export type TChangeNodeRequested = typeof changeNodeRequested;
|
||||
export function changeNodeRequested(payload: string): interfaces.ChangeNodeRequestedAction {
|
||||
return {
|
||||
|
|
|
@ -20,11 +20,6 @@ export interface ChangeLanguageAction {
|
|||
payload: string;
|
||||
}
|
||||
|
||||
/*** Poll offline status ***/
|
||||
export interface PollOfflineStatus {
|
||||
type: TypeKeys.CONFIG_POLL_OFFLINE_STATUS;
|
||||
}
|
||||
|
||||
/*** Change Node Requested ***/
|
||||
export interface ChangeNodeRequestedAction {
|
||||
type: TypeKeys.CONFIG_CHANGE_NODE_REQUESTED;
|
||||
|
@ -120,7 +115,6 @@ export type MetaAction =
|
|||
| SetOnlineAction
|
||||
| SetOfflineAction
|
||||
| ToggleAutoGasLimitAction
|
||||
| PollOfflineStatus
|
||||
| SetLatestBlockAction;
|
||||
|
||||
/*** Union Type ***/
|
||||
|
|
|
@ -6,7 +6,7 @@ export enum TypeKeys {
|
|||
|
||||
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
|
||||
CONFIG_TOGGLE_AUTO_GAS_LIMIT = 'CONFIG_TOGGLE_AUTO_GAS_LIMIT',
|
||||
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
|
||||
|
||||
CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK',
|
||||
|
||||
CONFIG_NODE_WEB3_SET = 'CONFIG_NODE_WEB3_SET',
|
||||
|
|
|
@ -50,6 +50,7 @@ class NonceField extends React.Component<Props> {
|
|||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
disabled={noncePending}
|
||||
showInvalidWithoutValue={true}
|
||||
/>
|
||||
{noncePending ? (
|
||||
<div className="Nonce-spinner">
|
||||
|
|
|
@ -81,11 +81,11 @@
|
|||
color: $input-color-placeholder;
|
||||
}
|
||||
&:not([disabled]):not([readonly]) {
|
||||
&.invalid.has-blurred.has-value {
|
||||
&.invalid {
|
||||
border-color: $brand-danger;
|
||||
box-shadow: inset 0px 0px 0px 1px $brand-danger;
|
||||
}
|
||||
&.valid.has-value {
|
||||
&.valid {
|
||||
border-color: #8dd17b;
|
||||
box-shadow: inset 0px 0px 0px 1px #8dd17b;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@ import classnames from 'classnames';
|
|||
import './Input.scss';
|
||||
|
||||
interface OwnProps extends HTMLProps<HTMLInputElement> {
|
||||
isValid?: boolean;
|
||||
showInvalidBeforeBlur?: boolean;
|
||||
showInvalidWithoutValue?: boolean;
|
||||
showValidAsPlain?: boolean;
|
||||
setInnerRef?(ref: HTMLInputElement | null): void;
|
||||
}
|
||||
|
||||
|
@ -16,12 +19,9 @@ interface State {
|
|||
isStateless: boolean;
|
||||
}
|
||||
|
||||
interface OwnProps extends HTMLProps<HTMLInputElement> {
|
||||
isValid: boolean;
|
||||
showValidAsPlain?: boolean;
|
||||
}
|
||||
type Props = OwnProps & HTMLProps<HTMLInputElement>;
|
||||
|
||||
class Input extends React.Component<OwnProps, State> {
|
||||
class Input extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
hasBlurred: false,
|
||||
isStateless: true
|
||||
|
@ -31,18 +31,29 @@ class Input extends React.Component<OwnProps, State> {
|
|||
const {
|
||||
setInnerRef,
|
||||
showInvalidBeforeBlur,
|
||||
showInvalidWithoutValue,
|
||||
showValidAsPlain,
|
||||
isValid,
|
||||
...htmlProps
|
||||
} = this.props;
|
||||
const { hasBlurred, isStateless } = this.state;
|
||||
const hasValue = !!this.props.value && this.props.value.toString().length > 0;
|
||||
const classname = classnames(
|
||||
this.props.className,
|
||||
'input-group-input',
|
||||
this.state.isStateless ? '' : isValid ? (showValidAsPlain ? '' : '') : `invalid`,
|
||||
(showInvalidBeforeBlur || this.state.hasBlurred) && 'has-blurred',
|
||||
hasValue && 'has-value'
|
||||
);
|
||||
|
||||
// Currently we don't ever highlight valid, so go empty string instead
|
||||
let validClass = isValid ? '' : 'invalid';
|
||||
if (isStateless) {
|
||||
validClass = '';
|
||||
}
|
||||
if (!hasValue && !showInvalidWithoutValue) {
|
||||
validClass = '';
|
||||
} else if (!hasBlurred && !showInvalidBeforeBlur) {
|
||||
validClass = '';
|
||||
}
|
||||
if (!hasValue && showInvalidWithoutValue) {
|
||||
validClass = 'invalid';
|
||||
}
|
||||
|
||||
const classname = classnames('input-group-input', this.props.className, validClass);
|
||||
|
||||
return (
|
||||
<input
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
import { delay, SagaIterator } from 'redux-saga';
|
||||
import {
|
||||
call,
|
||||
cancel,
|
||||
fork,
|
||||
put,
|
||||
take,
|
||||
takeEvery,
|
||||
select,
|
||||
apply,
|
||||
takeLatest
|
||||
} from 'redux-saga/effects';
|
||||
import { call, fork, put, take, takeEvery, select, apply } from 'redux-saga/effects';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import {
|
||||
getNodeId,
|
||||
getNodeConfig,
|
||||
|
@ -53,64 +44,86 @@ import {
|
|||
makeAutoNodeName
|
||||
} from 'libs/nodes';
|
||||
import { INITIAL_STATE as selectedNodeInitialState } from 'reducers/config/nodes/selectedNode';
|
||||
import { configuredStore as store } from 'store';
|
||||
|
||||
export function* pollOfflineStatus(): SagaIterator {
|
||||
let hasCheckedOnline = false;
|
||||
window.addEventListener('load', () => {
|
||||
const getShepherdStatus = () => ({
|
||||
pending: getShepherdPending(),
|
||||
isOnline: !getShepherdOffline()
|
||||
});
|
||||
|
||||
const restoreNotif = showNotification(
|
||||
'success',
|
||||
'Your connection to the network has been restored!',
|
||||
3000
|
||||
);
|
||||
const lostNetworkNotif = 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
|
||||
);
|
||||
const offlineNotif = showNotification(
|
||||
'info',
|
||||
'You are currently offline. Some features will be unavailable.',
|
||||
5000
|
||||
const { online, offline, lostNetworkNotif, offlineNotif, restoreNotif } = bindActionCreators(
|
||||
{
|
||||
offline: setOffline,
|
||||
online: setOnline,
|
||||
restoreNotif: () =>
|
||||
showNotification('success', 'Your connection to the network has been restored!', 3000),
|
||||
lostNetworkNotif: () =>
|
||||
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
|
||||
),
|
||||
|
||||
offlineNotif: () =>
|
||||
showNotification(
|
||||
'info',
|
||||
'You are currently offline. Some features will be unavailable.',
|
||||
5000
|
||||
)
|
||||
},
|
||||
store.dispatch
|
||||
);
|
||||
|
||||
while (true) {
|
||||
yield call(delay, 2500);
|
||||
const getAppOnline = () => !getOffline(store.getState());
|
||||
|
||||
const pending: ReturnType<typeof getShepherdPending> = yield call(getShepherdPending);
|
||||
if (pending) {
|
||||
continue;
|
||||
/**
|
||||
* @description Repeatedly polls itself to check for online state conflict occurs, implemented in recursive style for flexible polling times
|
||||
* as network requests take a variable amount of time.
|
||||
*
|
||||
* Whenever an app online state conflict occurs, it resolves the conflict with the following priority:
|
||||
* * If shepherd is online but app is offline -> do a ping request via shepherd provider, with the result of the ping being the set app state
|
||||
* * If shepherd is offline but app is online -> set app to offline as it wont be able to make requests anyway
|
||||
*/
|
||||
async function detectOnlineStateConflict() {
|
||||
const shepherdStatus = getShepherdStatus();
|
||||
const appOffline = getAppOnline();
|
||||
const onlineStateConflict = shepherdStatus.isOnline !== appOffline;
|
||||
|
||||
if (shepherdStatus.pending || !onlineStateConflict) {
|
||||
return setTimeout(detectOnlineStateConflict, 1000);
|
||||
}
|
||||
|
||||
const isOffline: boolean = yield select(getOffline);
|
||||
const balancerOffline = yield call(getShepherdOffline);
|
||||
|
||||
if (!balancerOffline && isOffline) {
|
||||
// If we were able to ping but redux says we're offline, mark online
|
||||
yield put(restoreNotif);
|
||||
yield put(setOnline());
|
||||
} else if (balancerOffline && !isOffline) {
|
||||
// If we were unable to ping but redux says we're online, mark offline
|
||||
// If they had been online, show an error.
|
||||
// If they hadn't been online, just inform them with a warning.
|
||||
yield put(setOffline());
|
||||
if (hasCheckedOnline) {
|
||||
yield put(lostNetworkNotif);
|
||||
} else {
|
||||
yield put(offlineNotif);
|
||||
// if app reports online but shepherd offline, then set app offline
|
||||
if (appOffline && !shepherdStatus.isOnline) {
|
||||
lostNetworkNotif();
|
||||
offline();
|
||||
} else if (!appOffline && shepherdStatus.isOnline) {
|
||||
// if app reports offline but shepherd reports online
|
||||
// send a request to shepherd provider to see if we can still send out requests
|
||||
const success = await shepherdProvider.ping().catch(() => false);
|
||||
if (success) {
|
||||
restoreNotif();
|
||||
online();
|
||||
}
|
||||
}
|
||||
hasCheckedOnline = true;
|
||||
detectOnlineStateConflict();
|
||||
}
|
||||
}
|
||||
detectOnlineStateConflict();
|
||||
|
||||
// Fork our recurring API call, watch for the need to cancel.
|
||||
export function* handlePollOfflineStatus(): SagaIterator {
|
||||
const pollOfflineStatusTask = yield fork(pollOfflineStatus);
|
||||
yield take('CONFIG_STOP_POLL_OFFLINE_STATE');
|
||||
yield cancel(pollOfflineStatusTask);
|
||||
}
|
||||
window.addEventListener('offline', () => {
|
||||
const previouslyOnline = getAppOnline();
|
||||
|
||||
// if browser reports as offline and we were previously online
|
||||
// then set offline without checking balancer state
|
||||
if (!navigator.onLine && previouslyOnline) {
|
||||
offlineNotif();
|
||||
offline();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// @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.
|
||||
|
@ -283,7 +296,6 @@ export const node = [
|
|||
takeEvery(TypeKeys.CONFIG_CHANGE_NODE_REQUESTED, handleChangeNodeRequested),
|
||||
takeEvery(TypeKeys.CONFIG_CHANGE_NODE_FORCE, handleNodeChangeForce),
|
||||
takeEvery(TypeKeys.CONFIG_CHANGE_NETWORK_REQUESTED, handleChangeNetworkRequested),
|
||||
takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus),
|
||||
takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload),
|
||||
takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, handleAddCustomNode),
|
||||
takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, handleRemoveCustomNode)
|
||||
|
|
|
@ -65,10 +65,11 @@
|
|||
&-menu {
|
||||
max-height: 10.0625rem;
|
||||
}
|
||||
&.invalid.has-blurred {
|
||||
border-color: $brand-danger;
|
||||
box-shadow: inset 0px 0px 0px 1px $brand-danger;
|
||||
}
|
||||
// Selects should never have invalid input
|
||||
// &.invalid {
|
||||
// border-color: $brand-danger;
|
||||
// box-shadow: inset 0px 0px 0px 1px $brand-danger;
|
||||
// }
|
||||
&.is-focused {
|
||||
border-color: #4295bc;
|
||||
box-shadow: inset 0px 0px 0px 1px #4295bc;
|
||||
|
|
|
@ -5,7 +5,7 @@ Object {
|
|||
"@@redux-saga/IO": true,
|
||||
"SELECT": Object {
|
||||
"args": Array [
|
||||
"ELLA",
|
||||
"CLO",
|
||||
],
|
||||
"selector": [Function],
|
||||
},
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { configuredStore } from 'store';
|
||||
import { delay, SagaIterator } from 'redux-saga';
|
||||
import { call, cancel, fork, put, take, select, apply } from 'redux-saga/effects';
|
||||
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
|
||||
import { call, fork, put, take, select, apply } from 'redux-saga/effects';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import {
|
||||
setOffline,
|
||||
setOnline,
|
||||
changeNodeSucceeded,
|
||||
changeNodeRequested,
|
||||
changeNodeFailed,
|
||||
|
@ -16,8 +14,6 @@ import {
|
|||
} from 'actions/config';
|
||||
import {
|
||||
handleChangeNodeRequested,
|
||||
handlePollOfflineStatus,
|
||||
pollOfflineStatus,
|
||||
handleNewNetwork,
|
||||
handleChangeNodeRequestedOneTime
|
||||
} from 'sagas/config/node';
|
||||
|
@ -39,88 +35,10 @@ import { selectedNodeExpectedState } from './nodes/selectedNode.spec';
|
|||
import { customNodesExpectedState, firstCustomNode } from './nodes/customNodes.spec';
|
||||
import { unsetWeb3Node, unsetWeb3NodeOnWalletEvent } from 'sagas/config/web3';
|
||||
import { shepherd } from 'mycrypto-shepherd';
|
||||
import { getShepherdOffline, getShepherdPending } from 'libs/nodes';
|
||||
|
||||
// init module
|
||||
configuredStore.getState();
|
||||
|
||||
describe('pollOfflineStatus*', () => {
|
||||
const restoreNotif = 'Your connection to the network has been restored!';
|
||||
|
||||
const lostNetworkNotif = `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.`;
|
||||
|
||||
const offlineNotif = 'You are currently offline. Some features will be unavailable.';
|
||||
|
||||
const offlineOnFirstTimeCase = pollOfflineStatus();
|
||||
it('should delay by 2.5 seconds', () => {
|
||||
expect(offlineOnFirstTimeCase.next().value).toEqual(call(delay, 2500));
|
||||
});
|
||||
|
||||
it('should skip if a node change is pending', () => {
|
||||
expect(offlineOnFirstTimeCase.next().value).toEqual(call(getShepherdPending));
|
||||
expect(offlineOnFirstTimeCase.next(true).value).toEqual(call(delay, 2500));
|
||||
expect(offlineOnFirstTimeCase.next().value).toEqual(call(getShepherdPending));
|
||||
});
|
||||
|
||||
it('should select offline', () => {
|
||||
expect(offlineOnFirstTimeCase.next(false).value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
it('should select shepherd"s offline', () => {
|
||||
expect(offlineOnFirstTimeCase.next(false).value).toEqual(call(getShepherdOffline));
|
||||
});
|
||||
|
||||
// .PUT.action.payload.msg is used because the action creator uses an random ID, cant to a showNotif comparision
|
||||
it('should put a different notif if online for the first time ', () => {
|
||||
expect(offlineOnFirstTimeCase.next(true).value).toEqual(put(setOffline()));
|
||||
expect((offlineOnFirstTimeCase.next().value as any).PUT.action.payload.msg).toEqual(
|
||||
offlineNotif
|
||||
);
|
||||
});
|
||||
|
||||
it('should loop around then go back online, putting a restore msg', () => {
|
||||
expect(offlineOnFirstTimeCase.next().value).toEqual(call(delay, 2500));
|
||||
expect(offlineOnFirstTimeCase.next().value).toEqual(call(getShepherdPending));
|
||||
expect(offlineOnFirstTimeCase.next(false).value).toEqual(select(getOffline));
|
||||
expect(offlineOnFirstTimeCase.next(true).value).toEqual(call(getShepherdOffline));
|
||||
expect((offlineOnFirstTimeCase.next().value as any).PUT.action.payload.msg).toEqual(
|
||||
restoreNotif
|
||||
);
|
||||
expect(offlineOnFirstTimeCase.next(false).value).toEqual(put(setOnline()));
|
||||
});
|
||||
|
||||
it('should put a generic lost connection notif on every time afterwards', () => {
|
||||
expect(offlineOnFirstTimeCase.next().value).toEqual(call(delay, 2500));
|
||||
expect(offlineOnFirstTimeCase.next().value).toEqual(call(getShepherdPending));
|
||||
expect(offlineOnFirstTimeCase.next(false).value).toEqual(select(getOffline));
|
||||
expect(offlineOnFirstTimeCase.next(false).value).toEqual(call(getShepherdOffline));
|
||||
expect(offlineOnFirstTimeCase.next(true).value).toEqual(put(setOffline()));
|
||||
expect((offlineOnFirstTimeCase.next().value as any).PUT.action.payload.msg).toEqual(
|
||||
lostNetworkNotif
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePollOfflineStatus*', () => {
|
||||
const gen = handlePollOfflineStatus();
|
||||
const mockTask = createMockTask();
|
||||
|
||||
it('should fork pollOffineStatus', () => {
|
||||
const expectedForkYield = fork(pollOfflineStatus);
|
||||
expect(gen.next().value).toEqual(expectedForkYield);
|
||||
});
|
||||
|
||||
it('should take CONFIG_STOP_POLL_OFFLINE_STATE', () => {
|
||||
expect(gen.next(mockTask).value).toEqual(take('CONFIG_STOP_POLL_OFFLINE_STATE'));
|
||||
});
|
||||
|
||||
it('should cancel pollOfflineStatus', () => {
|
||||
expect(gen.next().value).toEqual(cancel(mockTask));
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleChangeNodeRequested*', () => {
|
||||
let originalRandom: any;
|
||||
|
||||
|
|
Loading…
Reference in New Issue