1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-12-25 00:18:07 +00:00

refactoring RouterService

This commit is contained in:
Szymon Lesisz 2018-09-20 23:05:48 +02:00
parent 6405c13da8
commit 9ecbdc5e38
3 changed files with 132 additions and 117 deletions

View File

@ -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<boo
}
// validate requested account
// TODO: check if discovery on this network is completed
// TODO: only if discovery on this network is completed
// if (params.hasOwnProperty('account')) {
// }
return true;
}
export const deviceModeValidation = (current: RouterLocationState, requested: RouterLocationState): PayloadAction<boolean> => (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<string> => (dispatch: Dispatch, getState: GetState): string => {
return "/";
export const paramsToPath = (params: RouterLocationState): PayloadAction<?string> => (dispatch: Dispatch, getState: GetState): ?string => {
// TODO: share this patterns with /views/index.js
const patterns: Array<Array<string>> = [
['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<string> = Object.keys(params);
let patternToUse: ?Array<string>;
for (let pattern of patterns) {
const match: Array<string> = 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<string> => (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

View File

@ -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<Action | ThunkAction> = [];
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;
};

View File

@ -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());
}