mirror of
https://github.com/trezor/trezor-wallet
synced 2024-11-24 09:18:09 +00:00
refactoring RouterService
This commit is contained in:
parent
6405c13da8
commit
9ecbdc5e38
@ -11,6 +11,7 @@ import type {
|
|||||||
Dispatch,
|
Dispatch,
|
||||||
GetState,
|
GetState,
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
|
import type { RouterAction } from 'react-router-redux';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Parse url string to RouterLocationState object (key/value)
|
* Parse url string to RouterLocationState object (key/value)
|
||||||
@ -67,39 +68,118 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction<boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate requested account
|
// 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')) {
|
// if (params.hasOwnProperty('account')) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
return true;
|
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
|
* Composing url string from given RouterLocationState object
|
||||||
* Filters unrecognized fields and sorting in correct order
|
* Filters unrecognized fields and sorting in correct order
|
||||||
*/
|
*/
|
||||||
export const paramsToPath = (params: RouterLocationState): PayloadAction<string> => (dispatch: Dispatch, getState: GetState): string => {
|
export const paramsToPath = (params: RouterLocationState): PayloadAction<?string> => (dispatch: Dispatch, getState: GetState): ?string => {
|
||||||
return "/";
|
// 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"
|
* Utility used in "selectDevice" and "selectFirstAvailableDevice"
|
||||||
* sorting device array by "ts" (timestamp) field
|
* sorting device array by "ts" (timestamp) field
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
import { LOCATION_CHANGE, replace } from 'react-router-redux';
|
import { LOCATION_CHANGE, replace } from 'react-router-redux';
|
||||||
import * as WALLET from 'actions/constants/wallet';
|
|
||||||
import * as RouterActions from 'actions/RouterActions';
|
import * as RouterActions from 'actions/RouterActions';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -21,97 +20,27 @@ import type {
|
|||||||
const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
||||||
// make sure that middleware should process this action
|
// make sure that middleware should process this action
|
||||||
if (action.type !== LOCATION_CHANGE || api.getState().wallet.unloading) {
|
if (action.type !== LOCATION_CHANGE || api.getState().wallet.unloading) {
|
||||||
// Pass action through by default
|
// pass action
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { location } = api.getState().router;
|
// compose valid url
|
||||||
const requestedUrl: string = action.payload.pathname;
|
const validUrl = api.dispatch( RouterActions.getValidUrl(action) );
|
||||||
// map current and incoming url into RouterLocationState object
|
// override action state (to be stored in RouterReducer)
|
||||||
const currentParams = api.dispatch( RouterActions.pathToParams(location ? location.pathname : '/') );
|
action.payload.state = api.dispatch( RouterActions.pathToParams(validUrl) );
|
||||||
const requestedParams = api.dispatch( RouterActions.pathToParams(requestedUrl) );
|
const redirect = action.payload.pathname !== validUrl;
|
||||||
// postActions will be dispatched AFTER propagation of LOCATION_CHANGE action using next(action) below
|
if (redirect) {
|
||||||
const postActions: Array<Action | ThunkAction> = [];
|
// override action pathname
|
||||||
const urlChanged: boolean = location ? requestedUrl !== location.pathname : true;
|
action.payload.pathname = validUrl;
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectUrl) {
|
// pass action
|
||||||
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
|
|
||||||
next(action);
|
next(action);
|
||||||
|
|
||||||
// resolve all post actions
|
if (redirect) {
|
||||||
postActions.forEach((a) => {
|
// replace invalid url
|
||||||
api.dispatch(a);
|
api.dispatch( replace(validUrl) );
|
||||||
});
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
};
|
};
|
||||||
|
@ -25,16 +25,22 @@ import type {
|
|||||||
*/
|
*/
|
||||||
const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
||||||
const prevState = api.getState();
|
const prevState = api.getState();
|
||||||
const locationChange: boolean = action.type === LOCATION_CHANGE;
|
|
||||||
|
|
||||||
// Application live cycle starts here
|
// Application live cycle starts HERE!
|
||||||
if (locationChange) {
|
// when first LOCATION_CHANGE is called router does not have "location" set yet
|
||||||
const { location } = api.getState().router;
|
if (action.type === LOCATION_CHANGE && !prevState.router.location) {
|
||||||
if (!location) {
|
// initialize wallet
|
||||||
api.dispatch(WalletActions.init());
|
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);
|
return next(action);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// pass action
|
// pass action
|
||||||
next(action);
|
next(action);
|
||||||
@ -65,10 +71,10 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
|
|||||||
if (!api.getState().wallet.ready) return action;
|
if (!api.getState().wallet.ready) return action;
|
||||||
|
|
||||||
// double verification needed
|
// 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 prevLocation = prevState.router.location;
|
||||||
const currentLocation = api.getState().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
|
// watch for coin change
|
||||||
if (prevLocation.state.network !== currentLocation.state.network) {
|
if (prevLocation.state.network !== currentLocation.state.network) {
|
||||||
api.dispatch({
|
api.dispatch({
|
||||||
@ -80,7 +86,7 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// watch for account change
|
// 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());
|
api.dispatch(SelectedAccountActions.dispose());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user