diff --git a/src/actions/RouterActions.js b/src/actions/RouterActions.js index 478d5dc2..d5293a05 100644 --- a/src/actions/RouterActions.js +++ b/src/actions/RouterActions.js @@ -11,6 +11,7 @@ import type { Dispatch, GetState, } from 'flowtype'; +import type { RouterAction } from 'react-router-redux'; /* * Parse url string to RouterLocationState object (key/value) @@ -67,39 +68,118 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction => (dispatch: Dispatch, getState: GetState): 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 } = 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; - - return true; -} - /* * Composing url string from given RouterLocationState object * Filters unrecognized fields and sorting in correct order */ -export const paramsToPath = (params: RouterLocationState): PayloadAction => (dispatch: Dispatch, getState: GetState): string => { - return "/"; +export const paramsToPath = (params: RouterLocationState): PayloadAction => (dispatch: Dispatch, getState: GetState): ?string => { + // TODO: share this patterns with /views/index.js + const patterns: Array> = [ + ['device', 'settings'], + ['device', 'bootloader'], + ['device', 'initialize'], + ['device', 'acquire'], + ['device', 'unreadable'], + ['device', 'network', 'account', 'send'], + ['device', 'network', 'account', 'receive'], + ['device', 'network', 'account'], + ['device'] + ]; + + // find pattern + const keys: Array = Object.keys(params); + let patternToUse: ?Array; + for (let pattern of patterns) { + const match: Array = keys.filter(key => pattern.indexOf(key) >= 0); + if (match.length === pattern.length) { + patternToUse = pattern; + break; + } + } + + // pattern not found, redirect back + if (!patternToUse) return null; + + // compose url string from pattern + let url: string = ''; + patternToUse.forEach(field => { + if (field === params[field]) { + // standalone (odd) fields + url += `/${ field }`; + } else { + url += `/${ field }/${ params[field] }`; + if (field === 'device') { + if (params.hasOwnProperty('deviceInstance')) { + url += `:${ params.deviceInstance }`; + } + } + } + + }); + return url; } +export const getValidUrl = (action: RouterAction): PayloadAction => (dispatch: Dispatch, getState: GetState): string => { + const { location } = getState().router; + + // redirect to landing page (loading screen) + // and wait until application is ready + if (!location) return '/'; + + const requestedUrl = action.payload.pathname; + // Corner case: LOCATION_CHANGE was called but pathname didn't changed (redirect action from RouterService) + if (requestedUrl === location.pathname) return requestedUrl; + + // Modal is opened + // redirect to previous url + if (getState().modal.opened) { + // 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 + const currentParams = dispatch( pathToParams(location.pathname) ); + const currentParamsAreValid = dispatch( paramsValidation(currentParams) ); + if (currentParamsAreValid) + return location.pathname; + } + + // there are no connected devices or application isn't ready or initialization error occurred + // redirect to landing page + const shouldBeLandingPage = getState().devices.length < 1 || !getState().wallet.ready || getState().connect.error !== null; + const landingPageUrl = dispatch( isLandingPageUrl(requestedUrl) ); + if (shouldBeLandingPage) { + return !landingPageUrl ? '/' : requestedUrl; + } + + // Disallow displaying landing page + // redirect to previous url + if (!shouldBeLandingPage && landingPageUrl) { + return location.pathname; + } + + // Regular url change during application live cycle + const requestedParams = dispatch( pathToParams(requestedUrl) ); + const requestedParamsAreValid: boolean = dispatch( paramsValidation(requestedParams) ); + + // Requested params are not valid + // Neither device or network doesn't exists + if (!requestedParamsAreValid) { + return location.pathname; + } + + // Compose valid url from requested params + const composedUrl = dispatch( paramsToPath(requestedParams) ); + return composedUrl || location.pathname; +} + + + /* * Utility used in "selectDevice" and "selectFirstAvailableDevice" * sorting device array by "ts" (timestamp) field diff --git a/src/services/RouterService.js b/src/services/RouterService.js index 65c4f09f..bc196ff3 100644 --- a/src/services/RouterService.js +++ b/src/services/RouterService.js @@ -1,6 +1,5 @@ /* @flow */ import { LOCATION_CHANGE, replace } from 'react-router-redux'; -import * as WALLET from 'actions/constants/wallet'; import * as RouterActions from 'actions/RouterActions'; import type { @@ -21,97 +20,27 @@ import type { 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 + // pass action 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 - redirectUrl = location.pathname; - console.warn('Modal still opened'); - } else if (shouldDisplayLandingPage) { - if (!isLandingPageUrl) { - // keep route on landing page - 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: make sure that currentParamsAreValid, otherwise selectFirstAvailableDevice - redirectUrl = location.pathname; - } else if (!requestedParamsAreValid) { - // Corner case: requested params are not valid - // Neither device or network doesn't exists - redirectUrl = location.pathname; - // postActions.push( RouterActions.selectFirstAvailableDevice() ); - } else if (requestedParams.device) { - if (!api.dispatch( RouterActions.deviceModeValidation(currentParams, requestedParams) )) { - redirectUrl = location.pathname; - console.warn('Device is not in valid mode'); - } - } - } + // compose valid url + const validUrl = api.dispatch( RouterActions.getValidUrl(action) ); + // override action state (to be stored in RouterReducer) + action.payload.state = api.dispatch( RouterActions.pathToParams(validUrl) ); + const redirect = action.payload.pathname !== validUrl; + if (redirect) { + // override action pathname + action.payload.pathname = validUrl; } - 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; - postActions.unshift( replace(url) ) - } - } else { - // override action to keep RouterReducer synchronized - action.payload.state = requestedParams; - } - - // resolve LOCATION_CHANGE action + // pass action next(action); - // resolve all post actions - postActions.forEach((a) => { - api.dispatch(a); - }); + if (redirect) { + // replace invalid url + api.dispatch( replace(validUrl) ); + } return action; }; diff --git a/src/services/WalletService.js b/src/services/WalletService.js index 86dd4089..76719298 100644 --- a/src/services/WalletService.js +++ b/src/services/WalletService.js @@ -25,15 +25,21 @@ import type { */ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { const prevState = api.getState(); - const locationChange: boolean = action.type === LOCATION_CHANGE; - // Application live cycle starts here - if (locationChange) { - const { location } = api.getState().router; - if (!location) { - api.dispatch(WalletActions.init()); - return next(action); - } + // Application live cycle starts HERE! + // when first LOCATION_CHANGE is called router does not have "location" set yet + if (action.type === LOCATION_CHANGE && !prevState.router.location) { + // initialize wallet + api.dispatch(WalletActions.init()); + // set initial url + // TODO: validate if initial url is potentially correct + api.dispatch({ + type: WALLET.SET_INITIAL_URL, + pathname: action.payload.pathname, + state: {}, + }); + // pass action and break process + return next(action); } // pass action @@ -65,10 +71,10 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa if (!api.getState().wallet.ready) return action; // double verification needed - // Corner case: LOCATION_CHANGE was called but pathname didn't changed (redirection in RouterService) + // Corner case: LOCATION_CHANGE was called but pathname didn't changed (redirection from RouterService) const prevLocation = prevState.router.location; const currentLocation = api.getState().router.location; - if (locationChange && prevLocation.pathname !== currentLocation.pathname) { + if (action.type === LOCATION_CHANGE && prevLocation.pathname !== currentLocation.pathname) { // watch for coin change if (prevLocation.state.network !== currentLocation.state.network) { api.dispatch({ @@ -80,7 +86,7 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa } // watch for account change - if (prevLocation.state.account !== currentLocation.state.account) { + if (prevLocation.state.network !== currentLocation.state.network || prevLocation.state.account !== currentLocation.state.account) { api.dispatch(SelectedAccountActions.dispose()); }