|
|
|
@ -1,42 +1,21 @@
|
|
|
|
|
/* @flow */
|
|
|
|
|
import { LOCATION_CHANGE/* , replace */ } from 'react-router-redux';
|
|
|
|
|
import { LOCATION_CHANGE, replace } from 'react-router-redux';
|
|
|
|
|
import * as CONNECT from 'actions/constants/TrezorConnect';
|
|
|
|
|
import * as WALLET from 'actions/constants/wallet';
|
|
|
|
|
import * as NotificationActions from 'actions/NotificationActions';
|
|
|
|
|
import * as RouterActions from 'actions/RouterActions';
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
Middleware,
|
|
|
|
|
MiddlewareAPI,
|
|
|
|
|
MiddlewareDispatch,
|
|
|
|
|
Action,
|
|
|
|
|
ThunkAction,
|
|
|
|
|
RouterLocationState,
|
|
|
|
|
TrezorDevice,
|
|
|
|
|
} from 'flowtype';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Middleware used for init application and managing router path.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const pathToParams = (path: string): RouterLocationState => {
|
|
|
|
|
const urlParts: Array<string> = path.split('/').slice(1);
|
|
|
|
|
const params: RouterLocationState = {};
|
|
|
|
|
if (urlParts.length < 1 || path === '/') return params;
|
|
|
|
|
|
|
|
|
|
for (let i = 0, len = urlParts.length; i < len; i += 2) {
|
|
|
|
|
params[urlParts[i]] = urlParts[i + 1] || urlParts[i];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (params.hasOwnProperty('device')) {
|
|
|
|
|
const isClonedDevice: Array<string> = params.device.split(':');
|
|
|
|
|
if (isClonedDevice.length > 1) {
|
|
|
|
|
params.device = isClonedDevice[0];
|
|
|
|
|
params.deviceInstance = isClonedDevice[1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return params;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
const validation = (api: MiddlewareAPI, params: RouterLocationState): boolean => {
|
|
|
|
|
if (params.hasOwnProperty('device')) {
|
|
|
|
|
const { devices } = api.getState();
|
|
|
|
@ -64,93 +43,148 @@ const validation = (api: MiddlewareAPI, params: RouterLocationState): boolean =>
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const deviceModeValidation = (api: MiddlewareAPI, current: RouterLocationState, requested: RouterLocationState): 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 } = api.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;
|
|
|
|
|
|
|
|
|
|
let __unloading: boolean = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
|
|
|
|
if (action.type === WALLET.ON_BEFORE_UNLOAD) {
|
|
|
|
|
__unloading = true;
|
|
|
|
|
} else if (action.type === LOCATION_CHANGE && !__unloading) {
|
|
|
|
|
const { location } = api.getState().router;
|
|
|
|
|
const { devices } = api.getState();
|
|
|
|
|
const { error } = api.getState().connect;
|
|
|
|
|
/**
|
|
|
|
|
* Redux Middleware used for managing router path
|
|
|
|
|
* This middleware couldn't use async/await because LOCATION_CHANGE action is also synchronized with RouterReducer (react-router-redux)
|
|
|
|
|
*/
|
|
|
|
|
const RouterService1: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
|
|
|
|
if (action.type !== LOCATION_CHANGE) {
|
|
|
|
|
return next(action);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const requestedParams: RouterLocationState = pathToParams(action.payload.pathname);
|
|
|
|
|
const currentParams: RouterLocationState = pathToParams(location ? location.pathname : '/');
|
|
|
|
|
const postActions: Array<Action> = [];
|
|
|
|
|
action.payload.state = api.dispatch( RouterActions.pathToParams(action.payload.pathname) );
|
|
|
|
|
|
|
|
|
|
let redirectPath: ?string;
|
|
|
|
|
// first event after application loads
|
|
|
|
|
if (!location) {
|
|
|
|
|
postActions.push({
|
|
|
|
|
type: WALLET.SET_INITIAL_URL,
|
|
|
|
|
pathname: action.payload.pathname,
|
|
|
|
|
state: requestedParams,
|
|
|
|
|
});
|
|
|
|
|
next(action);
|
|
|
|
|
|
|
|
|
|
redirectPath = '/';
|
|
|
|
|
} else {
|
|
|
|
|
const isModalOpened: boolean = api.getState().modal.opened;
|
|
|
|
|
// there are no devices attached or initialization error occurs
|
|
|
|
|
const landingPage: boolean = devices.length < 1 || error !== null;
|
|
|
|
|
return action;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// modal is still opened and currentPath is still valid
|
|
|
|
|
// example 1 (valid blocking): url changes while passphrase modal opened but device is still connected (we want user to finish this action)
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
if (isModalOpened && action.payload.pathname !== location.pathname && validation(api, currentParams)) {
|
|
|
|
|
redirectPath = location.pathname;
|
|
|
|
|
console.warn('Modal still opened');
|
|
|
|
|
} else if (landingPage) {
|
|
|
|
|
redirectUrl = location.pathname;
|
|
|
|
|
console.warn('Modal still opened');
|
|
|
|
|
} else if (shouldDisplayLandingPage) {
|
|
|
|
|
if (!isLandingPageUrl) {
|
|
|
|
|
// keep route on landing page
|
|
|
|
|
if (action.payload.pathname !== '/' && action.payload.pathname !== '/bridge') {
|
|
|
|
|
redirectPath = '/';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// PATH VALIDATION
|
|
|
|
|
// redirect from root view
|
|
|
|
|
if (action.payload.pathname === '/' || !validation(api, requestedParams)) {
|
|
|
|
|
// TODO redirect to first device
|
|
|
|
|
redirectPath = '/device/x';
|
|
|
|
|
} else if (requestedParams.device) {
|
|
|
|
|
if (requestedParams.network !== currentParams.network) {
|
|
|
|
|
postActions.push({
|
|
|
|
|
type: CONNECT.COIN_CHANGED,
|
|
|
|
|
payload: {
|
|
|
|
|
network: requestedParams.network,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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: && currentParamsAreValid
|
|
|
|
|
redirectUrl = location.pathname;
|
|
|
|
|
} else if (!requestedParamsAreValid) {
|
|
|
|
|
// Corner case: requested params are not valid
|
|
|
|
|
// Neither device or network doesn't exists
|
|
|
|
|
postActions.push( RouterActions.selectFirstAvailableDevice() );
|
|
|
|
|
} else if (requestedParams.device) {
|
|
|
|
|
if (!deviceModeValidation(api, currentParams, requestedParams)) {
|
|
|
|
|
redirectUrl = location.pathname;
|
|
|
|
|
console.warn('Device is not in valid mode');
|
|
|
|
|
} else if (requestedParams.network !== currentParams.network) {
|
|
|
|
|
postActions.push({
|
|
|
|
|
type: CONNECT.COIN_CHANGED,
|
|
|
|
|
payload: {
|
|
|
|
|
network: requestedParams.network,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (redirectPath) {
|
|
|
|
|
console.warn('Redirecting...', redirectPath);
|
|
|
|
|
// override action to keep routerReducer sync
|
|
|
|
|
const url: string = redirectPath;
|
|
|
|
|
action.payload.state = pathToParams(url);
|
|
|
|
|
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;
|
|
|
|
|
// change url
|
|
|
|
|
// api.dispatch(replace(url));
|
|
|
|
|
} else {
|
|
|
|
|
action.payload.state = requestedParams;
|
|
|
|
|
postActions.unshift( replace(url) )
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// override action to keep RouterReducer synchronized
|
|
|
|
|
action.payload.state = requestedParams;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolve LOCATION_CHANGE action
|
|
|
|
|
next(action);
|
|
|
|
|
// resolve LOCATION_CHANGE action
|
|
|
|
|
next(action);
|
|
|
|
|
|
|
|
|
|
// resolve post actions
|
|
|
|
|
postActions.forEach((a) => {
|
|
|
|
|
api.dispatch(a);
|
|
|
|
|
});
|
|
|
|
|
// resolve all post actions
|
|
|
|
|
postActions.forEach((a) => {
|
|
|
|
|
api.dispatch(a);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
api.dispatch(NotificationActions.clear(currentParams, requestedParams));
|
|
|
|
|
// TODO: move this to wallet service?
|
|
|
|
|
api.dispatch(NotificationActions.clear(currentParams, requestedParams));
|
|
|
|
|
|
|
|
|
|
return action;
|
|
|
|
|
}
|
|
|
|
|
return action;
|
|
|
|
|
|
|
|
|
|
// Pass all actions through by default
|
|
|
|
|
return next(action);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default RouterService;
|