1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-24 09:18:09 +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, 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

View File

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

View File

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