From 681194def3ef0f48ee3bb1c1e4f22bb95c58110b Mon Sep 17 00:00:00 2001 From: HenryNguyen5 Date: Thu, 7 Jun 2018 19:00:32 -0400 Subject: [PATCH 1/4] Revamp app status to be event listener based --- common/Root.tsx | 4 - common/actions/config/actionCreators.ts | 7 -- common/actions/config/actionTypes.ts | 6 -- common/actions/config/constants.ts | 2 +- common/sagas/config/node.ts | 136 +++++++++++++----------- spec/reducers/config/config.spec.ts | 86 +-------------- 6 files changed, 77 insertions(+), 164 deletions(-) diff --git a/common/Root.tsx b/common/Root.tsx index 59dba1a1bd3..3a060b01f1d 100644 --- a/common/Root.tsx +++ b/common/Root.tsx @@ -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 { }; 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); diff --git a/common/actions/config/actionCreators.ts b/common/actions/config/actionCreators.ts index c1e6c26aeed..2b8bf4342f8 100644 --- a/common/actions/config/actionCreators.ts +++ b/common/actions/config/actionCreators.ts @@ -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 { diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index bd2c11a1f5d..426a6321e8f 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -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 ***/ diff --git a/common/actions/config/constants.ts b/common/actions/config/constants.ts index 7a720c6c2e2..66a4fb95c76 100644 --- a/common/actions/config/constants.ts +++ b/common/actions/config/constants.ts @@ -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', diff --git a/common/sagas/config/node.ts b/common/sagas/config/node.ts index 91e80a98b2f..22c64ba84b1 100644 --- a/common/sagas/config/node.ts +++ b/common/sagas/config/node.ts @@ -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'; - -export function* pollOfflineStatus(): SagaIterator { - let hasCheckedOnline = false; - - 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 +import { configuredStore as store } from 'store'; + +window.addEventListener('load', () => { + const getShepherdStatus = () => ({ + pending: getShepherdPending(), + isOnline: !getShepherdOffline() + }); + + 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 pending: ReturnType = yield call(getShepherdPending); - if (pending) { - continue; + const getAppOnline = () => !getOffline(store.getState()); + + /** + * @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) diff --git a/spec/reducers/config/config.spec.ts b/spec/reducers/config/config.spec.ts index eba966816a0..0369122340c 100644 --- a/spec/reducers/config/config.spec.ts +++ b/spec/reducers/config/config.spec.ts @@ -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; From c296753872f186c06fe5261865a92097f21292dd Mon Sep 17 00:00:00 2001 From: HenryNguyen5 Date: Fri, 15 Jun 2018 15:42:05 -0400 Subject: [PATCH 2/4] Move test scripts to their own folder, set scripts to fail on any command failure --- .travis.yml | 21 ++++----------------- travis-scripts/test-linux.sh | 10 ++++++++++ travis-scripts/test-osx.sh | 5 +++++ 3 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 travis-scripts/test-linux.sh create mode 100644 travis-scripts/test-osx.sh diff --git a/.travis.yml b/.travis.yml index 0b22ddef750..da1c7c910dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,29 +35,16 @@ before_install: sh -e /etc/init.d/xvfb start # uncomment once integration tests are included in CI # docker pull dternyak/eth-priv-to-addr:latest - sudo apt-get install libusb-1.0 + sudo apt-get install libusb-1.0 fi - + install: - yarn --silent script: - - | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - npm run prettier:diff - npm run test:coverage -- --maxWorkers=2 - npm run report-coverage - npm run tslint - npm run tscheck - npm run freezer - npm run freezer:validate - fi + - ./travis-scripts/test-linux.sh + - ./travis-scripts/test-osx.sh - - | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - npm run build:electron - ls -la dist/electron-builds - fi notifications: email: diff --git a/travis-scripts/test-linux.sh b/travis-scripts/test-linux.sh new file mode 100644 index 00000000000..89450c13e4e --- /dev/null +++ b/travis-scripts/test-linux.sh @@ -0,0 +1,10 @@ +set -ev # return value 1 (error) if any command fails, and display each command before its run +if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then + yarn prettier:diff + yarn test:coverage -- --maxWorkers=2 + yarn report-coverage + yarn tslint + yarn tscheck + yarn freezer + yarn freezer:validate +fi diff --git a/travis-scripts/test-osx.sh b/travis-scripts/test-osx.sh new file mode 100644 index 00000000000..4216fd3f897 --- /dev/null +++ b/travis-scripts/test-osx.sh @@ -0,0 +1,5 @@ +set -ev # return value 1 (error) if any command fails, and display each command before its run +if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then + yarn build:electron + ls -la dist/electron-builds +fi From e27ef569868b2fc9b3743b8ec359590d65ad5813 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 Date: Fri, 15 Jun 2018 16:08:02 -0400 Subject: [PATCH 3/4] Make scripts executable --- .travis.yml | 3 +++ travis-scripts/test-linux.sh | 2 ++ travis-scripts/test-osx.sh | 2 ++ 3 files changed, 7 insertions(+) mode change 100644 => 100755 travis-scripts/test-linux.sh mode change 100644 => 100755 travis-scripts/test-osx.sh diff --git a/.travis.yml b/.travis.yml index da1c7c910dd..b11b578c1c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,6 +41,9 @@ before_install: install: - yarn --silent +before_script: + - chmod +x ./travis-scripts/test-{linux,osx}.sh + script: - ./travis-scripts/test-linux.sh - ./travis-scripts/test-osx.sh diff --git a/travis-scripts/test-linux.sh b/travis-scripts/test-linux.sh old mode 100644 new mode 100755 index 89450c13e4e..ebfd1ca0986 --- a/travis-scripts/test-linux.sh +++ b/travis-scripts/test-linux.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + set -ev # return value 1 (error) if any command fails, and display each command before its run if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then yarn prettier:diff diff --git a/travis-scripts/test-osx.sh b/travis-scripts/test-osx.sh old mode 100644 new mode 100755 index 4216fd3f897..158159f7979 --- a/travis-scripts/test-osx.sh +++ b/travis-scripts/test-osx.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + set -ev # return value 1 (error) if any command fails, and display each command before its run if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then yarn build:electron From cccda1e0316a359db3494e68aaa84234aaabdbba Mon Sep 17 00:00:00 2001 From: HenryNguyen5 Date: Fri, 15 Jun 2018 17:20:44 -0400 Subject: [PATCH 4/4] Fix context menu params --- electron-app/main/contextMenu.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/electron-app/main/contextMenu.ts b/electron-app/main/contextMenu.ts index 37db1a34f7c..f35623c2851 100644 --- a/electron-app/main/contextMenu.ts +++ b/electron-app/main/contextMenu.ts @@ -1,4 +1,11 @@ -import { MenuItemConstructorOptions, shell, BrowserWindow, Menu, clipboard } from 'electron'; +import { + MenuItemConstructorOptions, + shell, + BrowserWindow, + Menu, + clipboard, + PopupOptions +} from 'electron'; import { URL } from 'url'; function popupContextMenu( @@ -86,7 +93,14 @@ function popupContextMenu( } const ctxMenu = Menu.buildFromTemplate(ctxMenuTmpl); - ctxMenu.popup(window, props); + + const popupOpts: PopupOptions = { + window, + x: props.x, + y: props.y + }; + + ctxMenu.popup(popupOpts); } export default popupContextMenu;