1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-24 09:18:09 +00:00

Merge pull request #69 from satoshilabs/fix/router

Fix/router
This commit is contained in:
Vladimir Volek 2018-09-21 14:05:19 +02:00 committed by GitHub
commit ddc190d6dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 571 additions and 360 deletions

View File

@ -12,6 +12,7 @@
"jest": true "jest": true
}, },
"rules": { "rules": {
"no-use-before-define": 0,
"no-plusplus": 0, "no-plusplus": 0,
"class-methods-use-this": 0, "class-methods-use-this": 0,
"react/require-default-props": 0, "react/require-default-props": 0,

View File

@ -21,7 +21,7 @@ export type StorageAction = {
type: typeof STORAGE.READY, type: typeof STORAGE.READY,
config: Config, config: Config,
tokens: TokensCollection, tokens: TokensCollection,
ERC20Abi: Array<Object> ERC20Abi: Array<TokensCollection>
} | { } | {
type: typeof STORAGE.SAVE, type: typeof STORAGE.SAVE,
network: string, network: string,
@ -148,7 +148,6 @@ export function loadTokensFromJSON(): AsyncAction {
}); });
} }
dispatch({ dispatch({
type: STORAGE.READY, type: STORAGE.READY,
config, config,

View File

@ -0,0 +1,304 @@
/* @flow */
import { push, LOCATION_CHANGE } from 'react-router-redux';
import { routes } from 'support/routes';
import type {
RouterLocationState,
Device,
TrezorDevice,
ThunkAction,
PayloadAction,
Dispatch,
GetState,
} from 'flowtype';
import type { RouterAction } from 'react-router-redux';
/*
* Parse url string to RouterLocationState object (key/value)
*/
export const pathToParams = (path: string): PayloadAction<RouterLocationState> => (): RouterLocationState => {
// split url into parts
const parts: Array<string> = path.split('/').slice(1);
const params: RouterLocationState = {};
// return empty params
if (parts.length < 1 || path === '/') return params;
// map parts to params by key/value
// assuming that url is in format: "/key/value"
for (let i = 0, len = parts.length; i < len; i += 2) {
params[parts[i]] = parts[i + 1] || parts[i];
}
// check for special case: /device/device-id:instance-id
if (params.hasOwnProperty('device')) {
const isClonedDevice: Array<string> = params.device.split(':');
if (isClonedDevice.length > 1) {
const [device, instance] = isClonedDevice;
params.device = device;
params.deviceInstance = instance;
}
}
return params;
};
/*
* RouterLocationState validation
* Check if requested device or network exists in reducers
*/
export const paramsValidation = (params: RouterLocationState): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
// validate requested device
if (params.hasOwnProperty('device')) {
const { devices } = getState();
let device: ?TrezorDevice;
if (params.hasOwnProperty('deviceInstance')) {
device = devices.find(d => d.features && d.features.device_id === params.device && d.instance === parseInt(params.deviceInstance, 10));
} else {
device = devices.find(d => d.path === params.device || (d.features && d.features.device_id === params.device));
}
if (!device) return false;
}
// validate requested network
if (params.hasOwnProperty('network')) {
const { config } = getState().localStorage;
const coin = config.coins.find(c => c.network === params.network);
if (!coin) return false;
if (!params.account) return false;
}
// validate requested account
// TODO: only if discovery on this network is completed
// if (params.hasOwnProperty('account')) {
// }
return true;
};
/*
* Composing url string from given RouterLocationState object
* Filters unrecognized fields and sorting in correct order
*/
export const paramsToPath = (params: RouterLocationState): PayloadAction<?string> => (): ?string => {
// get patterns (fields) from routes and sort them by complexity
const patterns: Array<Array<string>> = routes.map(r => r.fields).sort((a, b) => (a.length > b.length ? -1 : 1));
// find pattern
const keys: Array<string> = Object.keys(params);
let patternToUse: ?Array<string>;
let i: number;
for (i = 0; i < patterns.length; i++) {
const pattern = patterns[i];
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
*/
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;
});
/*
* Compose url from given device object and redirect
*/
export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
let url: ?string;
if (!device.features) {
url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`;
} else if (device.features.bootloader_mode) {
url = `/device/${device.path}/bootloader`;
} else if (!device.features.initialized) {
url = `/device/${device.features.device_id}/initialize`;
} else if (typeof device.instance === 'number') {
url = `/device/${device.features.device_id}:${device.instance}`;
} else {
url = `/device/${device.features.device_id}`;
// make sure that device is not TrezorDevice type
if (!device.hasOwnProperty('ts')) {
// it is device from trezor-connect 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}`;
}
}
}
const currentParams: RouterLocationState = getState().router.location.state;
const requestedParams = dispatch(pathToParams(url));
if (currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) {
dispatch(goto(url));
}
};
/*
* Try to find first available device using order:
* 1. First unacquired
* 2. First connected
* 3. Saved with latest timestamp
* OR redirect to landing page
*/
export const selectFirstAvailableDevice = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const { devices } = getState();
if (devices.length > 0) {
const unacquired = devices.find(d => !d.features);
if (unacquired) {
dispatch(selectDevice(unacquired));
} else {
const latest: Array<TrezorDevice> = sortDevices(devices);
const firstConnected: ?TrezorDevice = latest.find(d => d.connected);
dispatch(selectDevice(firstConnected || latest[0]));
}
} else {
dispatch(gotoLandingPage());
}
};
/*
* Internal method. redirect to given url
*/
const goto = (url: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
if (getState().router.location.pathname !== url) {
dispatch(push(url));
}
};
/*
* Check if requested OR current url is landing page
*/
export const isLandingPageUrl = ($url?: string): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
let url: ?string = $url;
if (typeof url !== 'string') {
url = getState().router.location.pathname;
}
// TODO: add more landing page cases/urls to config.json (like /tools etc)
return (url === '/' || url === '/bridge');
};
/*
* Try to redirect to landing page
*/
export const gotoLandingPage = (): ThunkAction => (dispatch: Dispatch): void => {
const isLandingPage = dispatch(isLandingPageUrl());
if (!isLandingPage) {
dispatch(goto('/'));
}
};
/*
* Go to given device settings page
*/
export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispatch: Dispatch): void => {
if (device.features) {
const devUrl: string = `${device.features.device_id}${device.instance ? `:${device.instance}` : ''}`;
dispatch(goto(`/device/${devUrl}/settings`));
}
};
/*
* Try to redirect to initial url
*/
export const setInitialUrl = (): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
const { initialPathname } = getState().wallet;
if (typeof initialPathname === 'string' && !dispatch(isLandingPageUrl(initialPathname))) {
const valid = dispatch(getValidUrl({
type: LOCATION_CHANGE,
payload: {
pathname: initialPathname,
hash: '',
search: '',
state: {},
},
}));
if (valid === initialPathname) {
dispatch(goto(valid));
return true;
}
}
return false;
};

