From 900c3961cd59edc6323f852060fdd48b7b75afbd Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Thu, 20 Sep 2018 18:26:45 +0200 Subject: [PATCH] implement RouterActions in services and other actions --- src/actions/TrezorConnectActions.js | 129 +--------------- src/services/RouterService.js | 220 ++++++++++++++++----------- src/services/TrezorConnectService.js | 12 +- src/services/WalletService.js | 25 ++- 4 files changed, 157 insertions(+), 229 deletions(-) diff --git a/src/actions/TrezorConnectActions.js b/src/actions/TrezorConnectActions.js index 94f99347..5f44f9cc 100644 --- a/src/actions/TrezorConnectActions.js +++ b/src/actions/TrezorConnectActions.js @@ -6,8 +6,7 @@ import * as CONNECT from 'actions/constants/TrezorConnect'; import * as NOTIFICATION from 'actions/constants/notification'; import * as WALLET from 'actions/constants/wallet'; import { getDuplicateInstanceNumber } from 'reducers/utils'; - -import { push } from 'react-router-redux'; +import * as RouterActions from 'actions/RouterActions'; import type { DeviceMessage, @@ -79,14 +78,6 @@ export type TrezorConnectAction = { type: typeof CONNECT.STOP_ACQUIRING, }; - -const sortDevices = (devices: Array): Array => devices.sort((a, b) => { - if (!a.ts || !b.ts) { - return -1; - } - return a.ts > b.ts ? -1 : 1; -}); - export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { // set listeners TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => { @@ -132,7 +123,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS try { await TrezorConnect.init({ transportReconnect: true, - debug: true, + debug: false, popup: false, webusb: true, pendingTransportEvent: (getState().devices.length < 1), @@ -145,67 +136,11 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS } }; -// selection from Aside dropdown button -// after device_connect event -// or after acquiring device -// device type could be local TrezorDevice or Device (from trezor-connect device_connect event) -export const onSelectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - // || device.isUsedElsewhere - - // switch to initial url and reset this value - - if (!device.features) { - dispatch(push(`/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`)); - } else if (device.features.bootloader_mode) { - dispatch(push(`/device/${device.path}/bootloader`)); - } else if (!device.features.initialized) { - dispatch(push(`/device/${device.features.device_id}/initialize`)); - } else if (typeof device.instance === 'number') { - dispatch(push(`/device/${device.features.device_id}:${device.instance}`)); - } else { - const deviceId: string = device.features.device_id; - const urlParams: RouterLocationState = getState().router.location.state; - // let url: string = `/device/${ device.features.device_id }/network/ethereum/account/0`; - let url: string = `/device/${deviceId}`; - let instance: ?number; - // check if device is not TrezorDevice type - if (!device.hasOwnProperty('ts')) { - // its device from trezor-connect (called in initConnectedDevice triggered by device_connect event) - // need to lookup if there are unavailable instances - const available: Array = getState().devices.filter(d => d.path === device.path); - const latest: Array = sortDevices(available); - - if (latest.length > 0 && latest[0].instance) { - url += `:${latest[0].instance}`; - instance = latest[0].instance; - } - } - // check if current location is not set to this device - //dispatch( push(`/device/${ device.features.device_id }/network/etc/account/0`) ); - - if (urlParams.deviceInstance !== instance || urlParams.device !== deviceId) { - dispatch(push(url)); - } - } -}; - -export const initConnectedDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch/* , getState: GetState */): void => { - // const selected = getState().wallet.selectedDevice; - // if (!selected || (selected && selected.state)) { - dispatch(onSelectDevice(device)); - // } - // if (device.unacquired && selected && selected.path !== device.path && !selected.connected) { - // dispatch( onSelectDevice(device) ); - // } else if (!selected) { - // dispatch( onSelectDevice(device) ); - // } -}; - // called after backend was initialized // set listeners for connect/disconnect export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const handleDeviceConnect = (device: Device) => { - dispatch(initConnectedDevice(device)); + dispatch( RouterActions.selectDevice(device) ); }; TrezorConnect.off(DEVICE.CONNECT, handleDeviceConnect); @@ -216,56 +151,13 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetSta const { devices } = getState(); - const { initialPathname, initialParams } = getState().wallet; - - if (initialPathname) { - dispatch({ - type: WALLET.SET_INITIAL_URL, - // pathname: null, - // params: null - }); - } - - if (devices.length > 0) { - const unacquired: ?TrezorDevice = devices.find(d => d.features); - if (unacquired) { - dispatch(onSelectDevice(unacquired)); - } else { - const latest: Array = sortDevices(devices); - const firstConnected: ?TrezorDevice = latest.find(d => d.connected); - dispatch(onSelectDevice(firstConnected || latest[0])); - - // TODO - if (initialParams) { - if (!initialParams.hasOwnProperty('network') && initialPathname !== getState().router.location.pathname) { - // dispatch( push(initialPathname) ); - } - } - } + // try to redirect to initial url + if (!dispatch( RouterActions.setInitialUrl() )) { + // if initial redirection fails try to switch to first available device + dispatch( RouterActions.selectFirstAvailableDevice() ) } }; -export const switchToFirstAvailableDevice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { devices } = getState(); - if (devices.length > 0) { - // TODO: Priority: - // 1. First Unacquired - // 2. First connected - // 3. Saved with latest timestamp - const unacquired = devices.find(d => !d.features); - if (unacquired) { - dispatch(initConnectedDevice(unacquired)); - } else { - const latest: Array = sortDevices(devices); - const firstConnected: ?TrezorDevice = latest.find(d => d.connected); - dispatch(onSelectDevice(firstConnected || latest[0])); - } - } else { - dispatch(push('/')); - } -}; - - export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const selected = getState().wallet.selectedDevice; if (selected @@ -391,13 +283,6 @@ export function acquire(): AsyncAction { }; } -export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispatch: Dispatch): void => { - if (device.features) { - const devUrl: string = `${device.features.device_id}${device.instance ? `:${device.instance}` : ''}`; - dispatch(push(`/device/${devUrl}/settings`)); - } -}; - // called from Aside - device menu (forget single instance) export const forget = (device: TrezorDevice): Action => ({ type: CONNECT.FORGET_REQUEST, diff --git a/src/services/RouterService.js b/src/services/RouterService.js index 92ba3b4d..5ee37466 100644 --- a/src/services/RouterService.js +++ b/src/services/RouterService.js @@ -1,42 +1,21 @@ /* @flow */ -import { LOCATION_CHANGE/* , replace */ } from 'react-router-redux'; +import { LOCATION_CHANGE, replace } from 'react-router-redux'; import * as CONNECT from 'actions/constants/TrezorConnect'; import * as WALLET from 'actions/constants/wallet'; import * as NotificationActions from 'actions/NotificationActions'; +import * as RouterActions from 'actions/RouterActions'; import type { Middleware, MiddlewareAPI, MiddlewareDispatch, Action, + ThunkAction, RouterLocationState, TrezorDevice, } from 'flowtype'; -/** - * Middleware used for init application and managing router path. - */ - -const pathToParams = (path: string): RouterLocationState => { - const urlParts: Array = path.split('/').slice(1); - const params: RouterLocationState = {}; - if (urlParts.length < 1 || path === '/') return params; - - for (let i = 0, len = urlParts.length; i < len; i += 2) { - params[urlParts[i]] = urlParts[i + 1] || urlParts[i]; - } - - if (params.hasOwnProperty('device')) { - const isClonedDevice: Array = params.device.split(':'); - if (isClonedDevice.length > 1) { - params.device = isClonedDevice[0]; - params.deviceInstance = isClonedDevice[1]; - } - } - - return params; -}; - +/* const validation = (api: MiddlewareAPI, params: RouterLocationState): boolean => { if (params.hasOwnProperty('device')) { const { devices } = api.getState(); @@ -64,93 +43,148 @@ const validation = (api: MiddlewareAPI, params: RouterLocationState): boolean => return true; }; +*/ + +const deviceModeValidation = (api: MiddlewareAPI, current: RouterLocationState, requested: RouterLocationState): boolean => { + // allow url change if requested device is not the same as current state + if (current.device !== requested.device) return true; + // find device + const { devices } = api.getState(); + let device: ?TrezorDevice; + if (requested.hasOwnProperty('deviceInstance')) { + device = devices.find(d => d.features && d.features.device_id === requested.device && d.instance === parseInt(requested.deviceInstance, 10)); + } else { + device = devices.find(d => d.path === requested.device || (d.features && d.features.device_id === requested.device)); + } + if (!device) return false; + if (!device.features) return false; + if (device.firmware === 'required') return false; -let __unloading: boolean = false; + return true; +} -const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { - if (action.type === WALLET.ON_BEFORE_UNLOAD) { - __unloading = true; - } else if (action.type === LOCATION_CHANGE && !__unloading) { - const { location } = api.getState().router; - const { devices } = api.getState(); - const { error } = api.getState().connect; +/** + * Redux Middleware used for managing router path + * This middleware couldn't use async/await because LOCATION_CHANGE action is also synchronized with RouterReducer (react-router-redux) + */ +const RouterService1: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { + if (action.type !== LOCATION_CHANGE) { + return next(action); + } - const requestedParams: RouterLocationState = pathToParams(action.payload.pathname); - const currentParams: RouterLocationState = pathToParams(location ? location.pathname : '/'); - const postActions: Array = []; + action.payload.state = api.dispatch( RouterActions.pathToParams(action.payload.pathname) ); - let redirectPath: ?string; - // first event after application loads - if (!location) { - postActions.push({ - type: WALLET.SET_INITIAL_URL, - pathname: action.payload.pathname, - state: requestedParams, - }); + next(action); - redirectPath = '/'; - } else { - const isModalOpened: boolean = api.getState().modal.opened; - // there are no devices attached or initialization error occurs - const landingPage: boolean = devices.length < 1 || error !== null; + return action; +} - // modal is still opened and currentPath is still valid - // example 1 (valid blocking): url changes while passphrase modal opened but device is still connected (we want user to finish this action) +const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { + // make sure that middleware should process this action + if (action.type !== LOCATION_CHANGE || api.getState().wallet.unloading) { + // Pass action through by default + return next(action); + } + + const { location } = api.getState().router; + const requestedUrl: string = action.payload.pathname; + // map current and incoming url into RouterLocationState object + const currentParams = api.dispatch( RouterActions.pathToParams(location ? location.pathname : '/') ); + const requestedParams = api.dispatch( RouterActions.pathToParams(requestedUrl) ); + // postActions will be dispatched AFTER propagation of LOCATION_CHANGE action using next(action) below + const postActions: Array = []; + const urlChanged: boolean = location ? requestedUrl !== location.pathname : true; + let redirectUrl: ?string; + + if (!location) { + // handle first url change + // store requested url in WalletReducer and try to redirect back there if possible after application is ready + // TODO: validate if initial url is potentially correct + postActions.push({ + type: WALLET.SET_INITIAL_URL, + pathname: action.payload.pathname, + state: requestedParams, + }); + // temporary redirect to landing page (loading screen) + // and wait until application is ready + redirectUrl = '/'; + } else { + const { devices } = api.getState(); + const { ready } = api.getState().wallet; + const { error } = api.getState().connect; + const isModalOpened: boolean = api.getState().modal.opened; + // there are no connected devices or application isn't ready or initialization error occurred + const shouldDisplayLandingPage: boolean = devices.length < 1 || !ready || error !== null; + const isLandingPageUrl = api.dispatch( RouterActions.isLandingPageUrl(requestedUrl) ); + const currentParamsAreValid: boolean = api.dispatch( RouterActions.paramsValidation(currentParams) ); + + if (isModalOpened && urlChanged && currentParamsAreValid) { + // Corner case: modal is opened and currentParams are still valid + // example 1 (valid blocking): url changed while passphrase modal opened but device is still connected (we want user to finish this action) // example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect - if (isModalOpened && action.payload.pathname !== location.pathname && validation(api, currentParams)) { - redirectPath = location.pathname; - console.warn('Modal still opened'); - } else if (landingPage) { + redirectUrl = location.pathname; + console.warn('Modal still opened'); + } else if (shouldDisplayLandingPage) { + if (!isLandingPageUrl) { // keep route on landing page - if (action.payload.pathname !== '/' && action.payload.pathname !== '/bridge') { - redirectPath = '/'; - } - } else { - // PATH VALIDATION - // redirect from root view - if (action.payload.pathname === '/' || !validation(api, requestedParams)) { - // TODO redirect to first device - redirectPath = '/device/x'; - } else if (requestedParams.device) { - if (requestedParams.network !== currentParams.network) { - postActions.push({ - type: CONNECT.COIN_CHANGED, - payload: { - network: requestedParams.network, - }, - }); - } + redirectUrl = '/'; + } + } else { + // Process regular url change during application live cycle + const requestedParamsAreValid: boolean = api.dispatch( RouterActions.paramsValidation(requestedParams) ); + if (isLandingPageUrl) { + // Corner case: disallow displaying landing page + // redirect to previous url + // TODO: && currentParamsAreValid + redirectUrl = location.pathname; + } else if (!requestedParamsAreValid) { + // Corner case: requested params are not valid + // Neither device or network doesn't exists + postActions.push( RouterActions.selectFirstAvailableDevice() ); + } else if (requestedParams.device) { + if (!deviceModeValidation(api, currentParams, requestedParams)) { + redirectUrl = location.pathname; + console.warn('Device is not in valid mode'); + } else if (requestedParams.network !== currentParams.network) { + postActions.push({ + type: CONNECT.COIN_CHANGED, + payload: { + network: requestedParams.network, + }, + }); } } } + } - if (redirectPath) { - console.warn('Redirecting...', redirectPath); - // override action to keep routerReducer sync - const url: string = redirectPath; - action.payload.state = pathToParams(url); + if (redirectUrl) { + const url: string = redirectUrl; + // override action to keep RouterReducer synchronized + action.payload.state = api.dispatch( RouterActions.pathToParams(url) ); + // change url + if (requestedUrl !== url) { + console.warn('Redirecting from', requestedUrl, 'to', url); action.payload.pathname = url; - // change url - // api.dispatch(replace(url)); - } else { - action.payload.state = requestedParams; + postActions.unshift( replace(url) ) } + } else { + // override action to keep RouterReducer synchronized + action.payload.state = requestedParams; + } - // resolve LOCATION_CHANGE action - next(action); + // resolve LOCATION_CHANGE action + next(action); - // resolve post actions - postActions.forEach((a) => { - api.dispatch(a); - }); + // resolve all post actions + postActions.forEach((a) => { + api.dispatch(a); + }); - api.dispatch(NotificationActions.clear(currentParams, requestedParams)); + // TODO: move this to wallet service? + api.dispatch(NotificationActions.clear(currentParams, requestedParams)); - return action; - } + return action; - // Pass all actions through by default - return next(action); }; export default RouterService; \ No newline at end of file diff --git a/src/services/TrezorConnectService.js b/src/services/TrezorConnectService.js index 5a4255c2..7f5816e7 100644 --- a/src/services/TrezorConnectService.js +++ b/src/services/TrezorConnectService.js @@ -1,5 +1,4 @@ /* @flow */ -import { push } from 'react-router-redux'; import TrezorConnect, { TRANSPORT, DEVICE_EVENT, UI_EVENT, UI, DEVICE, BLOCKCHAIN @@ -7,6 +6,7 @@ import TrezorConnect, { import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as BlockchainActions from 'actions/BlockchainActions'; +import * as RouterActions from 'actions/RouterActions'; import * as ModalActions from 'actions/ModalActions'; import * as STORAGE from 'actions/constants/localStorage'; import * as CONNECT from 'actions/constants/TrezorConnect'; @@ -31,7 +31,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa api.dispatch(TrezorConnectActions.init()); } else if (action.type === TRANSPORT.ERROR) { // TODO: check if modal is open - // api.dispatch( push('/') ); + // api.dispatch( RouterActions.gotoLandingPage() ); } else if (action.type === TRANSPORT.START) { api.dispatch(BlockchainActions.init()); } else if (action.type === BLOCKCHAIN_READY) { @@ -42,7 +42,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa api.dispatch(ModalActions.onRememberRequest(prevModalState)); } else if (action.type === CONNECT.FORGET) { //api.dispatch( TrezorConnectActions.forgetDevice(action.device) ); - api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice()); + api.dispatch( RouterActions.selectFirstAvailableDevice() ); } else if (action.type === CONNECT.FORGET_SINGLE) { if (api.getState().devices.length < 1 && action.device.connected) { // prompt disconnect device info in LandingPage @@ -50,9 +50,9 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa type: CONNECT.DISCONNECT_REQUEST, device: action.device, }); - api.dispatch(push('/')); + api.dispatch( RouterActions.gotoLandingPage() ); } else { - api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice()); + api.dispatch( RouterActions.selectFirstAvailableDevice() ); } } else if (action.type === DEVICE.CONNECT || action.type === DEVICE.CONNECT_UNACQUIRED) { api.dispatch(DiscoveryActions.restore()); @@ -60,7 +60,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa } else if (action.type === CONNECT.AUTH_DEVICE) { api.dispatch(DiscoveryActions.check()); } else if (action.type === CONNECT.DUPLICATE) { - api.dispatch(TrezorConnectActions.onSelectDevice(action.device)); + api.dispatch(RouterActions.selectDevice(action.device)); } else if (action.type === CONNECT.COIN_CHANGED) { api.dispatch(TrezorConnectActions.coinChanged(action.payload.network)); } else if (action.type === BLOCKCHAIN.BLOCK) { diff --git a/src/services/WalletService.js b/src/services/WalletService.js index 8b0a69ad..97448262 100644 --- a/src/services/WalletService.js +++ b/src/services/WalletService.js @@ -6,6 +6,7 @@ import { LOCATION_CHANGE } from 'react-router-redux'; import * as WALLET from 'actions/constants/wallet'; import * as WalletActions from 'actions/WalletActions'; +import * as RouterActions from 'actions/RouterActions'; import * as LocalStorageActions from 'actions/LocalStorageActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as SelectedAccountActions from 'actions/SelectedAccountActions'; @@ -29,11 +30,14 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa const { location } = api.getState().router; if (!location) { api.dispatch(WalletActions.init()); - // load data from config.json and local storage - api.dispatch(LocalStorageActions.loadData()); } } + if (action.type === WALLET.SET_INITIAL_URL) { + // load data from config.json and local storage + api.dispatch(LocalStorageActions.loadData()); + } + // pass action next(action); @@ -41,18 +45,23 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device)); } - // update common values in WallerReducer - api.dispatch(WalletActions.updateSelectedValues(prevState, action)); + // update common values ONLY if application is ready + if (api.getState().wallet.ready) { + // update common values in WallerReducer + api.dispatch(WalletActions.updateSelectedValues(prevState, action)); - // update common values in SelectedAccountReducer - api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action)); + // update common values in SelectedAccountReducer + api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action)); + } - // selected device changed + // handle selected device change if (action.type === WALLET.SET_SELECTED_DEVICE) { if (action.device) { + // try to authorize device api.dispatch(TrezorConnectActions.getSelectedDeviceState()); } else { - api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice()); + // try select different device + api.dispatch(RouterActions.selectFirstAvailableDevice()); } }