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

Merge branch 'master' into add-sentry-to-devel

This commit is contained in:
Vladimir Volek 2018-09-21 14:34:01 +02:00 committed by GitHub
commit 976222476b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1149 additions and 578 deletions

View File

@ -4,11 +4,15 @@
"plugin:flowtype/recommended",
"plugin:jest/recommended"
],
"globals": {
"COMMITHASH": true
},
"env": {
"browser": true,
"jest": true
},
"rules": {
"no-use-before-define": 0,
"no-plusplus": 0,
"class-methods-use-this": 0,
"react/require-default-props": 0,

View File

@ -1,11 +1,19 @@
module.exports = {
rootDir: './src',
automock: false,
coverageDirectory: 'coverage/',
collectCoverage: true,
testURL: 'http://localhost',
modulePathIgnorePatterns: [
'node_modules',
'utils/windowUtils.js',
'utils/promiseUtils.js',
'utils/networkUtils.js',
],
collectCoverageFrom: [
'utils/**.js',
],
setupFiles: [
'./support/setupJest.js',
],
};

View File

@ -34,9 +34,12 @@
"ethereumjs-tx": "^1.3.3",
"ethereumjs-units": "^0.2.0",
"ethereumjs-util": "^5.1.4",
"git-revision-webpack-plugin": "^3.0.3",
"flow-webpack-plugin": "^1.2.0",
"hdkey": "^0.8.0",
"html-webpack-plugin": "^3.2.0",
"http-server": "^0.11.1",
"jest-fetch-mock": "^1.6.5",
"npm-run-all": "^4.1.3",
"prop-types": "^15.6.2",
"raf": "^3.4.0",
@ -51,8 +54,7 @@
"react-router-redux": "next",
"react-scale-text": "^1.2.2",
"react-select": "2.0.0",
"react-sticky-el": "^1.0.20",
"react-transition-group": "^2.2.1",
"react-transition-group": "^2.4.0",
"redbox-react": "^1.6.0",
"redux": "4.0.0",
"redux-logger": "^3.0.6",

View File

@ -58,11 +58,11 @@
"fiatValueTickers": [
{
"network": "ethereum",
"network": "eth",
"url": "https://api.coinmarketcap.com/v1/ticker/ethereum/"
},
{
"network": "ethereum-classic",
"network": "etc",
"url": "https://api.coinmarketcap.com/v1/ticker/ethereum-classic/"
}
],

View File

@ -111,7 +111,7 @@ export const onBlockMined = (coinInfo: any): PromiseAction<void> => async (dispa
dispatch( Web3Actions.updateAccount(accounts[i], a, network) )
} else {
// there are no new txs, just update block
dispatch( AccountsActions.update( { ...accounts[i], ...a }) );
dispatch( AccountsActions.update( { ...accounts[i], block: a.block }) );
// HACK: since blockbook can't work with smart contracts for now
// try to update tokens balances added to this account using Web3

View File

@ -21,7 +21,7 @@ export type StorageAction = {
type: typeof STORAGE.READY,
config: Config,
tokens: TokensCollection,
ERC20Abi: Array<Object>
ERC20Abi: Array<TokensCollection>
} | {
type: typeof STORAGE.SAVE,
network: string,
@ -148,7 +148,6 @@ export function loadTokensFromJSON(): AsyncAction {
});
}
dispatch({
type: STORAGE.READY,
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 PENDING from 'actions/constants/pendingTx';
import * as SendFormActions from 'actions/SendFormActions';
import * as SessionStorageActions from 'actions/SessionStorageActions';
import * as stateUtils from 'reducers/utils';
import type {
@ -16,8 +19,6 @@ import type {
Dispatch,
State,
} from 'flowtype';
import * as SendFormActions from './SendFormActions';
export type SelectedAccountAction = {
type: typeof ACCOUNT.DISPOSE,
@ -42,17 +43,16 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
// SessionStorageActions.clear(location.pathname);
}
if (prevState.sendForm !== state.sendForm) {
dispatch(SessionStorageActions.save());
}
// handle devices state change (from trezor-connect events or location change)
if (locationChange
|| prevState.accounts !== state.accounts
|| prevState.discovery !== state.discovery
|| prevState.tokens !== state.tokens
|| prevState.pending !== state.pending) {
if (locationChange) {
// dispose current account view
dispatch(dispose());
}
const account = stateUtils.getSelectedAccount(state);
const network = stateUtils.getSelectedNetwork(state);
const discovery = stateUtils.getDiscoveryProcess(state);

View File

@ -13,6 +13,7 @@ import { initialState } from 'reducers/SendFormReducer';
import { findToken } from 'reducers/TokensReducer';
import { findDevice, getPendingAmount, getPendingNonce } from 'reducers/utils';
import * as stateUtils from 'reducers/utils';
import { validateAddress } from 'utils/ethUtils';
import type {
Dispatch,
@ -246,7 +247,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
// const gasPrice: BigNumber = new BigNumber(EthereumjsUnits.convert(web3.gasPrice, 'wei', 'gwei')) || new BigNumber(network.defaultGasPrice);
const gasPrice: BigNumber = await dispatch( BlockchainActions.getGasPrice(network.network, network.defaultGasPrice) );
const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.network, network.defaultGasPrice));
// const gasPrice: BigNumber = new BigNumber(network.defaultGasPrice);
const gasLimit: string = network.defaultGasLimit.toString();
const feeLevels: Array<FeeLevel> = getFeeLevels(network.symbol, gasPrice, gasLimit);
@ -345,32 +346,19 @@ export const validation = (props: Props): void => {
if (state.untouched) return;
// valid address
if (state.touched.address) {
/* if (state.address.length < 1) {
errors.address = 'Address is not set';
} else if (!EthereumjsUtil.isValidAddress(state.address)) {
errors.address = 'Address is not valid';
} else {
// address warning or info are set in addressValidation ThunkAction
// do not override this
if (state.warnings.address) {
warnings.address = state.warnings.address;
} else if (state.infos.address) {
infos.address = state.infos.address;
}
} */
const addressError = validateAddress(state.address);
if (addressError) {
errors.address = addressError;
}
/* eslint (no-lonely-if) */
if (state.address.length < 1) {
errors.address = 'Address is not set';
} else if (!EthereumjsUtil.isValidAddress(state.address)) {
errors.address = 'Address is not valid';
} else if (state.warnings.address) {
// address warning or info are set in addressValidation ThunkAction
// do not override this
// address warning or info may be set in addressValidation ThunkAction
// do not override them
if (state.warnings.address) {
warnings.address = state.warnings.address;
if (state.infos.address) {
infos.address = state.infos.address;
}
}
if (state.infos.address) {
infos.address = state.infos.address;
}
}
@ -733,7 +721,7 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState:
return;
}
const gasLimit: number = await dispatch( BlockchainActions.estimateGasLimit(network.network, state.data, state.amount, state.gasPrice) );
const gasLimit: number = await dispatch(BlockchainActions.estimateGasLimit(network.network, state.data, state.amount, state.gasPrice));
if (getState().sendForm.data === requestedData) {
dispatch(onGasLimitChange(gasLimit.toString()));
@ -783,7 +771,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
const pendingNonce: number = stateUtils.getPendingNonce(pending);
const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce;
const txData = await dispatch( prepareEthereumTx({
const txData = await dispatch(prepareEthereumTx({
network: network.network,
token: isToken ? findToken(getState().tokens, account.address, currentState.currency, account.deviceState) : null,
from: account.address,
@ -792,8 +780,8 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
data: currentState.data,
gasLimit: currentState.gasLimit,
gasPrice: currentState.gasPrice,
nonce
}) );
nonce,
}));
const selected: ?TrezorDevice = getState().wallet.selectedDevice;
if (!selected) return;
@ -828,14 +816,14 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
txData.v = signedTransaction.payload.v;
try {
const serializedTx: string = await dispatch( serializeEthereumTx(txData) );
const serializedTx: string = await dispatch(serializeEthereumTx(txData));
const push = await TrezorConnect.pushTransaction({
tx: serializedTx,
coin: network.network
coin: network.network,
});
if (!push.success) {
throw new Error( push.payload.error );
throw new Error(push.payload.error);
}
const txid = push.payload.txid;

View File

@ -1,13 +1,11 @@
/* @flow */
import TrezorConnect, {
UI, DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT
DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT,
} from 'trezor-connect';
import * as CONNECT from 'actions/constants/TrezorConnect';
import * as NOTIFICATION from 'actions/constants/notification';
import * as WALLET from 'actions/constants/wallet';
import { getDuplicateInstanceNumber } from 'reducers/utils';
import { push } from 'react-router-redux';
import * as RouterActions from 'actions/RouterActions';
import type {
DeviceMessage,
@ -28,7 +26,6 @@ import type {
AsyncAction,
Device,
TrezorDevice,
RouterLocationState,
} from 'flowtype';
import * as DiscoveryActions from './DiscoveryActions';
@ -79,19 +76,11 @@ export type TrezorConnectAction = {
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> => {
// set listeners
TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => {
// post event to reducers
const type: DeviceMessageType = event.type; // assert flow type
const type: DeviceMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({
type,
device: event.payload,
@ -100,7 +89,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(UI_EVENT, (event: UiMessage): void => {
// post event to reducers
const type: UiMessageType = event.type; // assert flow type
const type: UiMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({
type,
payload: event.payload,
@ -109,7 +98,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(TRANSPORT_EVENT, (event: TransportMessage): void => {
// post event to reducers
const type: TransportMessageType = event.type; // assert flow type
const type: TransportMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({
type,
payload: event.payload,
@ -118,21 +107,22 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainMessage): void => {
// post event to reducers
const type: BlockchainMessageType = event.type; // assert flow type
const type: BlockchainMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({
type,
payload: event.payload,
});
});
/* global LOCAL */
// $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://connect.trezor.io/5/';
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/'; // eslint-disable-line no-underscore-dangle
try {
await TrezorConnect.init({
transportReconnect: true,
debug: true,
debug: false,
popup: false,
webusb: true,
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
// 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) => {
dispatch(initConnectedDevice(device));
dispatch(RouterActions.selectDevice(device));
};
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_UNACQUIRED, handleDeviceConnect);
const { devices } = getState();
const { initialPathname, initialParams } = getState().wallet;
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) );
}
}
}
// try to redirect to initial url
if (!dispatch(RouterActions.setInitialUrl())) {
// if initial redirection fails try to switch to first available device
dispatch(RouterActions.selectFirstAvailableDevice());
}
};
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> => {
const selected = getState().wallet.selectedDevice;
if (selected
@ -345,7 +234,7 @@ export const coinChanged = (network: ?string): ThunkAction => (dispatch: Dispatc
};
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)
export const forget = (device: TrezorDevice): Action => ({
type: CONNECT.FORGET_REQUEST,

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import colors from 'config/colors';
import Icon from 'components/Icon';
import icons from 'config/icons';
@ -34,8 +34,10 @@ const IconWrapper = styled.div`
&:hover,
&:focus {
border: 1px solid ${colors.TEXT_PRIMARY};
background: ${props => (props.checked ? colors.TEXT_PRIMARY : colors.WHITE)};
${props => !props.checked && css`
border: 1px solid ${colors.GREEN_PRIMARY};
`}
background: ${props => (props.checked ? colors.GREEN_PRIMARY : colors.WHITE)};
}
`;
@ -74,7 +76,12 @@ class Checkbox extends PureComponent {
<IconWrapper checked={checked}>
{checked && (
<Tick>
<Icon size={26} color={checked ? colors.WHITE : colors.GREEN_PRIMARY} icon={icons.SUCCESS} />
<Icon
hoverColor={colors.WHITE}
size={26}
color={checked ? colors.WHITE : colors.GREEN_PRIMARY}
icon={icons.SUCCESS}
/>
</Tick>
)
}

View File

@ -29,7 +29,7 @@ const Copy = styled.div`
const Footer = ({ toggle }) => (
<Wrapper>
<Copy>&copy; {getYear(new Date())}</Copy>
<Copy title={COMMITHASH}>&copy; {getYear(new Date())}</Copy>
<StyledLink href="http://satoshilabs.com" target="_blank" rel="noreferrer noopener" isGreen>SatoshiLabs</StyledLink>
<StyledLink href="/assets/tos.pdf" target="_blank" rel="noreferrer noopener" isGreen>Terms</StyledLink>
<StyledLink onClick={toggle} isGreen>Show Log</StyledLink>

View File

@ -27,7 +27,7 @@ const SvgWrapper = styled.svg`
:hover {
path {
fill: ${props => props.hoverColor || colors.TEXT_SECONDARY}
fill: ${props => props.hoverColor}
}
}
`;

View File

@ -26,10 +26,7 @@ const P = ({ children, className, isSmaller = false }) => (
P.propTypes = {
className: PropTypes.string,
isSmaller: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.array,
PropTypes.string,
]),
children: PropTypes.node,
};
export default P;

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import ReactSelect from 'react-select';
import ReactAsyncSelect from 'react-select/lib/Async';
import colors from 'config/colors';
import { FONT_FAMILY } from 'config/variables';
const styles = isSearchable => ({
singleValue: base => ({
@ -13,7 +12,6 @@ const styles = isSearchable => ({
}),
control: (base, { isDisabled }) => ({
...base,
fontFamily: FONT_FAMILY.MONOSPACE,
minHeight: 'initial',
height: '100%',
borderRadius: '2px',
@ -45,17 +43,16 @@ const styles = isSearchable => ({
menuList: base => ({
...base,
padding: 0,
fontFamily: FONT_FAMILY.MONOSPACE,
boxShadow: 'none',
background: colors.WHITE,
borderLeft: `1px solid ${colors.DIVIDER}`,
borderRight: `1px solid ${colors.DIVIDER}`,
borderBottom: `1px solid ${colors.DIVIDER}`,
}),
option: (base, { isSelected }) => ({
option: (base, { isFocused }) => ({
...base,
color: colors.TEXT_SECONDARY,
background: isSelected ? colors.LANDING : colors.WHITE,
background: isFocused ? colors.LANDING : colors.WHITE,
borderRadius: 0,
'&:hover': {
cursor: 'pointer',

View File

@ -3,19 +3,13 @@ import RcTooltip from 'rc-tooltip';
import colors from 'config/colors';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { FONT_SIZE } from 'config/variables';
const TooltipContent = styled.div`
width: ${props => (props.isAside ? '260px' : '320px')};
font-size: ${FONT_SIZE.SMALLEST};
`;
const Wrapper = styled.div`
.rc-tooltip {
max-width: ${props => `${props.maxWidth}px` || 'auto'};
position: absolute;
z-index: 1070;
display: block;
background: red;
visibility: visible;
border: 1px solid ${colors.DIVIDER};
border-radius: 3px;
@ -178,9 +172,11 @@ class Tooltip extends Component {
placement,
content,
children,
maxWidth,
} = this.props;
return (
<Wrapper
maxWidth={maxWidth}
className={className}
innerRef={(node) => { this.tooltipContainerRef = node; }}
>
@ -188,7 +184,7 @@ class Tooltip extends Component {
getTooltipContainer={() => this.tooltipContainerRef}
arrowContent={<div className="rc-tooltip-arrow-inner" />}
placement={placement}
overlay={<TooltipContent>{content}</TooltipContent>}
overlay={content}
>
{children}
</RcTooltip>
@ -204,6 +200,7 @@ Tooltip.propTypes = {
PropTypes.element,
PropTypes.string,
]),
maxWidth: PropTypes.number,
content: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,

View File

@ -8,7 +8,6 @@ import {
FONT_SIZE,
FONT_WEIGHT,
TRANSITION,
FONT_FAMILY,
} from 'config/variables';
const Wrapper = styled.div`
@ -35,10 +34,9 @@ const TopLabel = styled.span`
const StyledInput = styled.input`
width: 100%;
padding: 6px 12px;
padding: 6px ${props => (props.hasIcon ? '40px' : '12px')} 6px 12px;
line-height: 1.42857143;
font-family: ${FONT_FAMILY.MONOSPACE};
font-size: ${FONT_SIZE.SMALL};
font-weight: ${FONT_WEIGHT.BASE};
color: ${colors.TEXT_PRIMARY};
@ -114,6 +112,8 @@ class Input extends Component {
/>
)}
<StyledInput
hasIcon={!!this.props.state}
innerRef={this.props.innerRef}
hasAddon={!!this.props.sideAddons}
type={this.props.type}
placeholder={this.props.placeholder}
@ -143,13 +143,14 @@ class Input extends Component {
Input.propTypes = {
className: PropTypes.string,
innerRef: PropTypes.func,
placeholder: PropTypes.string,
type: PropTypes.string,
autoComplete: PropTypes.string,
autoCorrect: PropTypes.string,
autoCapitalize: PropTypes.string,
spellCheck: PropTypes.string,
value: PropTypes.string.isRequired,
value: PropTypes.string,
onChange: PropTypes.func,
state: PropTypes.string,
bottomText: PropTypes.string,

View File

@ -5,6 +5,7 @@ import P from 'components/Paragraph';
import Icon from 'components/Icon';
import icons from 'config/icons';
import { H3 } from 'components/Heading';
import { LINE_HEIGHT } from 'config/variables';
const Wrapper = styled.div`
width: 390px;
@ -21,6 +22,12 @@ const Content = styled.div`
padding: 24px 48px;
`;
const StyledP = styled(P)`
word-wrap: break-word;
padding: 5px 0;
line-height: ${LINE_HEIGHT.SMALL}
`;
const Label = styled.div`
padding-top: 5px;
font-size: 10px;
@ -49,7 +56,7 @@ const ConfirmSignTx = (props) => {
<Label>Send</Label>
<P>{`${amount} ${currency}` }</P>
<Label>To</Label>
<P>{ address }</P>
<StyledP>{ address }</StyledP>
<Label>Fee</Label>
<P>{ selectedFeeLevel.label }</P>
</Content>

View File

@ -2,6 +2,7 @@
import React, { Component } from 'react';
import raf from 'raf';
import colors from 'config/colors';
import { H2 } from 'components/Heading';
import P from 'components/Paragraph';
import { FONT_SIZE } from 'config/variables';
import Link from 'components/Link';
@ -23,7 +24,10 @@ const Label = styled.div`
padding-bottom: 5px;
`;
const PassphraseError = styled.div``;
const PassphraseError = styled.div`
margin-top: 8px;
color: ${colors.ERROR_PRIMARY};
`;
const Row = styled.div`
position: relative;
@ -137,22 +141,25 @@ export default class PinModal extends Component<Props, State> {
} = this.state;
// } = this.props.modal;
let passphraseInputValue: string = passphrase;
let passphraseRevisionInputValue: string = passphraseRevision;
if (!visible && !passphraseFocused) {
passphraseInputValue = passphrase.replace(/./g, '•');
}
if (!visible && !passphraseRevisionFocused) {
passphraseRevisionInputValue = passphraseRevision.replace(/./g, '•');
}
const passphraseInputValue: string = passphrase;
const passphraseRevisionInputValue: string = passphraseRevision;
// if (!visible && !passphraseFocused) {
// passphraseInputValue = passphrase.replace(/./g, '•');
// }
// if (!visible && !passphraseRevisionFocused) {
// passphraseRevisionInputValue = passphraseRevision.replace(/./g, '•');
// }
if (this.passphraseInput) {
this.passphraseInput.value = passphraseInputValue;
this.passphraseInput.setAttribute('type', visible || (!visible && !passphraseFocused) ? 'text' : 'password');
// this.passphraseInput.value = passphraseInputValue;
// this.passphraseInput.setAttribute('type', visible || (!visible && !passphraseFocused) ? 'text' : 'password');
this.passphraseInput.setAttribute('type', visible ? 'text' : 'password');
}
if (this.passphraseRevisionInput) {
this.passphraseRevisionInput.value = passphraseRevisionInputValue;
this.passphraseRevisionInput.setAttribute('type', visible || (!visible && !passphraseRevisionFocused) ? 'text' : 'password');
// this.passphraseRevisionInput.value = passphraseRevisionInputValue;
// this.passphraseRevisionInput.setAttribute('type', visible || (!visible && !passphraseRevisionFocused) ? 'text' : 'password');
this.passphraseRevisionInput.setAttribute('type', visible ? 'text' : 'password');
}
}
@ -174,6 +181,14 @@ export default class PinModal extends Component<Props, State> {
match: previousState.singleInput || previousState.passphraseRevision === value,
passphrase: value,
}));
if (this.state.visible && this.passphraseRevisionInput) {
this.setState({
match: true,
passphraseRevision: value,
});
this.passphraseRevisionInput.value = value;
}
} else {
this.setState(previousState => ({
match: previousState.passphrase === value,
@ -211,12 +226,29 @@ export default class PinModal extends Component<Props, State> {
this.setState({
visible: true,
});
if (this.passphraseRevisionInput) {
this.passphraseRevisionInput.disabled = true;
this.passphraseRevisionInput.value = this.state.passphrase;
this.setState(previousState => ({
passphraseRevision: previousState.passphrase,
match: true,
}));
}
}
onPassphraseHide = (): void => {
this.setState({
visible: false,
});
if (this.passphraseRevisionInput) {
const emptyPassphraseRevisionValue = '';
this.passphraseRevisionInput.value = emptyPassphraseRevisionValue;
this.setState(previousState => ({
passphraseRevision: emptyPassphraseRevisionValue,
match: emptyPassphraseRevisionValue === previousState.passphrase,
}));
this.passphraseRevisionInput.disabled = false;
}
}
submit = (empty: boolean = false): void => {
@ -232,7 +264,7 @@ export default class PinModal extends Component<Props, State> {
//this.passphraseRevisionInput.style.display = 'none';
//this.passphraseRevisionInput.setAttribute('readonly', 'readonly');
const p = passphrase;
// const p = passphrase;
this.setState({
passphrase: '',
@ -271,11 +303,10 @@ export default class PinModal extends Component<Props, State> {
//let passphraseRevisionInputType: string = visible || passphraseRevisionFocused ? "text" : "password";
const showPassphraseCheckboxFn: Function = visible ? this.onPassphraseHide : this.onPassphraseShow;
console.log('passphraseInputType', passphraseInputType);
return (
<Wrapper>
{/* ?<H2>Enter { deviceLabel } passphrase</H2> */}
{/* <P isSmaller>Note that passphrase is case-sensitive.</P> */}
<H2>Enter { deviceLabel } passphrase</H2>
<P isSmaller>Note that passphrase is case-sensitive.</P>
<Row>
<Label>Passphrase</Label>
<Input
@ -289,7 +320,7 @@ export default class PinModal extends Component<Props, State> {
data-lpignore="true"
onFocus={() => this.onPassphraseFocus('passphrase')}
onBlur={() => this.onPassphraseBlur('passphrase')}
tabIndex="1"
tabIndex="0"
/>
</Row>
{!singleInput && (
@ -306,7 +337,6 @@ export default class PinModal extends Component<Props, State> {
data-lpignore="true"
onFocus={() => this.onPassphraseFocus('revision')}
onBlur={() => this.onPassphraseBlur('revision')}
tabIndex="2"
/>
{!match && passphraseRevisionTouched && <PassphraseError>Passphrases do not match</PassphraseError> }
</Row>
@ -315,7 +345,7 @@ export default class PinModal extends Component<Props, State> {
<Checkbox onClick={showPassphraseCheckboxFn} checked={visible}>Show passphrase</Checkbox>
</Row>
<Row>
<Button type="button" tabIndex="4" disabled={!match} onClick={event => this.submit()}>Enter</Button>
<Button type="button" disabled={!match} onClick={() => this.submit()}>Enter</Button>
</Row>
<Footer>
<P isSmaller>If you want to access your default account</P>

View File

@ -41,5 +41,6 @@ export const TRANSITION = {
};
export const LINE_HEIGHT = {
SMALL: '1.4',
BASE: '1.8',
};

View File

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

View File

@ -2,7 +2,6 @@
declare module 'redux' {
/*
S = State
A = Action
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 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 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 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 PayloadDispatch<S, A> = <R>(action: PayloadAction<S, A, R>) => R;
declare export type PlainDispatch<A: {type: $Subtype<string>}> = DispatchAPI<A>;
/* 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> = {
// 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: Dispatch<S, A>;
// dispatch: D;
getState(): S;
};
declare export type Middleware<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>> = {
// 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;
}
export type State = {
initialized: boolean;
error: ?string;

View File

@ -57,6 +57,7 @@ export default function modal(state: State = initialState, action: Action): Stat
if (state.opened && action.device.path === state.device.path && action.device.status === 'occupied') {
return initialState;
}
return state;
case DEVICE.DISCONNECT:

View File

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

View File

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

View File

@ -1,156 +1,46 @@
/* @flow */
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 { LOCATION_CHANGE, replace } from 'react-router-redux';
import * as RouterActions from 'actions/RouterActions';
import type {
Middleware,
MiddlewareAPI,
MiddlewareDispatch,
Action,
RouterLocationState,
TrezorDevice,
} 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 => {
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;
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;
// make sure that middleware should process this action
if (action.type !== LOCATION_CHANGE || api.getState().wallet.unloading) {
// pass action
return next(action);
}
// Pass all actions through by default
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;

View File

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

View File

@ -1,11 +1,12 @@
/* @flow */
import { DEVICE } from 'trezor-connect';
import { LOCATION_CHANGE } from 'react-router-redux';
import * as WALLET from 'actions/constants/wallet';
import * as CONNECT from 'actions/constants/TrezorConnect';
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 TrezorConnectActions from 'actions/TrezorConnectActions';
import * as SelectedAccountActions from 'actions/SelectedAccountActions';
@ -22,40 +23,81 @@ import type {
*/
const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
const prevState = api.getState();
const locationChange: boolean = action.type === LOCATION_CHANGE;
// Application live cycle starts here
if (locationChange) {
const { location } = api.getState().router;
if (!location) {
api.dispatch(WalletActions.init());
// load data from config.json and local storage
api.dispatch(LocalStorageActions.loadData());
}
// Application live cycle starts HERE!
// when first LOCATION_CHANGE is called router does not have "location" set yet
if (action.type === LOCATION_CHANGE && !prevState.router.location) {
// initialize wallet
api.dispatch(WalletActions.init());
// set initial url
// TODO: validate if initial url is potentially correct
api.dispatch({
type: WALLET.SET_INITIAL_URL,
pathname: action.payload.pathname,
state: {},
});
// pass action and break process
return next(action);
}
// pass action
next(action);
if (action.type === DEVICE.CONNECT) {
api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device));
switch (action.type) {
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
api.dispatch(WalletActions.updateSelectedValues(prevState, action));
// update common values in SelectedAccountReducer
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;
};

View File

@ -63,6 +63,44 @@ const baseStyles = () => injectGlobal`
url('./fonts/roboto/roboto-mono-v4-greek_cyrillic-ext_greek-ext_latin_cyrillic_vietnamese_latin-ext-regular.svg#RobotoMono') format('svg'); /* Legacy iOS */
}
.slide-left-enter {
transform: translate(100%);
pointer-events: none;
}
.slide-left-enter.slide-left-enter-active {
transform: translate(0%);
transition: transform 300ms ease-in-out;
}
.slide-left-exit {
transform: translate(-100%);
}
.slide-left-exit.slide-left-exit-active {
transform: translate(0%);
transition: transform 300ms ease-in-out;
}
.slide-right-enter {
transform: translate(-100%);
pointer-events: none;
}
.slide-right-enter.slide-right-enter-active {
transform: translate(0%);
transition: transform 300ms ease-in-out;
}
.slide-right-exit {
transform: translate(-100%);
}
.slide-right-exit.slide-right-exit-active {
transform: translate(-200%);
transition: transform 300ms ease-in-out;
}
`;
export default baseStyles;

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

1
src/support/setupJest.js Normal file
View File

@ -0,0 +1 @@
global.fetch = require('jest-fetch-mock');

View File

@ -55,3 +55,13 @@ exports[`device utils get version 4`] = `"1"`;
exports[`device utils get version 5`] = `"1"`;
exports[`device utils get version 6`] = `"T"`;
exports[`device utils isDisabled 1`] = `false`;
exports[`device utils isDisabled 2`] = `true`;
exports[`device utils isWebUSB 1`] = `true`;
exports[`device utils isWebUSB 2`] = `false`;
exports[`device utils isWebUSB 3`] = `true`;

View File

@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`eth utils calcGasPrice 1`] = `"89090990901"`;
exports[`eth utils decimalToHex 1`] = `"0"`;
exports[`eth utils decimalToHex 2`] = `"1"`;
@ -8,4 +10,36 @@ exports[`eth utils decimalToHex 3`] = `"2"`;
exports[`eth utils decimalToHex 4`] = `"64"`;
exports[`eth utils decimalToHex 5`] = `"3e7"`;
exports[`eth utils decimalToHex 5`] = `"2540be3ff"`;
exports[`eth utils hexToDecimal 1`] = `"9999999999"`;
exports[`eth utils hexToDecimal 2`] = `"100"`;
exports[`eth utils hexToDecimal 3`] = `"2"`;
exports[`eth utils hexToDecimal 4`] = `"1"`;
exports[`eth utils hexToDecimal 5`] = `"0"`;
exports[`eth utils hexToDecimal 6`] = `"null"`;
exports[`eth utils padLeftEven 1`] = `"02540be3ff"`;
exports[`eth utils sanitizeHex 1`] = `"0x02540be3ff"`;
exports[`eth utils sanitizeHex 2`] = `"0x01"`;
exports[`eth utils sanitizeHex 3`] = `"0x02"`;
exports[`eth utils sanitizeHex 4`] = `"0x0100"`;
exports[`eth utils sanitizeHex 5`] = `null`;
exports[`eth utils sanitizeHex 6`] = `""`;
exports[`eth utils strip 1`] = `""`;
exports[`eth utils strip 2`] = `"02540be3ff"`;
exports[`eth utils strip 3`] = `"02540be3ff"`;

View File

@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`format utils btckb2satoshib 1`] = `0`;
exports[`format utils btckb2satoshib 2`] = `100000`;
exports[`format utils btckb2satoshib 3`] = `200000`;
exports[`format utils btckb2satoshib 4`] = `10000000`;
exports[`format utils btckb2satoshib 5`] = `99900000`;
exports[`format utils formatAmount 1`] = `"0 btc"`;
exports[`format utils formatAmount 2`] = `"10 mBTC"`;
exports[`format utils formatAmount 3`] = `"0.000005 mBTC"`;
exports[`format utils formatAmount 4`] = `"1e-8 eth"`;
exports[`format utils formatAmount 5`] = `"0.00099999 tau"`;
exports[`format utils formatTime 1`] = `"No time estimate"`;
exports[`format utils formatTime 2`] = `"1 minutes"`;
exports[`format utils formatTime 3`] = `"2 minutes"`;
exports[`format utils formatTime 4`] = `"1 hour 40 minutes"`;
exports[`format utils formatTime 5`] = `"16 hours 39 minutes"`;
exports[`format utils formatTime 6`] = `"45 minutes"`;
exports[`format utils hexToString 1`] = `"test"`;
exports[`format utils hexToString 2`] = `"0001"`;
exports[`format utils hexToString 3`] = `"test99999"`;
exports[`format utils stringToHex 1`] = `"0074006500730074"`;
exports[`format utils stringToHex 2`] = `"0030003000300031"`;
exports[`format utils stringToHex 3`] = `"007400650073007400390039003900390039"`;

View File

@ -38,6 +38,29 @@ describe('device utils', () => {
});
});
it('isWebUSB', () => {
const data = [
{ transport: { version: ['webusb'] } },
{ transport: { version: ['aaaaaa'] } },
{ transport: { version: ['webusb', 'test'] } },
];
data.forEach((item) => {
expect(dUtils.isWebUSB(item.transport)).toMatchSnapshot();
});
});
it('isDisabled', () => {
const data = [
{ selectedDevice: { features: null }, devices: [1, 2, 3], transport: { version: ['webusb', 'test'] } },
{ selectedDevice: { features: null }, devices: [], transport: { version: ['test'] } },
];
data.forEach((item) => {
expect(dUtils.isDisabled(item.selectedDevice, item.devices, item.transport)).toMatchSnapshot();
});
});
it('get version', () => {
const deviceMock = [
{ },

View File

@ -1,11 +1,53 @@
import BigNumber from 'bignumber.js';
import * as ethUtils from '../ethUtils';
describe('eth utils', () => {
it('decimalToHex', () => {
const input = [0, 1, 2, 100, 999];
const input = [0, 1, 2, 100, 9999999999];
input.forEach((entry) => {
expect(ethUtils.decimalToHex(entry)).toMatchSnapshot();
});
});
it('hexToDecimal', () => {
const input = ['2540be3ff', '64', '2', '1', '0', ''];
input.forEach((entry) => {
expect(ethUtils.hexToDecimal(entry)).toMatchSnapshot();
});
});
it('padLeftEven', () => {
const input = ['2540be3ff'];
input.forEach((entry) => {
expect(ethUtils.padLeftEven(entry)).toMatchSnapshot();
});
});
it('sanitizeHex', () => {
const input = ['0x2540be3ff', '1', '2', '100', 999, ''];
input.forEach((entry) => {
expect(ethUtils.sanitizeHex(entry)).toMatchSnapshot();
});
});
it('strip', () => {
const input = ['0x', '0x2540be3ff', '2540be3ff'];
input.forEach((entry) => {
expect(ethUtils.strip(entry)).toMatchSnapshot();
});
});
it('calcGasPrice', () => {
const input = [{ price: new BigNumber(9898998989), limit: '9' }];
input.forEach((entry) => {
expect(ethUtils.calcGasPrice(entry.price, entry.limit)).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,49 @@
import * as formatUtils from '../formatUtils';
describe('format utils', () => {
it('formatAmount', () => {
const input = [
{ amount: 0, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } },
{ amount: 1000000, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } },
{ amount: 0.5, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } },
{ amount: 1, coinInfo: { isBitcoin: false, shortcut: 'eth' } },
{ amount: 99999, coinInfo: { isBitcoin: false, shortcut: 'tau' } },
];
input.forEach((entry) => {
expect(formatUtils.formatAmount(entry.amount, entry.coinInfo, entry.coinInfo.currencyUnits)).toMatchSnapshot();
});
});
it('formatTime', () => {
const input = [0, 1, 2, 100, 999, 45];
input.forEach((entry) => {
expect(formatUtils.formatTime(entry)).toMatchSnapshot();
});
});
it('btckb2satoshib', () => {
const input = [0, 1, 2, 100, 999];
input.forEach((entry) => {
expect(formatUtils.btckb2satoshib(entry)).toMatchSnapshot();
});
});
it('stringToHex', () => {
const input = ['test', '0001', 'test99999'];
input.forEach((entry) => {
expect(formatUtils.stringToHex(entry)).toMatchSnapshot();
});
});
it('hexToString', () => {
const input = ['0074006500730074', '0030003000300031', '007400650073007400390039003900390039'];
input.forEach((entry) => {
expect(formatUtils.hexToString(entry)).toMatchSnapshot();
});
});
});

View File

@ -1,6 +1,7 @@
/* @flow */
import BigNumber from 'bignumber.js';
import EthereumjsUtil from 'ethereumjs-util';
export const decimalToHex = (dec: number): string => new BigNumber(dec).toString(16);
@ -28,4 +29,16 @@ export const strip = (str: string): string => {
return padLeftEven(str);
};
export const calcGasPrice = (price: BigNumber, limit: string): string => price.times(limit).toString();
export const calcGasPrice = (price: BigNumber, limit: string): string => price.times(limit).toString();
export const validateAddress = (address: string): ?string => {
const hasUpperCase = new RegExp('^(.*[A-Z].*)$');
if (address.length < 1) {
return 'Address is not set';
} else if (!EthereumjsUtil.isValidAddress(address)) {
return 'Address is not valid';
} else if (address.match(hasUpperCase) && !EthereumjsUtil.isValidChecksumAddress(address)) {
return 'Address is not a valid checksum';
}
return null;
}

View File

@ -1,11 +1,9 @@
/* @flow */
const currencyUnits: string = 'mbtc2';
// TODO: chagne currency units
const currencyUnitsConstant: string = 'mbtc2';
export const formatAmount = (n: number, coinInfo: any): string => {
export const formatAmount = (n: number, coinInfo: any, currencyUnits: string = currencyUnitsConstant): string => {
const amount = (n / 1e8);
if (coinInfo.isBitcoin && currencyUnits === 'mbtc' && amount <= 0.1 && n !== 0) {
const s = (n / 1e5).toString();

View File

@ -1,6 +1,5 @@
/* @flow */
import 'whatwg-fetch';
export const httpRequest = async (url: string, type: string = 'text'): any => {
@ -15,16 +14,6 @@ export const httpRequest = async (url: string, type: string = 'text'): any => {
await response.text();
}
throw new Error(`${url} ${response.statusText}`);
// return fetch(url, { credentials: 'same-origin' }).then((response) => {
// if (response.status === 200) {
// return response.text().then(result => (json ? JSON.parse(result) : result));
// } else {
// throw new Error(response.statusText);
// }
// })
};
export const JSONRequest = async (url: string): Promise<JSON> => {

View File

@ -86,7 +86,7 @@ class ConnectDevice extends Component<Props> {
</React.Fragment>
)}
</ConnectTrezorWrapper>
{this.props.showWebUsb && (
{this.props.showWebUsb && !this.props.showDisconnect && (
<React.Fragment>
<P>and</P>
<Button isWebUsb>

View File

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

View File

@ -20,7 +20,6 @@ import type { Props } from '../common';
import Row from '../Row';
import RowCoin from '../RowCoin';
const Wrapper = styled.div``;
const Text = styled.span`
@ -28,6 +27,10 @@ const Text = styled.span`
color: ${colors.TEXT_SECONDARY};
`;
const TooltipContent = styled.div`
font-size: ${FONT_SIZE.SMALLEST};
`;
const RowAccountWrapper = styled.div`
width: 100%;
display: flex;
@ -115,12 +118,12 @@ const AccountMenu = (props: Props): ?React$Element<string> => {
const selectedCoin = config.coins.find(c => c.network === location.state.network);
if (!selected || !selectedCoin) return;
const fiatRate = props.fiat.find(f => f.network === selectedCoin.network);
const deviceAccounts: Accounts = findDeviceAccounts(accounts, selected, location.state.network);
let selectedAccounts = deviceAccounts.map((account, i) => {
const selectedAccounts = deviceAccounts.map((account, i) => {
// const url: string = `${baseUrl}/network/${location.state.network}/account/${i}`;
const url: string = location.pathname.replace(/account+\/([0-9]*)/, `account/${i}`);
@ -158,26 +161,6 @@ const AccountMenu = (props: Props): ?React$Element<string> => {
);
});
if (selectedAccounts.length < 1) {
if (selected.connected) {
const url: string = location.pathname.replace(/account+\/([0-9]*)/, 'account/0');
selectedAccounts = (
<NavLink
to={url}
>
<Row column>
<RowAccountWrapper
isSelected
borderTop
>
Account #1
</RowAccountWrapper>
</Row>
</NavLink>
);
}
}
let discoveryStatus = null;
const discovery = props.discovery.find(d => d.deviceState === selected.state && d.network === location.state.network);
@ -204,7 +187,8 @@ const AccountMenu = (props: Props): ?React$Element<string> => {
} else {
discoveryStatus = (
<Tooltip
content={<React.Fragment>To add a new account, last account must have some transactions.</React.Fragment>}
maxWidth={300}
content={<TooltipContent>To add a new account, last account must have some transactions.</TooltipContent>}
placement="bottom"
>
<Row>

View File

@ -20,6 +20,8 @@ const RowCoinWrapper = styled.div`
display: block;
font-size: ${FONT_SIZE.BASE};
color: ${colors.TEXT_PRIMARY};
transition: background-color 0.3s, color 0.3s;
&:hover {
background-color: ${colors.GRAY_LIGHT};
}

View File

@ -19,6 +19,7 @@ const AsideWrapper = styled.aside`
position: relative;
top: 0;
width: 320px;
min-width: 320px;
overflow: hidden;
background: ${colors.MAIN};
border-right: 1px solid ${colors.DIVIDER};

View File

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

View File

@ -24,15 +24,15 @@ const TransitionContentWrapper = styled.div`
`;
const Footer = styled.div`
position: fixed;
position: relative;
width: 320px;
bottom: 0;
background: ${colors.MAIN};
border-right: 1px solid ${colors.DIVIDER};
`;
const Body = styled.div`
overflow: auto;
background: ${colors.LANDING};
width: 320px;
`;
const Help = styled.div`
@ -58,6 +58,25 @@ const A = styled.a`
}
`;
const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGroup> => (
<TransitionGroupWrapper component="div" className="transition-container">
<CSSTransition
key={props.animationType}
onExit={() => { window.dispatchEvent(new Event('resize')); }}
onExited={() => window.dispatchEvent(new Event('resize'))}
in
out
classNames={props.animationType}
appear={false}
timeout={300}
>
<TransitionContentWrapper>
{ props.children }
</TransitionContentWrapper>
</CSSTransition>
</TransitionGroupWrapper>
);
class LeftNavigation extends Component {
constructor(props) {
super(props);
@ -96,39 +115,6 @@ class LeftNavigation extends Component {
}
}
// TODO: refactor to transition component for reuse of transitions
getMenuTransition(children) {
return (
<TransitionGroupWrapper component="div" className="transition-container">
<CSSTransition
key={this.state.animationType}
onEnter={() => {
console.warn('ON ENTER');
}}
onEntering={() => {
console.warn('ON ENTERING (ACTIVE)');
}}
onExit={() => {
console.warn('ON EXIT');
window.dispatchEvent(new Event('resize'));
}}
onExiting={() => {
console.warn('ON EXITING (ACTIVE)');
}}
onExited={() => window.dispatchEvent(new Event('resize'))}
classNames={this.state.animationType}
appear={false}
timeout={30000}
in
out
>
<TransitionContentWrapper>
{children}
</TransitionContentWrapper>
</CSSTransition>
</TransitionGroupWrapper>);
}
shouldRenderAccounts() {
const { selectedDevice } = this.props.wallet;
return selectedDevice
@ -148,6 +134,22 @@ class LeftNavigation extends Component {
}
render() {
const { props } = this;
let menu;
if (this.shouldRenderAccounts()) {
menu = (
<TransitionMenu animationType="slide-left">
<AccountMenu {...props} />
</TransitionMenu>
);
} else if (this.shouldRenderCoins()) {
menu = (
<TransitionMenu animationType="slide-right">
<CoinMenu {...props} />
</TransitionMenu>
);
}
return (
<StickyContainer
location={this.props.location.pathname}
@ -163,8 +165,7 @@ class LeftNavigation extends Component {
/>
<Body>
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />}
{this.shouldRenderAccounts() && this.getMenuTransition(<AccountMenu {...this.props} />)}
{this.shouldRenderCoins() && this.getMenuTransition(<CoinMenu {...this.props} />)}
{menu}
</Body>
<Footer className="sticky-bottom">
<Help>

View File

@ -30,25 +30,27 @@ const SelectedAccount = (props: Props) => {
const {
account,
discovery,
network
network,
} = 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);
if (blockchain && !blockchain.connected) {
return (
<Notification
type="error"
<Notification
type="error"
title="Backend not connected"
actions={
[{
label: "Try again",
label: 'Try again',
callback: async () => {
await props.blockchainReconnect(network.network);
}
},
}]
} />
}
/>
);
}

View File

@ -7,7 +7,6 @@ import { connect } from 'react-redux';
import { reconnect } from 'actions/DiscoveryActions';
import SendFormActions from 'actions/SendFormActions';
import * as SessionStorageActions from 'actions/SessionStorageActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype';
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 & {
sendFormActions: typeof SendFormActions,
saveSessionStorage: typeof SessionStorageActions.save
}
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 => ({
blockchainReconnect: bindActionCreators(reconnect, dispatch),
sendFormActions: bindActionCreators(SendFormActions, dispatch),
saveSessionStorage: bindActionCreators(SessionStorageActions.save, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(AccountSend);

View File

@ -18,12 +18,17 @@ type Props = {
}
const Wrapper = styled.div`
padding-top: 20px;
border-top: 1px solid ${colors.DIVIDER};
`;
const StyledLink = styled(Link)`
text-decoration: none;
`;
const TransactionWrapper = styled.div`
border-bottom: 1px solid ${colors.DIVIDER};
padding: 14px 48px;
padding: 14px 0;
display: flex;
flex-direction: row;
align-items: center;
@ -39,13 +44,17 @@ const TransactionIcon = styled.div`
width: 36px;
height: 36px;
border-radius: 50%;
line-height: 30px;
line-height: 25px;
text-transform: uppercase;
user-select: none;
text-align: center;
color: ${props => props.color};
color: ${props => props.textColor};
background: ${props => props.background};
border-color: ${props => props.borderColor};
&:before {
content: ${props => props.color};
}
`;
const P = styled.p``;
@ -54,7 +63,9 @@ const TransactionName = styled.div`
flex: 1;
`;
const TransactionAmount = styled.div``;
const TransactionAmount = styled.div`
color: colors.TEXT_SECONDARY;
`;
class PendingTransactions extends Component<Props> {
getPendingTransactions() {
@ -62,7 +73,11 @@ class PendingTransactions extends Component<Props> {
}
getTransactionIconColors(tx: any) {
let iconColors = { };
let iconColors = {
textColor: '#fff',
background: '#000',
borderColor: '#000',
};
const bgColorFactory = new ColorHash({ lightness: 0.7 });
const textColorFactory = new ColorHash();
@ -73,26 +88,25 @@ class PendingTransactions extends Component<Props> {
if (!token) {
iconColors = {
color: '#ffffff',
textColor: '#ffffff',
background: '#000000',
borderColor: '#000000',
};
} else {
const bgColor: string = bgColorFactory.hex(token.name);
iconColors = {
color: textColorFactory.hex(token.name),
textColor: textColorFactory.hex(token.name),
background: bgColor,
borderColor: bgColor,
};
}
} else {
iconColors = {
color: textColorFactory.hex(tx.network),
textColor: textColorFactory.hex(tx.network),
background: bgColorFactory.hex(tx.network),
borderColor: bgColorFactory.hex(tx.network),
};
}
return iconColors;
}
@ -125,9 +139,11 @@ class PendingTransactions extends Component<Props> {
<Wrapper>
<H2>Pending transactions</H2>
{this.getPendingTransactions().map(tx => (
<TransactionWrapper>
<TransactionWrapper
key={tx.id}
>
<TransactionIcon
color={() => this.getTransactionIconColors(tx).color}
textColor={() => this.getTransactionIconColors(tx).textColor}
background={() => this.getTransactionIconColors(tx).background}
borderColor={() => this.getTransactionIconColors(tx).borderColor}
>
@ -137,13 +153,14 @@ class PendingTransactions extends Component<Props> {
</TransactionIcon>
<TransactionName>
<Link
<StyledLink
href={`${this.props.network.explorer.tx}${tx.id}`}
target="_blank"
rel="noreferrer noopener"
isGray
>
{this.getTransactionName(tx)}
</Link>
</StyledLink>
</TransactionName>
<TransactionAmount>

View File

@ -21,6 +21,10 @@ import PendingTransactions from './components/PendingTransactions';
import type { Props } from './Container';
// TODO: Decide on a small screen width for the whole app
// and put it inside config/variables.js
const SmallScreenWidth = '850px';
type State = {
isAdvancedSettingsHidden: boolean,
shouldAnimateAdvancedSettingsToggle: boolean,
@ -76,6 +80,7 @@ const SetMaxAmountButton = styled(Button)`
`;
const CurrencySelect = styled(Select)`
min-width: 77px;
height: 34px;
flex: 0.2;
`;
@ -108,12 +113,21 @@ const StyledLink = styled(Link)`
`;
const ToggleAdvancedSettingsWrapper = styled.div`
min-height: 40px;
margin-bottom: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
@media screen and (max-width: ${SmallScreenWidth}) {
${props => (props.isAdvancedSettingsHidden && css`
flex-direction: column;
`)}
}
`;
const ToggleAdvancedSettingsButton = styled(Button)`
min-height: 40px;
padding: 0;
display: flex;
align-items: center;
@ -121,7 +135,12 @@ const ToggleAdvancedSettingsButton = styled(Button)`
`;
const SendButton = styled(Button)`
min-width: 50%;
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
font-size: 13px;
@media screen and (max-width: ${SmallScreenWidth}) {
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};
}
`;
const AdvancedSettingsWrapper = styled.div`
@ -180,17 +199,17 @@ class AccountSend extends Component<Props, State> {
componentWillReceiveProps(newProps: Props) {
calculate(this.props, newProps);
validation(newProps);
this.props.saveSessionStorage();
}
getAddressInputState(address: string, addressErrors: string, addressWarnings: string) {
let state = '';
if (address && !addressErrors) {
state = 'success';
} else if (addressWarnings && !addressErrors) {
}
if (addressWarnings && !addressErrors) {
state = 'warning';
} else if (addressErrors) {
}
if (addressErrors) {
state = 'error';
}
return state;
@ -200,27 +219,19 @@ class AccountSend extends Component<Props, State> {
let state = '';
if (amountWarnings && !amountErrors) {
state = 'warning';
} else if (amountErrors) {
}
if (amountErrors) {
state = 'error';
}
return state;
}
getAmountInputBottomText(amountErrors: string, amountWarnings: string) {
let text = '';
if (amountWarnings && !amountErrors) {
text = amountWarnings;
} else if (amountErrors) {
text = amountErrors;
}
return text;
}
getGasLimitInputState(gasLimitErrors: string, gasLimitWarnings: string) {
let state = '';
if (gasLimitWarnings && !gasLimitErrors) {
state = 'warning';
} else if (gasLimitErrors) {
}
if (gasLimitErrors) {
state = 'error';
}
return state;
@ -230,7 +241,8 @@ class AccountSend extends Component<Props, State> {
let state = '';
if (gasPriceWarnings && !gasPriceErrors) {
state = 'warning';
} else if (gasPriceErrors) {
}
if (gasPriceErrors) {
state = 'error';
}
return state;
@ -275,6 +287,7 @@ class AccountSend extends Component<Props, State> {
total,
errors,
warnings,
infos,
data,
sending,
gasLimit,
@ -340,7 +353,7 @@ class AccountSend extends Component<Props, State> {
autoCapitalize="off"
spellCheck="false"
topLabel="Address"
bottomText={errors.address ? 'Wrong Address' : ''}
bottomText={errors.address || warnings.address || infos.address}
value={address}
onChange={event => onAddressChange(event.target.value)}
/>
@ -356,7 +369,7 @@ class AccountSend extends Component<Props, State> {
topLabel="Amount"
value={amount}
onChange={event => onAmountChange(event.target.value)}
bottomText={this.getAmountInputBottomText(errors.amount, warnings.amount)}
bottomText={errors.amount || warnings.amount || infos.amount}
sideAddons={[
(
<SetMaxAmountButton
@ -430,7 +443,9 @@ class AccountSend extends Component<Props, State> {
/>
</InputRow>
<ToggleAdvancedSettingsWrapper>
<ToggleAdvancedSettingsWrapper
isAdvancedSettingsHidden={this.state.isAdvancedSettingsHidden}
>
<ToggleAdvancedSettingsButton
isTransparent
onClick={() => this.handleToggleAdvancedSettingsButton()}
@ -448,6 +463,7 @@ class AccountSend extends Component<Props, State> {
{this.state.isAdvancedSettingsHidden && (
<SendButton
isDisabled={isSendButtonDisabled}
isAdvancedSettingsHidden={this.state.isAdvancedSettingsHidden}
onClick={() => onSend()}
>
{sendButtonText}
@ -486,7 +502,7 @@ class AccountSend extends Component<Props, State> {
</Tooltip>
</InputLabelWrapper>
)}
bottomText={errors.gasLimit ? errors.gasLimit : warnings.gasLimit}
bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit}
value={gasLimit}
isDisabled={networkSymbol === currency && data.length > 0}
onChange={event => onGasLimitChange(event.target.value)}
@ -520,7 +536,7 @@ class AccountSend extends Component<Props, State> {
</Tooltip>
</InputLabelWrapper>
)}
bottomText={errors.gasPrice ? errors.gasPrice : warnings.gasPrice}
bottomText={errors.gasPrice || warnings.gasPrice || infos.gasPrice}
value={gasPrice}
onChange={event => onGasPriceChange(event.target.value)}
/>

View File

@ -131,7 +131,12 @@ const AccountSummary = (props: Props) => {
placeholder="Search for the token"
loadingMessage={() => 'Loading...'}
noOptionsMessage={() => 'Token not found'}
onChange={token => props.addToken(token, account)}
onChange={(token) => {
const isAdded = tokens.find(t => t.symbol === token.symbol);
if (!isAdded) {
props.addToken(token, account);
}
}}
loadOptions={input => props.loadTokens(input, account.network)}
formatOptionLabel={(option) => {
const isAdded = tokens.find(t => t.symbol === option.symbol);

View File

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

View File

@ -1,12 +1,16 @@
import webpack from 'webpack';
import GitRevisionPlugin from 'git-revision-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import FlowWebpackPlugin from 'flow-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import {
SRC, BUILD, PORT, PUBLIC,
} from './constants';
const gitRevisionPlugin = new GitRevisionPlugin();
module.exports = {
watch: true,
mode: 'development',
@ -45,6 +49,7 @@ module.exports = {
{
loader: 'stylelint-custom-processor-loader',
options: {
emitWarning: true,
configPath: '.stylelintrc',
},
},
@ -88,6 +93,12 @@ module.exports = {
hints: false,
},
plugins: [
new webpack.DefinePlugin({
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()),
}),
new FlowWebpackPlugin({
reportingSeverity: 'warning'
}),
new HtmlWebpackPlugin({
chunks: ['index'],
template: `${SRC}index.html`,

View File

@ -1,5 +1,6 @@
import webpack from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import GitRevisionPlugin from 'git-revision-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import MiniCssExtractPlugin from '../../trezor-connect/node_modules/mini-css-extract-plugin';
@ -15,6 +16,8 @@ import {
PORT,
} from './constants';
const gitRevisionPlugin = new GitRevisionPlugin();
module.exports = {
watch: true,
mode: 'development',
@ -115,7 +118,6 @@ module.exports = {
filename: '[name].css',
chunkFilename: '[id].css',
}),
new HtmlWebpackPlugin({
chunks: ['index'],
template: `${SRC}index.html`,
@ -164,6 +166,7 @@ module.exports = {
new webpack.DefinePlugin({
LOCAL: JSON.stringify(`http://localhost:${PORT}/`),
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()),
}),
// ignore node lib from trezor-link

View File

@ -1,9 +1,13 @@
import webpack from 'webpack';
import GitRevisionPlugin from 'git-revision-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import FlowWebpackPlugin from 'flow-webpack-plugin';
import { SRC, BUILD, PUBLIC } from './constants';
const gitRevisionPlugin = new GitRevisionPlugin();
module.exports = {
mode: 'production',
entry: {
@ -62,7 +66,9 @@ module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.BUILD': JSON.stringify(process.env.BUILD),
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()
}),
new FlowWebpackPlugin(),
new HtmlWebpackPlugin({
chunks: ['index'],
template: `${SRC}index.html`,

View File

@ -255,6 +255,10 @@
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
"@types/jest@^23.0.0":
version "23.3.2"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.2.tgz#07b90f6adf75d42c34230c026a2529e56c249dbb"
"@webassemblyjs/ast@1.5.13":
version "1.5.13"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.13.tgz#81155a570bd5803a30ec31436bc2c9c0ede38f25"
@ -3304,6 +3308,10 @@ dom-helpers@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
dom-helpers@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
dom-serializer@0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@ -4326,6 +4334,10 @@ flow-parser@^0.*:
version "0.72.0"
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.72.0.tgz#6c8041e76ac7d0be1a71ce29c00cd1435fb6013c"
flow-webpack-plugin@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/flow-webpack-plugin/-/flow-webpack-plugin-1.2.0.tgz#1958821d16135028e391cad5ee2f3a4fa78197ec"
flush-write-stream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
@ -4539,6 +4551,10 @@ gh-got@^6.0.0:
got "^7.0.0"
is-plain-obj "^1.1.0"
git-revision-webpack-plugin@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/git-revision-webpack-plugin/-/git-revision-webpack-plugin-3.0.3.tgz#f909949d7851d1039ed530518f73f5d46594e66f"
github-username@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417"
@ -5622,7 +5638,7 @@ isobject@^3.0.0, isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
isomorphic-fetch@^2.1.1:
isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies:
@ -5814,6 +5830,14 @@ jest-environment-node@^23.4.0:
jest-mock "^23.2.0"
jest-util "^23.4.0"
jest-fetch-mock@^1.6.5:
version "1.6.5"
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-1.6.5.tgz#178fa1a937ef6f61fb8e8483b6d4602b17e0d96d"
dependencies:
"@types/jest" "^23.0.0"
isomorphic-fetch "^2.2.1"
promise-polyfill "^7.1.1"
jest-get-type@^22.1.0:
version "22.4.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4"
@ -8079,6 +8103,10 @@ promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
promise-polyfill@^7.1.1:
version "7.1.2"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-7.1.2.tgz#ab05301d8c28536301622d69227632269a70ca3b"
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@ -8100,13 +8128,6 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, pr
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@>=15.5.10, prop-types@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies:
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
@ -8115,6 +8136,13 @@ prop-types@^15.6.1:
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies:
loose-envify "^1.3.1"
object-assign "^4.1.1"
proxy-addr@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
@ -8451,12 +8479,6 @@ react-select@2.0.0:
react-input-autosize "^2.2.1"
react-transition-group "^2.2.1"
react-sticky-el@^1.0.20:
version "1.0.20"
resolved "https://registry.yarnpkg.com/react-sticky-el/-/react-sticky-el-1.0.20.tgz#b3c5e7128218633f440dc67aec239d1cd078342d"
dependencies:
prop-types ">=15.5.10"
react-transition-group@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10"
@ -8468,6 +8490,15 @@ react-transition-group@^2.2.1:
prop-types "^15.5.8"
warning "^3.0.0"
react-transition-group@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
dependencies:
dom-helpers "^3.3.1"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
"react@^15.4.2 || ^16.0.0":
version "16.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"