View File

@ -7,6 +7,9 @@ import * as SEND from 'actions/constants/send';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as SendFormActions from 'actions/SendFormActions';
import * as SessionStorageActions from 'actions/SessionStorageActions';
import * as stateUtils from 'reducers/utils'; import * as stateUtils from 'reducers/utils';
import type { import type {
@ -16,8 +19,6 @@ import type {
Dispatch, Dispatch,
State, State,
} from 'flowtype'; } from 'flowtype';
import * as SendFormActions from './SendFormActions';
export type SelectedAccountAction = { export type SelectedAccountAction = {
type: typeof ACCOUNT.DISPOSE, type: typeof ACCOUNT.DISPOSE,
@ -42,17 +43,16 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
// SessionStorageActions.clear(location.pathname); // SessionStorageActions.clear(location.pathname);
} }
if (prevState.sendForm !== state.sendForm) {
dispatch(SessionStorageActions.save());
}
// handle devices state change (from trezor-connect events or location change) // handle devices state change (from trezor-connect events or location change)
if (locationChange if (locationChange
|| prevState.accounts !== state.accounts || prevState.accounts !== state.accounts
|| prevState.discovery !== state.discovery || prevState.discovery !== state.discovery
|| prevState.tokens !== state.tokens || prevState.tokens !== state.tokens
|| prevState.pending !== state.pending) { || prevState.pending !== state.pending) {
if (locationChange) {
// dispose current account view
dispatch(dispose());
}
const account = stateUtils.getSelectedAccount(state); const account = stateUtils.getSelectedAccount(state);
const network = stateUtils.getSelectedNetwork(state); const network = stateUtils.getSelectedNetwork(state);
const discovery = stateUtils.getDiscoveryProcess(state); const discovery = stateUtils.getDiscoveryProcess(state);

View File

@ -1,13 +1,11 @@
/* @flow */ /* @flow */
import TrezorConnect, { import TrezorConnect, {
UI, DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT,
} from 'trezor-connect'; } from 'trezor-connect';
import * as CONNECT from 'actions/constants/TrezorConnect'; 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 { 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,
@ -28,7 +26,6 @@ import type {
AsyncAction, AsyncAction,
Device, Device,
TrezorDevice, TrezorDevice,
RouterLocationState,
} from 'flowtype'; } from 'flowtype';
import * as DiscoveryActions from './DiscoveryActions'; import * as DiscoveryActions from './DiscoveryActions';
@ -79,19 +76,11 @@ 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 => {
// post event to reducers // post event to reducers
const type: DeviceMessageType = event.type; // assert flow type const type: DeviceMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
device: event.payload, device: event.payload,
@ -100,7 +89,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(UI_EVENT, (event: UiMessage): void => { TrezorConnect.on(UI_EVENT, (event: UiMessage): void => {
// post event to reducers // post event to reducers
const type: UiMessageType = event.type; // assert flow type const type: UiMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
payload: event.payload, payload: event.payload,
@ -109,7 +98,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(TRANSPORT_EVENT, (event: TransportMessage): void => { TrezorConnect.on(TRANSPORT_EVENT, (event: TransportMessage): void => {
// post event to reducers // post event to reducers
const type: TransportMessageType = event.type; // assert flow type const type: TransportMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
payload: event.payload, payload: event.payload,
@ -118,21 +107,22 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainMessage): void => { TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainMessage): void => {
// post event to reducers // post event to reducers
const type: BlockchainMessageType = event.type; // assert flow type const type: BlockchainMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
payload: event.payload, payload: event.payload,
}); });
}); });
/* global LOCAL */
// $FlowIssue LOCAL not declared // $FlowIssue LOCAL not declared
window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; // eslint-disable-line no-underscore-dangle
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/'; // window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/'; // eslint-disable-line no-underscore-dangle
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 +135,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): 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);
@ -214,58 +148,13 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetSta
TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect); TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect);
TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect);
const { devices } = getState(); // try to redirect to initial url
if (!dispatch(RouterActions.setInitialUrl())) {
const { initialPathname, initialParams } = getState().wallet; // if initial redirection fails try to switch to first available device
dispatch(RouterActions.selectFirstAvailableDevice());
if (initialPathname) {
dispatch({
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
@ -345,7 +234,7 @@ export const coinChanged = (network: ?string): ThunkAction => (dispatch: Dispatc
}; };
export function reload(): AsyncAction { export function reload(): AsyncAction {
return async (dispatch: Dispatch, getState: GetState): Promise<void> => { return async (): Promise<void> => {
}; };
} }
@ -391,13 +280,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

@ -6,9 +6,9 @@ import type {
MiddlewareAPI as ReduxMiddlewareAPI, MiddlewareAPI as ReduxMiddlewareAPI,
Middleware as ReduxMiddleware, Middleware as ReduxMiddleware,
ThunkAction as ReduxThunkAction, ThunkAction as ReduxThunkAction,
PayloadAction as ReduxPayloadAction,
AsyncAction as ReduxAsyncAction, AsyncAction as ReduxAsyncAction,
PromiseAction as ReduxPromiseAction, PromiseAction as ReduxPromiseAction,
ThunkDispatch as ReduxThunkDispatch,
PlainDispatch as ReduxPlainDispatch, PlainDispatch as ReduxPlainDispatch,
} from 'redux'; } from 'redux';
@ -164,6 +164,7 @@ export type MiddlewareAPI = ReduxMiddlewareAPI<State, Action>;
export type Middleware = ReduxMiddleware<State, Action>; export type Middleware = ReduxMiddleware<State, Action>;
export type ThunkAction = ReduxThunkAction<State, Action>; export type ThunkAction = ReduxThunkAction<State, Action>;
export type PayloadAction<R> = ReduxPayloadAction<State, Action, R>;
export type AsyncAction = ReduxAsyncAction<State, Action>; export type AsyncAction = ReduxAsyncAction<State, Action>;
export type PromiseAction<R> = ReduxPromiseAction<State, Action, R>; export type PromiseAction<R> = ReduxPromiseAction<State, Action, R>;

View File

@ -2,7 +2,6 @@
declare module 'redux' { declare module 'redux' {
/* /*
S = State S = State
A = Action A = Action
D = Dispatch D = Dispatch
@ -16,28 +15,25 @@ declare module 'redux' {
declare export type ThunkAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => void; declare export type ThunkAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => void;
declare export type AsyncAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => Promise<void>; declare export type AsyncAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => Promise<void>;
declare export type PromiseAction<S, A, R> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => Promise<R>; declare export type PromiseAction<S, A, R> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => Promise<R>;
declare export type PayloadAction<S, A, R> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => R;
declare export type ThunkDispatch<S, A> = (action: ThunkAction<S, A>) => void; declare export type ThunkDispatch<S, A> = (action: ThunkAction<S, A>) => void;
declare export type AsyncDispatch<S, A> = (action: AsyncAction<S, A>) => Promise<void>; declare export type AsyncDispatch<S, A> = (action: AsyncAction<S, A>) => Promise<void>;
declare export type PromiseDispatch<S, A> = <R>(action: PromiseAction<S, A, R>) => Promise<R>; declare export type PromiseDispatch<S, A> = <R>(action: PromiseAction<S, A, R>) => Promise<R>;
declare export type PayloadDispatch<S, A> = <R>(action: PayloadAction<S, A, R>) => R;
declare export type PlainDispatch<A: {type: $Subtype<string>}> = DispatchAPI<A>; declare export type PlainDispatch<A: {type: $Subtype<string>}> = DispatchAPI<A>;
/* NEW: Dispatch is now a combination of these different dispatch types */ /* NEW: Dispatch is now a combination of these different dispatch types */
declare export type ReduxDispatch<S, A> = PlainDispatch<A> & ThunkDispatch<S, A> & AsyncDispatch<S, A> & PromiseDispatch<S, A>; declare export type ReduxDispatch<S, A> = PlainDispatch<A> & ThunkDispatch<S, A> & AsyncDispatch<S, A> & PromiseDispatch<S, A> & PayloadDispatch<S, A>;
declare export type MiddlewareAPI<S, A> = { declare export type MiddlewareAPI<S, A> = {
// dispatch: Dispatch<S, A>;
// dispatch: (action: A | ThunkAction<S, A>) => A | void;
// dispatch: PlainDispatch<A> | () => A;
// dispatch: ( A | ThunkAction<S, A> | void ) => void;
dispatch: ReduxDispatch<S, A>; dispatch: ReduxDispatch<S, A>;
// dispatch: Dispatch<S, A>;
// dispatch: D;
getState(): S; getState(): S;
}; };
declare export type Middleware<S, A> = declare export type Middleware<S, A> =
(api: MiddlewareAPI<S, A>) => (api: MiddlewareAPI<S, A>) =>
(next: PlainDispatch<A>) => PlainDispatch<A>; (next: PlainDispatch<A>) =>
(PlainDispatch<A> | (action: A) => Promise<A>);
declare export type Store<S, A, D = ReduxDispatch<S, A>> = { declare export type Store<S, A, D = ReduxDispatch<S, A>> = {
// rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages)

View File

@ -66,7 +66,6 @@ export type CustomBackend = {
url: string; url: string;
} }
export type State = { export type State = {
initialized: boolean; initialized: boolean;
error: ?string; error: ?string;

View File

@ -1,6 +1,4 @@
/* @flow */ /* @flow */
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import type { import type {
@ -14,7 +12,6 @@ import type {
export type State = { export type State = {
location?: string; location?: string;
account: ?Account; account: ?Account;
network: ?Coin; network: ?Coin;
tokens: Array<Token>, tokens: Array<Token>,

View File

@ -12,17 +12,18 @@ import type { Action, RouterLocationState, TrezorDevice } from 'flowtype';
type State = { type State = {
ready: boolean; ready: boolean;
unloading: boolean;
online: boolean; online: boolean;
dropdownOpened: boolean; dropdownOpened: boolean;
initialParams: ?RouterLocationState; initialParams: ?RouterLocationState;
initialPathname: ?string; initialPathname: ?string;
disconnectRequest: ?TrezorDevice; disconnectRequest: ?TrezorDevice;
selectedDevice: ?TrezorDevice; selectedDevice: ?TrezorDevice;
} }
const initialState: State = { const initialState: State = {
ready: false, ready: false,
unloading: false,
online: navigator.onLine, online: navigator.onLine,
dropdownOpened: false, dropdownOpened: false,
initialParams: null, initialParams: null,
@ -33,6 +34,12 @@ const initialState: State = {
export default function wallet(state: State = initialState, action: Action): State { export default function wallet(state: State = initialState, action: Action): State {
switch (action.type) { switch (action.type) {
case WALLET.ON_BEFORE_UNLOAD:
return {
...state,
unloading: true,
};
case WALLET.SET_INITIAL_URL: case WALLET.SET_INITIAL_URL:
return { return {
...state, ...state,

View File

@ -1,156 +1,46 @@
/* @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 RouterActions from 'actions/RouterActions';
import * as WALLET from 'actions/constants/wallet';
import * as NotificationActions from 'actions/NotificationActions';
import type { import type {
Middleware, Middleware,
MiddlewareAPI, MiddlewareAPI,
MiddlewareDispatch, MiddlewareDispatch,
Action, Action,
RouterLocationState,
TrezorDevice,
} from 'flowtype'; } from 'flowtype';
/** /**
* Middleware used for init application and managing router path. * 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 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();
let device: ?TrezorDevice;
if (params.hasOwnProperty('deviceInstance')) {
device = devices.find(d => d.features && d.features.device_id === params.device && d.instance === parseInt(params.deviceInstance, 10));
} else {
device = devices.find(d => d.path === params.device || (d.features && d.features.device_id === params.device));
}
if (!device) return false;
}
if (params.hasOwnProperty('network')) {
const { config } = api.getState().localStorage;
const coin = config.coins.find(c => c.network === params.network);
if (!coin) return false;
if (!params.account) return false;
}
// if (params.account) {
// }
return true;
};
let __unloading: boolean = false;
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
const { location } = api.getState().router;
const { devices } = api.getState();
const { error } = api.getState().connect;
const requestedParams: RouterLocationState = pathToParams(action.payload.pathname);
const currentParams: RouterLocationState = pathToParams(location ? location.pathname : '/');
const postActions: Array<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
if (isModalOpened && action.payload.pathname !== location.pathname && validation(api, currentParams)) {
redirectPath = location.pathname;
console.warn('Modal still opened');
} else if (landingPage) {
// 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,
},
});
}
}
}
}
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
return next(action); return next(action);
}
// compose valid url
const validUrl = api.dispatch(RouterActions.getValidUrl(action));
// override action state (to be stored in RouterReducer)
const override = action;
override.payload.state = api.dispatch(RouterActions.pathToParams(validUrl));
const redirect = action.payload.pathname !== validUrl;
if (redirect) {
// override action pathname
override.payload.pathname = validUrl;
}
// pass action
next(override);
if (redirect) {
// replace invalid url
api.dispatch(replace(validUrl));
}
return override;
}; };
export default RouterService; export default RouterService;

View File

@ -1,12 +1,12 @@
/* @flow */ /* @flow */
import { push } from 'react-router-redux';
import TrezorConnect, { import {
TRANSPORT, DEVICE_EVENT, UI_EVENT, UI, DEVICE, BLOCKCHAIN TRANSPORT, DEVICE, BLOCKCHAIN,
} from 'trezor-connect'; } from 'trezor-connect';
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

@ -1,11 +1,12 @@
/* @flow */ /* @flow */
import { DEVICE } from 'trezor-connect'; import { DEVICE } from 'trezor-connect';
import { LOCATION_CHANGE } from 'react-router-redux'; import { LOCATION_CHANGE } from 'react-router-redux';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as CONNECT from 'actions/constants/TrezorConnect';
import * as WalletActions from 'actions/WalletActions'; import * as WalletActions from 'actions/WalletActions';
import * as RouterActions from 'actions/RouterActions';
import * as NotificationActions from 'actions/NotificationActions';
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';
@ -22,24 +23,74 @@ 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());
// load data from config.json and local storage // set initial url
api.dispatch(LocalStorageActions.loadData()); // 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 // pass action
next(action); next(action);
if (action.type === DEVICE.CONNECT) { switch (action.type) {
api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device)); case WALLET.SET_INITIAL_URL:
api.dispatch(LocalStorageActions.loadData());
break;
case WALLET.SET_SELECTED_DEVICE:
if (action.device) {
// try to authorize device
api.dispatch(TrezorConnectActions.getSelectedDeviceState());
} else {
// try select different device
api.dispatch(RouterActions.selectFirstAvailableDevice());
} }
break;
case DEVICE.CONNECT:
api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device));
break;
default: {
break;
}
}
// update common values ONLY if application is ready
if (!api.getState().wallet.ready) return action;
// double verification needed
// 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 (action.type === LOCATION_CHANGE && prevLocation.pathname !== currentLocation.pathname) {
// watch for coin change
if (prevLocation.state.network !== currentLocation.state.network) {
api.dispatch({
type: CONNECT.COIN_CHANGED,
payload: {
network: currentLocation.state.network,
},
});
}
// watch for account change
if (prevLocation.state.network !== currentLocation.state.network || prevLocation.state.account !== currentLocation.state.account) {
api.dispatch(SelectedAccountActions.dispose());
}
// clear notifications
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
}
// update common values in WallerReducer // update common values in WallerReducer
api.dispatch(WalletActions.updateSelectedValues(prevState, action)); api.dispatch(WalletActions.updateSelectedValues(prevState, action));
@ -47,15 +98,6 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
// 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
if (action.type === WALLET.SET_SELECTED_DEVICE) {
if (action.device) {
api.dispatch(TrezorConnectActions.getSelectedDeviceState());
} else {
api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice());
}
}
return action; return action;
}; };

94
src/support/routes.js Normal file
View File

@ -0,0 +1,94 @@
/* @flow */
export type Route = {
+name: string;
+pattern: string;
fields: Array<string>;
}
export const routes: Array<Route> = [
{
name: 'landing-home',
pattern: '/',
fields: [],
},
{
name: 'landing-bridge',
pattern: '/bridge',
fields: ['bridge'],
},
{
name: 'landing-import',
pattern: '/import',
fields: ['import'],
},
{
name: 'wallet-setting',
pattern: '/settings',
fields: ['settings'],
},
{
name: 'wallet-acquire',
pattern: '/device/:device/acquire',
fields: ['device', 'acquire'],
},
{
name: 'wallet-unreadable',
pattern: '/device/:device/unreadable',
fields: ['device', 'unreadable'],
},
{
name: 'wallet-bootloader',
pattern: '/device/:device/bootloader',
fields: ['device', 'bootloader'],
},
{
name: 'wallet-initialize',
pattern: '/device/:device/initialize',
fields: ['device', 'initialize'],
},
{
name: 'wallet-device-settings',
pattern: '/device/:device/settings',
fields: ['device', 'settings'],
},
{
name: 'wallet-dashboard',
pattern: '/device/:device',
fields: ['device'],
},
{
name: 'wallet-account-summary',
pattern: '/device/:device/network/:network/account/:account',
fields: ['device', 'network', 'account'],
},
{
name: 'wallet-account-send',
pattern: '/device/:device/network/:network/account/:account/send',
fields: ['device', 'network', 'account', 'send'],
},
{
name: 'wallet-account-send-override',
pattern: '/device/:device/network/:network/account/:account/send/override',
fields: ['device', 'network', 'account', 'send'],
},
{
name: 'wallet-account-receive',
pattern: '/device/:device/network/:network/account/:account/receive',
fields: ['device', 'network', 'account', 'receive'],
},
{
name: 'wallet-account-signverify',
pattern: '/device/:device/network/:network/account/:account/signverify',
fields: ['device', 'network', 'account', 'signverify'],
},
];
export const getPattern = (name: string): string => {
const entry = routes.find(r => r.name === name);
if (!entry) {
console.error(`Route for ${name} not found`);
return '/';
}
return entry.pattern;
};

View File

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as RouterActions from 'actions/RouterActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
@ -36,8 +37,8 @@ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps>
acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch),
forgetDevice: bindActionCreators(TrezorConnectActions.forget, dispatch), forgetDevice: bindActionCreators(TrezorConnectActions.forget, dispatch),
duplicateDevice: bindActionCreators(TrezorConnectActions.duplicateDevice, dispatch), duplicateDevice: bindActionCreators(TrezorConnectActions.duplicateDevice, dispatch),
gotoDeviceSettings: bindActionCreators(TrezorConnectActions.gotoDeviceSettings, dispatch), gotoDeviceSettings: bindActionCreators(RouterActions.gotoDeviceSettings, dispatch),
onSelectDevice: bindActionCreators(TrezorConnectActions.onSelectDevice, dispatch), onSelectDevice: bindActionCreators(RouterActions.selectDevice, dispatch),
}); });
export default withRouter( export default withRouter(

View File

@ -1,5 +1,6 @@
/* @flow */ /* @flow */
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as RouterActions from 'actions/RouterActions';
import { toggleDeviceDropdown } from 'actions/WalletActions'; import { toggleDeviceDropdown } from 'actions/WalletActions';
import type { State } from 'flowtype'; import type { State } from 'flowtype';
@ -22,8 +23,8 @@ export type DispatchProps = {
acquireDevice: typeof TrezorConnectActions.acquire, acquireDevice: typeof TrezorConnectActions.acquire,
forgetDevice: typeof TrezorConnectActions.forget, forgetDevice: typeof TrezorConnectActions.forget,
duplicateDevice: typeof TrezorConnectActions.duplicateDevice, duplicateDevice: typeof TrezorConnectActions.duplicateDevice,
gotoDeviceSettings: typeof TrezorConnectActions.gotoDeviceSettings, gotoDeviceSettings: typeof RouterActions.gotoDeviceSettings,
onSelectDevice: typeof TrezorConnectActions.onSelectDevice, onSelectDevice: typeof RouterActions.selectDevice,
} }
export type Props = StateProps & DispatchProps; export type Props = StateProps & DispatchProps;

View File

@ -30,10 +30,11 @@ const SelectedAccount = (props: Props) => {
const { const {
account, account,
discovery, discovery,
network network,
} = accountState; } = accountState;
if (!network) return; // TODO: this shouldn't happen. change accountState reducer? // corner case: accountState didn't finish loading state after LOCATION_CHANGE action
if (!network) return (<Notification type="info" title="Loading account state..." />);
const blockchain = props.blockchain.find(b => b.name === network.network); const blockchain = props.blockchain.find(b => b.name === network.network);
if (blockchain && !blockchain.connected) { if (blockchain && !blockchain.connected) {
@ -43,12 +44,13 @@ const SelectedAccount = (props: Props) => {
title="Backend not connected" title="Backend not connected"
actions={ actions={
[{ [{
label: "Try again", label: 'Try again',
callback: async () => { callback: async () => {
await props.blockchainReconnect(network.network); await props.blockchainReconnect(network.network);
} },
}] }]
} /> }
/>
); );
} }

View File

@ -7,7 +7,6 @@ import { connect } from 'react-redux';
import { reconnect } from 'actions/DiscoveryActions'; import { reconnect } from 'actions/DiscoveryActions';
import SendFormActions from 'actions/SendFormActions'; import SendFormActions from 'actions/SendFormActions';
import * as SessionStorageActions from 'actions/SessionStorageActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
import type { StateProps as BaseStateProps, DispatchProps as BaseDispatchProps } from 'views/Wallet/components/SelectedAccount'; import type { StateProps as BaseStateProps, DispatchProps as BaseDispatchProps } from 'views/Wallet/components/SelectedAccount';
@ -24,7 +23,6 @@ export type StateProps = BaseStateProps & {
export type DispatchProps = BaseDispatchProps & { export type DispatchProps = BaseDispatchProps & {
sendFormActions: typeof SendFormActions, sendFormActions: typeof SendFormActions,
saveSessionStorage: typeof SessionStorageActions.save
} }
export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps; export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps;
@ -43,7 +41,6 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
blockchainReconnect: bindActionCreators(reconnect, dispatch), blockchainReconnect: bindActionCreators(reconnect, dispatch),
sendFormActions: bindActionCreators(SendFormActions, dispatch), sendFormActions: bindActionCreators(SendFormActions, dispatch),
saveSessionStorage: bindActionCreators(SessionStorageActions.save, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(AccountSend); export default connect(mapStateToProps, mapDispatchToProps)(AccountSend);

View File

@ -199,8 +199,6 @@ class AccountSend extends Component<Props, State> {
componentWillReceiveProps(newProps: Props) { componentWillReceiveProps(newProps: Props) {
calculate(this.props, newProps); calculate(this.props, newProps);
validation(newProps); validation(newProps);
this.props.saveSessionStorage();
} }
getAddressInputState(address: string, addressErrors: string, addressWarnings: string) { getAddressInputState(address: string, addressErrors: string, addressWarnings: string) {

View File

@ -6,6 +6,7 @@ import { ConnectedRouter } from 'react-router-redux';
// general // general
import ErrorBoundary from 'support/ErrorBoundary'; import ErrorBoundary from 'support/ErrorBoundary';
import { getPattern } from 'support/routes';
import LandingContainer from 'views/Landing/Container'; import LandingContainer from 'views/Landing/Container';
// wallet views // wallet views
@ -29,25 +30,24 @@ const App = () => (
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<Switch> <Switch>
<Route exact path="/" component={LandingContainer} /> <Route exact path={getPattern('landing-home')} component={LandingContainer} />
<Route exact path="/bridge" component={LandingContainer} /> <Route exact path={getPattern('landing-bridge')} component={LandingContainer} />
<Route exact path="/import" component={LandingContainer} /> <Route exact path={getPattern('landing-import')} component={LandingContainer} />
<Route> <Route>
<ErrorBoundary> <ErrorBoundary>
<WalletContainer> <WalletContainer>
<Route exact path="/settings" component={WalletSettings} /> <Route exact path={getPattern('wallet-setting')} component={WalletSettings} />
<Route exact path="/device/:device/" component={WalletDashboard} /> <Route exact path={getPattern('wallet-dashboard')} component={WalletDashboard} />
<Route exact path="/device/:device/network/:network" component={WalletDashboard} /> <Route exact path={getPattern('wallet-acquire')} component={WalletAcquire} />
<Route exact path="/device/:device/acquire" component={WalletAcquire} /> <Route exact path={getPattern('wallet-unreadable')} component={WalletUnreadableDevice} />
<Route exact path="/device/:device/unreadable" component={WalletUnreadableDevice} /> <Route exact path={getPattern('wallet-bootloader')} component={WalletBootloader} />
<Route exact path="/device/:device/bootloader" component={WalletBootloader} /> <Route exact path={getPattern('wallet-initialize')} component={WalletInitialize} />
<Route exact path="/device/:device/initialize" component={WalletInitialize} /> <Route exact path={getPattern('wallet-device-settings')} component={WalletDeviceSettings} />
<Route exact path="/device/:device/settings" component={WalletDeviceSettings} /> <Route exact path={getPattern('wallet-account-summary')} component={AccountSummary} />
<Route exact path="/device/:device/network/:network/account/:account" component={AccountSummary} /> <Route path={getPattern('wallet-account-send')} component={AccountSend} />
<Route path="/device/:device/network/:network/account/:account/send" component={AccountSend} /> <Route path={getPattern('wallet-account-send-override')} component={AccountSend} />
<Route path="/device/:device/network/:network/account/:account/send/override" component={AccountSend} /> <Route path={getPattern('wallet-account-receive')} component={AccountReceive} />
<Route path="/device/:device/network/:network/account/:account/receive" component={AccountReceive} /> <Route path={getPattern('wallet-account-signverify')} component={AccountSignVerify} />
<Route path="/device/:device/network/:network/account/:account/signverify" component={AccountSignVerify} />
</WalletContainer> </WalletContainer>
</ErrorBoundary> </ErrorBoundary>
</Route> </Route>