1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-28 03:08:30 +00:00

implement RouterActions in services and other actions

This commit is contained in:
Szymon Lesisz 2018-09-20 18:26:45 +02:00
parent 8009ec647f
commit 900c3961cd
4 changed files with 163 additions and 235 deletions

View File

@ -6,8 +6,7 @@ import * as CONNECT from 'actions/constants/TrezorConnect';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import { getDuplicateInstanceNumber } from 'reducers/utils'; import { getDuplicateInstanceNumber } from 'reducers/utils';
import * as RouterActions from 'actions/RouterActions';
import { push } from 'react-router-redux';
import type { import type {
DeviceMessage, DeviceMessage,
@ -79,14 +78,6 @@ export type TrezorConnectAction = {
type: typeof CONNECT.STOP_ACQUIRING, type: typeof CONNECT.STOP_ACQUIRING,
}; };
const sortDevices = (devices: Array<TrezorDevice>): Array<TrezorDevice> => devices.sort((a, b) => {
if (!a.ts || !b.ts) {
return -1;
}
return a.ts > b.ts ? -1 : 1;
});
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
// set listeners // set listeners
TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => { TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => {
@ -132,7 +123,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
try { try {
await TrezorConnect.init({ await TrezorConnect.init({
transportReconnect: true, transportReconnect: true,
debug: true, debug: false,
popup: false, popup: false,
webusb: true, webusb: true,
pendingTransportEvent: (getState().devices.length < 1), pendingTransportEvent: (getState().devices.length < 1),
@ -145,67 +136,11 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
} }
}; };
// selection from Aside dropdown button
// after device_connect event
// or after acquiring device
// device type could be local TrezorDevice or Device (from trezor-connect device_connect event)
export const onSelectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
// || device.isUsedElsewhere
// switch to initial url and reset this value
if (!device.features) {
dispatch(push(`/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`));
} else if (device.features.bootloader_mode) {
dispatch(push(`/device/${device.path}/bootloader`));
} else if (!device.features.initialized) {
dispatch(push(`/device/${device.features.device_id}/initialize`));
} else if (typeof device.instance === 'number') {
dispatch(push(`/device/${device.features.device_id}:${device.instance}`));
} else {
const deviceId: string = device.features.device_id;
const urlParams: RouterLocationState = getState().router.location.state;
// let url: string = `/device/${ device.features.device_id }/network/ethereum/account/0`;
let url: string = `/device/${deviceId}`;
let instance: ?number;
// check if device is not TrezorDevice type
if (!device.hasOwnProperty('ts')) {
// its device from trezor-connect (called in initConnectedDevice triggered by device_connect event)
// need to lookup if there are unavailable instances
const available: Array<TrezorDevice> = getState().devices.filter(d => d.path === device.path);
const latest: Array<TrezorDevice> = sortDevices(available);
if (latest.length > 0 && latest[0].instance) {
url += `:${latest[0].instance}`;
instance = latest[0].instance;
}
}
// check if current location is not set to this device
//dispatch( push(`/device/${ device.features.device_id }/network/etc/account/0`) );
if (urlParams.deviceInstance !== instance || urlParams.device !== deviceId) {
dispatch(push(url));
}
}
};
export const initConnectedDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch/* , getState: GetState */): void => {
// const selected = getState().wallet.selectedDevice;
// if (!selected || (selected && selected.state)) {
dispatch(onSelectDevice(device));
// }
// if (device.unacquired && selected && selected.path !== device.path && !selected.connected) {
// dispatch( onSelectDevice(device) );
// } else if (!selected) {
// dispatch( onSelectDevice(device) );
// }
};
// called after backend was initialized // called after backend was initialized
// set listeners for connect/disconnect // set listeners for connect/disconnect
export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const handleDeviceConnect = (device: Device) => { const handleDeviceConnect = (device: Device) => {
dispatch(initConnectedDevice(device)); dispatch( RouterActions.selectDevice(device) );
}; };
TrezorConnect.off(DEVICE.CONNECT, handleDeviceConnect); TrezorConnect.off(DEVICE.CONNECT, handleDeviceConnect);
@ -216,56 +151,13 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetSta
const { devices } = getState(); const { devices } = getState();
const { initialPathname, initialParams } = getState().wallet; // try to redirect to initial url
if (!dispatch( RouterActions.setInitialUrl() )) {
if (initialPathname) { // if initial redirection fails try to switch to first available device
dispatch({ dispatch( RouterActions.selectFirstAvailableDevice() )
type: WALLET.SET_INITIAL_URL,
// pathname: null,
// params: null
});
}
if (devices.length > 0) {
const unacquired: ?TrezorDevice = devices.find(d => d.features);
if (unacquired) {
dispatch(onSelectDevice(unacquired));
} else {
const latest: Array<TrezorDevice> = sortDevices(devices);
const firstConnected: ?TrezorDevice = latest.find(d => d.connected);
dispatch(onSelectDevice(firstConnected || latest[0]));
// TODO
if (initialParams) {
if (!initialParams.hasOwnProperty('network') && initialPathname !== getState().router.location.pathname) {
// dispatch( push(initialPathname) );
}
}
}
} }
}; };
export const switchToFirstAvailableDevice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { devices } = getState();
if (devices.length > 0) {
// TODO: Priority:
// 1. First Unacquired
// 2. First connected
// 3. Saved with latest timestamp
const unacquired = devices.find(d => !d.features);
if (unacquired) {
dispatch(initConnectedDevice(unacquired));
} else {
const latest: Array<TrezorDevice> = sortDevices(devices);
const firstConnected: ?TrezorDevice = latest.find(d => d.connected);
dispatch(onSelectDevice(firstConnected || latest[0]));
}
} else {
dispatch(push('/'));
}
};
export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (selected if (selected
@ -391,13 +283,6 @@ export function acquire(): AsyncAction {
}; };
} }
export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispatch: Dispatch): void => {
if (device.features) {
const devUrl: string = `${device.features.device_id}${device.instance ? `:${device.instance}` : ''}`;
dispatch(push(`/device/${devUrl}/settings`));
}
};
// called from Aside - device menu (forget single instance) // called from Aside - device menu (forget single instance)
export const forget = (device: TrezorDevice): Action => ({ export const forget = (device: TrezorDevice): Action => ({
type: CONNECT.FORGET_REQUEST, type: CONNECT.FORGET_REQUEST,

View File

@ -1,42 +1,21 @@
/* @flow */ /* @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 CONNECT from 'actions/constants/TrezorConnect';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as NotificationActions from 'actions/NotificationActions'; import * as NotificationActions from 'actions/NotificationActions';
import * as RouterActions from 'actions/RouterActions';
import type { import type {
Middleware, Middleware,
MiddlewareAPI, MiddlewareAPI,
MiddlewareDispatch, MiddlewareDispatch,
Action, Action,
ThunkAction,
RouterLocationState, RouterLocationState,
TrezorDevice, TrezorDevice,
} from 'flowtype'; } 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 => { const validation = (api: MiddlewareAPI, params: RouterLocationState): boolean => {
if (params.hasOwnProperty('device')) { if (params.hasOwnProperty('device')) {
const { devices } = api.getState(); const { devices } = api.getState();
@ -64,93 +43,148 @@ const validation = (api: MiddlewareAPI, params: RouterLocationState): boolean =>
return true; return true;
}; };
*/
let __unloading: boolean = false; 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;
return true;
}
/**
* 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);
}
action.payload.state = api.dispatch( RouterActions.pathToParams(action.payload.pathname) );
next(action);
return action;
}
const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
if (action.type === WALLET.ON_BEFORE_UNLOAD) { // make sure that middleware should process this action
__unloading = true; if (action.type !== LOCATION_CHANGE || api.getState().wallet.unloading) {
} else if (action.type === LOCATION_CHANGE && !__unloading) { // Pass action through by default
const { location } = api.getState().router; 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 { devices } = api.getState();
const { ready } = api.getState().wallet;
const { error } = api.getState().connect; 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) );
const requestedParams: RouterLocationState = pathToParams(action.payload.pathname); if (isModalOpened && urlChanged && currentParamsAreValid) {
const currentParams: RouterLocationState = pathToParams(location ? location.pathname : '/'); // Corner case: modal is opened and currentParams are still valid
const postActions: Array<Action> = []; // example 1 (valid blocking): url changed while passphrase modal opened but device is still connected (we want user to finish this action)
let redirectPath: ?string;
// first event after application loads
if (!location) {
postActions.push({
type: WALLET.SET_INITIAL_URL,
pathname: action.payload.pathname,
state: requestedParams,
});
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;
// 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)
// example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect // example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect
if (isModalOpened && action.payload.pathname !== location.pathname && validation(api, currentParams)) { redirectUrl = location.pathname;
redirectPath = location.pathname; console.warn('Modal still opened');
console.warn('Modal still opened'); } else if (shouldDisplayLandingPage) {
} else if (landingPage) { if (!isLandingPageUrl) {
// keep route on landing page // keep route on landing page
if (action.payload.pathname !== '/' && action.payload.pathname !== '/bridge') { redirectUrl = '/';
redirectPath = '/'; }
} } else {
} else { // Process regular url change during application live cycle
// PATH VALIDATION const requestedParamsAreValid: boolean = api.dispatch( RouterActions.paramsValidation(requestedParams) );
// redirect from root view if (isLandingPageUrl) {
if (action.payload.pathname === '/' || !validation(api, requestedParams)) { // Corner case: disallow displaying landing page
// TODO redirect to first device // redirect to previous url
redirectPath = '/device/x'; // TODO: && currentParamsAreValid
} else if (requestedParams.device) { redirectUrl = location.pathname;
if (requestedParams.network !== currentParams.network) { } else if (!requestedParamsAreValid) {
postActions.push({ // Corner case: requested params are not valid
type: CONNECT.COIN_CHANGED, // Neither device or network doesn't exists
payload: { postActions.push( RouterActions.selectFirstAvailableDevice() );
network: requestedParams.network, } 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);
action.payload.pathname = url;
// change url
// api.dispatch(replace(url));
} else {
action.payload.state = requestedParams;
}
// resolve LOCATION_CHANGE action
next(action);
// resolve post actions
postActions.forEach((a) => {
api.dispatch(a);
});
api.dispatch(NotificationActions.clear(currentParams, requestedParams));
return action;
} }
// Pass all actions through by default if (redirectUrl) {
return next(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);
// resolve all post actions
postActions.forEach((a) => {
api.dispatch(a);
});
// TODO: move this to wallet service?
api.dispatch(NotificationActions.clear(currentParams, requestedParams));
return action;
}; };
export default RouterService; export default RouterService;

View File

@ -1,5 +1,4 @@
/* @flow */ /* @flow */
import { push } from 'react-router-redux';
import TrezorConnect, { import TrezorConnect, {
TRANSPORT, DEVICE_EVENT, UI_EVENT, UI, DEVICE, BLOCKCHAIN TRANSPORT, DEVICE_EVENT, UI_EVENT, UI, DEVICE, BLOCKCHAIN
@ -7,6 +6,7 @@ import TrezorConnect, {
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as DiscoveryActions from 'actions/DiscoveryActions';
import * as BlockchainActions from 'actions/BlockchainActions'; import * as BlockchainActions from 'actions/BlockchainActions';
import * as RouterActions from 'actions/RouterActions';
import * as ModalActions from 'actions/ModalActions'; import * as ModalActions from 'actions/ModalActions';
import * as STORAGE from 'actions/constants/localStorage'; import * as STORAGE from 'actions/constants/localStorage';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
@ -31,7 +31,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
api.dispatch(TrezorConnectActions.init()); api.dispatch(TrezorConnectActions.init());
} else if (action.type === TRANSPORT.ERROR) { } else if (action.type === TRANSPORT.ERROR) {
// TODO: check if modal is open // TODO: check if modal is open
// api.dispatch( push('/') ); // api.dispatch( RouterActions.gotoLandingPage() );
} else if (action.type === TRANSPORT.START) { } else if (action.type === TRANSPORT.START) {
api.dispatch(BlockchainActions.init()); api.dispatch(BlockchainActions.init());
} else if (action.type === BLOCKCHAIN_READY) { } else if (action.type === BLOCKCHAIN_READY) {
@ -42,7 +42,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
api.dispatch(ModalActions.onRememberRequest(prevModalState)); api.dispatch(ModalActions.onRememberRequest(prevModalState));
} else if (action.type === CONNECT.FORGET) { } else if (action.type === CONNECT.FORGET) {
//api.dispatch( TrezorConnectActions.forgetDevice(action.device) ); //api.dispatch( TrezorConnectActions.forgetDevice(action.device) );
api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice()); api.dispatch( RouterActions.selectFirstAvailableDevice() );
} else if (action.type === CONNECT.FORGET_SINGLE) { } else if (action.type === CONNECT.FORGET_SINGLE) {
if (api.getState().devices.length < 1 && action.device.connected) { if (api.getState().devices.length < 1 && action.device.connected) {
// prompt disconnect device info in LandingPage // prompt disconnect device info in LandingPage
@ -50,9 +50,9 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
type: CONNECT.DISCONNECT_REQUEST, type: CONNECT.DISCONNECT_REQUEST,
device: action.device, device: action.device,
}); });
api.dispatch(push('/')); api.dispatch( RouterActions.gotoLandingPage() );
} else { } else {
api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice()); api.dispatch( RouterActions.selectFirstAvailableDevice() );
} }
} else if (action.type === DEVICE.CONNECT || action.type === DEVICE.CONNECT_UNACQUIRED) { } else if (action.type === DEVICE.CONNECT || action.type === DEVICE.CONNECT_UNACQUIRED) {
api.dispatch(DiscoveryActions.restore()); api.dispatch(DiscoveryActions.restore());
@ -60,7 +60,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
} else if (action.type === CONNECT.AUTH_DEVICE) { } else if (action.type === CONNECT.AUTH_DEVICE) {
api.dispatch(DiscoveryActions.check()); api.dispatch(DiscoveryActions.check());
} else if (action.type === CONNECT.DUPLICATE) { } else if (action.type === CONNECT.DUPLICATE) {
api.dispatch(TrezorConnectActions.onSelectDevice(action.device)); api.dispatch(RouterActions.selectDevice(action.device));
} else if (action.type === CONNECT.COIN_CHANGED) { } else if (action.type === CONNECT.COIN_CHANGED) {
api.dispatch(TrezorConnectActions.coinChanged(action.payload.network)); api.dispatch(TrezorConnectActions.coinChanged(action.payload.network));
} else if (action.type === BLOCKCHAIN.BLOCK) { } else if (action.type === BLOCKCHAIN.BLOCK) {

View File

@ -6,6 +6,7 @@ import { LOCATION_CHANGE } from 'react-router-redux';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as WalletActions from 'actions/WalletActions'; import * as WalletActions from 'actions/WalletActions';
import * as RouterActions from 'actions/RouterActions';
import * as LocalStorageActions from 'actions/LocalStorageActions'; import * as LocalStorageActions from 'actions/LocalStorageActions';
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as SelectedAccountActions from 'actions/SelectedAccountActions'; import * as SelectedAccountActions from 'actions/SelectedAccountActions';
@ -29,11 +30,14 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
const { location } = api.getState().router; const { location } = api.getState().router;
if (!location) { if (!location) {
api.dispatch(WalletActions.init()); api.dispatch(WalletActions.init());
// load data from config.json and local storage
api.dispatch(LocalStorageActions.loadData());
} }
} }
if (action.type === WALLET.SET_INITIAL_URL) {
// load data from config.json and local storage
api.dispatch(LocalStorageActions.loadData());
}
// pass action // pass action
next(action); next(action);
@ -41,18 +45,23 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device)); api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device));
} }
// update common values in WallerReducer // update common values ONLY if application is ready
api.dispatch(WalletActions.updateSelectedValues(prevState, action)); if (api.getState().wallet.ready) {
// update common values in WallerReducer
api.dispatch(WalletActions.updateSelectedValues(prevState, action));
// update common values in SelectedAccountReducer // update common values in SelectedAccountReducer
api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action)); api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action));
}
// selected device changed // handle selected device change
if (action.type === WALLET.SET_SELECTED_DEVICE) { if (action.type === WALLET.SET_SELECTED_DEVICE) {
if (action.device) { if (action.device) {
// try to authorize device
api.dispatch(TrezorConnectActions.getSelectedDeviceState()); api.dispatch(TrezorConnectActions.getSelectedDeviceState());
} else { } else {
api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice()); // try select different device
api.dispatch(RouterActions.selectFirstAvailableDevice());
} }
} }