mirror of
https://github.com/trezor/trezor-wallet
synced 2024-12-25 00:18:07 +00:00
refactoring RouterService
This commit is contained in:
parent
6405c13da8
commit
9ecbdc5e38
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user