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

pull/60/head
Vladimir Volek 6 years ago committed by GitHub
commit 976222476b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

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

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

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

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

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

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

@ -1,13 +1,11 @@
/* @flow */ /* @flow */
import TrezorConnect, { import TrezorConnect, {
UI, DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT,
} from 'trezor-connect'; } from 'trezor-connect';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as WALLET from 'actions/constants/wallet';
import { getDuplicateInstanceNumber } from 'reducers/utils'; import { getDuplicateInstanceNumber } from 'reducers/utils';
import * as RouterActions from 'actions/RouterActions';
import { push } from 'react-router-redux';
import type { import type {
DeviceMessage, DeviceMessage,
@ -28,7 +26,6 @@ import type {
AsyncAction, AsyncAction,
Device, Device,
TrezorDevice, TrezorDevice,
RouterLocationState,
} from 'flowtype'; } from 'flowtype';
import * as DiscoveryActions from './DiscoveryActions'; import * as DiscoveryActions from './DiscoveryActions';
@ -79,19 +76,11 @@ export type TrezorConnectAction = {
type: typeof CONNECT.STOP_ACQUIRING, type: typeof CONNECT.STOP_ACQUIRING,
}; };
const sortDevices = (devices: Array<TrezorDevice>): Array<TrezorDevice> => devices.sort((a, b) => {
if (!a.ts || !b.ts) {
return -1;
}
return a.ts > b.ts ? -1 : 1;
});
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
// set listeners // set listeners
TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => { TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => {
// post event to reducers // post event to reducers
const type: DeviceMessageType = event.type; // assert flow type const type: DeviceMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
device: event.payload, device: event.payload,
@ -100,7 +89,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(UI_EVENT, (event: UiMessage): void => { TrezorConnect.on(UI_EVENT, (event: UiMessage): void => {
// post event to reducers // post event to reducers
const type: UiMessageType = event.type; // assert flow type const type: UiMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
payload: event.payload, payload: event.payload,
@ -109,7 +98,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(TRANSPORT_EVENT, (event: TransportMessage): void => { TrezorConnect.on(TRANSPORT_EVENT, (event: TransportMessage): void => {
// post event to reducers // post event to reducers
const type: TransportMessageType = event.type; // assert flow type const type: TransportMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
payload: event.payload, payload: event.payload,
@ -118,21 +107,22 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainMessage): void => { TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainMessage): void => {
// post event to reducers // post event to reducers
const type: BlockchainMessageType = event.type; // assert flow type const type: BlockchainMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({ dispatch({
type, type,
payload: event.payload, payload: event.payload,
}); });
}); });
/* global LOCAL */
// $FlowIssue LOCAL not declared // $FlowIssue LOCAL not declared
window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; // eslint-disable-line no-underscore-dangle
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/'; // window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/'; // eslint-disable-line no-underscore-dangle
try { try {
await TrezorConnect.init({ await TrezorConnect.init({
transportReconnect: true, transportReconnect: true,
debug: true, debug: false,
popup: false, popup: false,
webusb: true, webusb: true,
pendingTransportEvent: (getState().devices.length < 1), pendingTransportEvent: (getState().devices.length < 1),
@ -145,67 +135,11 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
} }
}; };
// selection from Aside dropdown button
// after device_connect event
// or after acquiring device
// device type could be local TrezorDevice or Device (from trezor-connect device_connect event)
export const onSelectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
// || device.isUsedElsewhere
// switch to initial url and reset this value
if (!device.features) {
dispatch(push(`/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`));
} else if (device.features.bootloader_mode) {
dispatch(push(`/device/${device.path}/bootloader`));
} else if (!device.features.initialized) {
dispatch(push(`/device/${device.features.device_id}/initialize`));
} else if (typeof device.instance === 'number') {
dispatch(push(`/device/${device.features.device_id}:${device.instance}`));
} else {
const deviceId: string = device.features.device_id;
const urlParams: RouterLocationState = getState().router.location.state;
// let url: string = `/device/${ device.features.device_id }/network/ethereum/account/0`;
let url: string = `/device/${deviceId}`;
let instance: ?number;
// check if device is not TrezorDevice type
if (!device.hasOwnProperty('ts')) {
// its device from trezor-connect (called in initConnectedDevice triggered by device_connect event)
// need to lookup if there are unavailable instances
const available: Array<TrezorDevice> = getState().devices.filter(d => d.path === device.path);
const latest: Array<TrezorDevice> = sortDevices(available);
if (latest.length > 0 && latest[0].instance) {
url += `:${latest[0].instance}`;
instance = latest[0].instance;
}
}
// check if current location is not set to this device
//dispatch( push(`/device/${ device.features.device_id }/network/etc/account/0`) );
if (urlParams.deviceInstance !== instance || urlParams.device !== deviceId) {
dispatch(push(url));
}
}
};
export const initConnectedDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch/* , getState: GetState */): void => {
// const selected = getState().wallet.selectedDevice;
// if (!selected || (selected && selected.state)) {
dispatch(onSelectDevice(device));
// }
// if (device.unacquired && selected && selected.path !== device.path && !selected.connected) {
// dispatch( onSelectDevice(device) );
// } else if (!selected) {
// dispatch( onSelectDevice(device) );
// }
};
// called after backend was initialized // called after backend was initialized
// set listeners for connect/disconnect // set listeners for connect/disconnect
export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const postInit = (): ThunkAction => (dispatch: Dispatch): void => {
const handleDeviceConnect = (device: Device) => { const handleDeviceConnect = (device: Device) => {
dispatch(initConnectedDevice(device)); dispatch(RouterActions.selectDevice(device));
}; };
TrezorConnect.off(DEVICE.CONNECT, handleDeviceConnect); TrezorConnect.off(DEVICE.CONNECT, handleDeviceConnect);
@ -214,58 +148,13 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetSta
TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect); TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect);
TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect);
const { devices } = getState(); // try to redirect to initial url
if (!dispatch(RouterActions.setInitialUrl())) {
const { initialPathname, initialParams } = getState().wallet; // if initial redirection fails try to switch to first available device
dispatch(RouterActions.selectFirstAvailableDevice());
if (initialPathname) {
dispatch({
type: WALLET.SET_INITIAL_URL,
// pathname: null,
// params: null
});
}
if (devices.length > 0) {
const unacquired: ?TrezorDevice = devices.find(d => d.features);
if (unacquired) {
dispatch(onSelectDevice(unacquired));
} else {
const latest: Array<TrezorDevice> = sortDevices(devices);
const firstConnected: ?TrezorDevice = latest.find(d => d.connected);
dispatch(onSelectDevice(firstConnected || latest[0]));
// TODO
if (initialParams) {
if (!initialParams.hasOwnProperty('network') && initialPathname !== getState().router.location.pathname) {
// dispatch( push(initialPathname) );
}
}
}
} }
}; };
export const switchToFirstAvailableDevice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { devices } = getState();
if (devices.length > 0) {
// TODO: Priority:
// 1. First Unacquired
// 2. First connected
// 3. Saved with latest timestamp
const unacquired = devices.find(d => !d.features);
if (unacquired) {
dispatch(initConnectedDevice(unacquired));
} else {
const latest: Array<TrezorDevice> = sortDevices(devices);
const firstConnected: ?TrezorDevice = latest.find(d => d.connected);
dispatch(onSelectDevice(firstConnected || latest[0]));
}
} else {
dispatch(push('/'));
}
};
export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (selected if (selected
@ -345,7 +234,7 @@ export const coinChanged = (network: ?string): ThunkAction => (dispatch: Dispatc
}; };
export function reload(): AsyncAction { export function reload(): AsyncAction {
return async (dispatch: Dispatch, getState: GetState): Promise<void> => { return async (): Promise<void> => {
}; };
} }
@ -391,13 +280,6 @@ export function acquire(): AsyncAction {
}; };
} }
export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispatch: Dispatch): void => {
if (device.features) {
const devUrl: string = `${device.features.device_id}${device.instance ? `:${device.instance}` : ''}`;
dispatch(push(`/device/${devUrl}/settings`));
}
};
// called from Aside - device menu (forget single instance) // called from Aside - device menu (forget single instance)
export const forget = (device: TrezorDevice): Action => ({ export const forget = (device: TrezorDevice): Action => ({
type: CONNECT.FORGET_REQUEST, type: CONNECT.FORGET_REQUEST,

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import icons from 'config/icons'; import icons from 'config/icons';
@ -34,8 +34,10 @@ const IconWrapper = styled.div`
&:hover, &:hover,
&:focus { &:focus {
border: 1px solid ${colors.TEXT_PRIMARY}; ${props => !props.checked && css`
background: ${props => (props.checked ? colors.TEXT_PRIMARY : colors.WHITE)}; 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}> <IconWrapper checked={checked}>
{checked && ( {checked && (
<Tick> <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> </Tick>
) )
} }

@ -29,7 +29,7 @@ const Copy = styled.div`
const Footer = ({ toggle }) => ( const Footer = ({ toggle }) => (
<Wrapper> <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="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 href="/assets/tos.pdf" target="_blank" rel="noreferrer noopener" isGreen>Terms</StyledLink>
<StyledLink onClick={toggle} isGreen>Show Log</StyledLink> <StyledLink onClick={toggle} isGreen>Show Log</StyledLink>

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

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

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

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

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

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

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

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

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

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

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

@ -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') { if (state.opened && action.device.path === state.device.path && action.device.status === 'occupied') {
return initialState; return initialState;
} }
return state; return state;
case DEVICE.DISCONNECT: case DEVICE.DISCONNECT:

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

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

@ -1,156 +1,46 @@
/* @flow */ /* @flow */
import { LOCATION_CHANGE/* , replace */ } from 'react-router-redux'; import { LOCATION_CHANGE, replace } from 'react-router-redux';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as RouterActions from 'actions/RouterActions';
import * as WALLET from 'actions/constants/wallet';
import * as NotificationActions from 'actions/NotificationActions';
import type { import type {
Middleware, Middleware,
MiddlewareAPI, MiddlewareAPI,
MiddlewareDispatch, MiddlewareDispatch,
Action, Action,
RouterLocationState,
TrezorDevice,
} from 'flowtype'; } from 'flowtype';
/** /**
* Middleware used for init application and managing router path. * Redux Middleware used for managing router path
* This middleware couldn't use async/await because LOCATION_CHANGE action is also synchronized with RouterReducer (react-router-redux)
*/ */
const pathToParams = (path: string): RouterLocationState => { const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
const urlParts: Array<string> = path.split('/').slice(1); // make sure that middleware should process this action
const params: RouterLocationState = {}; if (action.type !== LOCATION_CHANGE || api.getState().wallet.unloading) {
if (urlParts.length < 1 || path === '/') return params; // pass action
return next(action);
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')) { // compose valid url
const { config } = api.getState().localStorage; const validUrl = api.dispatch(RouterActions.getValidUrl(action));
const coin = config.coins.find(c => c.network === params.network); // override action state (to be stored in RouterReducer)
if (!coin) return false; const override = action;
if (!params.account) return false; override.payload.state = api.dispatch(RouterActions.pathToParams(validUrl));
const redirect = action.payload.pathname !== validUrl;
if (redirect) {
// override action pathname
override.payload.pathname = validUrl;
} }
// if (params.account) { // pass action
next(override);
// }
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; if (redirect) {
// replace invalid url
api.dispatch(replace(validUrl));
} }
// Pass all actions through by default return override;
return next(action);
}; };
export default RouterService; export default RouterService;

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

@ -1,11 +1,12 @@
/* @flow */ /* @flow */
import { DEVICE } from 'trezor-connect'; import { DEVICE } from 'trezor-connect';
import { LOCATION_CHANGE } from 'react-router-redux'; import { LOCATION_CHANGE } from 'react-router-redux';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as CONNECT from 'actions/constants/TrezorConnect';
import * as WalletActions from 'actions/WalletActions'; import * as WalletActions from 'actions/WalletActions';
import * as RouterActions from 'actions/RouterActions';
import * as NotificationActions from 'actions/NotificationActions';
import * as LocalStorageActions from 'actions/LocalStorageActions'; import * as LocalStorageActions from 'actions/LocalStorageActions';
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as SelectedAccountActions from 'actions/SelectedAccountActions'; import * as SelectedAccountActions from 'actions/SelectedAccountActions';
@ -22,40 +23,81 @@ import type {
*/ */
const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
const prevState = api.getState(); const prevState = api.getState();
const locationChange: boolean = action.type === LOCATION_CHANGE;
// Application live cycle starts HERE!
// Application live cycle starts here // when first LOCATION_CHANGE is called router does not have "location" set yet
if (locationChange) { if (action.type === LOCATION_CHANGE && !prevState.router.location) {
const { location } = api.getState().router; // initialize wallet
if (!location) { api.dispatch(WalletActions.init());
api.dispatch(WalletActions.init()); // set initial url
// load data from config.json and local storage // TODO: validate if initial url is potentially correct
api.dispatch(LocalStorageActions.loadData()); api.dispatch({
} type: WALLET.SET_INITIAL_URL,
pathname: action.payload.pathname,
state: {},
});
// pass action and break process
return next(action);
} }
// pass action // pass action
next(action); next(action);
if (action.type === DEVICE.CONNECT) { switch (action.type) {
api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device)); case WALLET.SET_INITIAL_URL:
api.dispatch(LocalStorageActions.loadData());
break;
case WALLET.SET_SELECTED_DEVICE:
if (action.device) {
// try to authorize device
api.dispatch(TrezorConnectActions.getSelectedDeviceState());
} else {
// try select different device
api.dispatch(RouterActions.selectFirstAvailableDevice());
}
break;
case DEVICE.CONNECT:
api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device));
break;
default: {
break;
}
}
// update common values ONLY if application is ready
if (!api.getState().wallet.ready) return action;
// double verification needed
// Corner case: LOCATION_CHANGE was called but pathname didn't changed (redirection from RouterService)
const prevLocation = prevState.router.location;
const currentLocation = api.getState().router.location;
if (action.type === LOCATION_CHANGE && prevLocation.pathname !== currentLocation.pathname) {
// watch for coin change
if (prevLocation.state.network !== currentLocation.state.network) {
api.dispatch({
type: CONNECT.COIN_CHANGED,
payload: {
network: currentLocation.state.network,
},
});
}
// watch for account change
if (prevLocation.state.network !== currentLocation.state.network || prevLocation.state.account !== currentLocation.state.account) {
api.dispatch(SelectedAccountActions.dispose());
}
// clear notifications
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
} }
// update common values in WallerReducer // update common values in WallerReducer
api.dispatch(WalletActions.updateSelectedValues(prevState, action)); api.dispatch(WalletActions.updateSelectedValues(prevState, action));
// update common values in SelectedAccountReducer // update common values in SelectedAccountReducer
api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action)); api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action));
// selected device changed
if (action.type === WALLET.SET_SELECTED_DEVICE) {
if (action.device) {
api.dispatch(TrezorConnectActions.getSelectedDeviceState());
} else {
api.dispatch(TrezorConnectActions.switchToFirstAvailableDevice());
}
}
return action; return action;
}; };

@ -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 */ 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; export default baseStyles;

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

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

@ -55,3 +55,13 @@ exports[`device utils get version 4`] = `"1"`;
exports[`device utils get version 5`] = `"1"`; exports[`device utils get version 5`] = `"1"`;
exports[`device utils get version 6`] = `"T"`; 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`;

@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`eth utils calcGasPrice 1`] = `"89090990901"`;
exports[`eth utils decimalToHex 1`] = `"0"`; exports[`eth utils decimalToHex 1`] = `"0"`;
exports[`eth utils decimalToHex 2`] = `"1"`; 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 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"`;

@ -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"`;

@ -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', () => { it('get version', () => {
const deviceMock = [ const deviceMock = [
{ }, { },

@ -1,11 +1,53 @@
import BigNumber from 'bignumber.js';
import * as ethUtils from '../ethUtils'; import * as ethUtils from '../ethUtils';
describe('eth utils', () => { describe('eth utils', () => {
it('decimalToHex', () => { it('decimalToHex', () => {
const input = [0, 1, 2, 100, 999]; const input = [0, 1, 2, 100, 9999999999];
input.forEach((entry) => { input.forEach((entry) => {
expect(ethUtils.decimalToHex(entry)).toMatchSnapshot(); 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();
});
});
}); });

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

@ -1,6 +1,7 @@
/* @flow */ /* @flow */
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import EthereumjsUtil from 'ethereumjs-util';
export const decimalToHex = (dec: number): string => new BigNumber(dec).toString(16); export const decimalToHex = (dec: number): string => new BigNumber(dec).toString(16);
@ -28,4 +29,16 @@ export const strip = (str: string): string => {
return padLeftEven(str); 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;
}

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

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
import 'whatwg-fetch'; import 'whatwg-fetch';
export const httpRequest = async (url: string, type: string = 'text'): any => { 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(); await response.text();
} }
throw new Error(`${url} ${response.statusText}`); 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> => { export const JSONRequest = async (url: string): Promise<JSON> => {

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

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

@ -20,7 +20,6 @@ import type { Props } from '../common';
import Row from '../Row'; import Row from '../Row';
import RowCoin from '../RowCoin'; import RowCoin from '../RowCoin';
const Wrapper = styled.div``; const Wrapper = styled.div``;
const Text = styled.span` const Text = styled.span`
@ -28,6 +27,10 @@ const Text = styled.span`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
`; `;
const TooltipContent = styled.div`
font-size: ${FONT_SIZE.SMALLEST};
`;
const RowAccountWrapper = styled.div` const RowAccountWrapper = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
@ -115,12 +118,12 @@ const AccountMenu = (props: Props): ?React$Element<string> => {
const selectedCoin = config.coins.find(c => c.network === location.state.network); const selectedCoin = config.coins.find(c => c.network === location.state.network);
if (!selected || !selectedCoin) return; if (!selected || !selectedCoin) return;
const fiatRate = props.fiat.find(f => f.network === selectedCoin.network); const fiatRate = props.fiat.find(f => f.network === selectedCoin.network);
const deviceAccounts: Accounts = findDeviceAccounts(accounts, selected, location.state.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 = `${baseUrl}/network/${location.state.network}/account/${i}`;
const url: string = location.pathname.replace(/account+\/([0-9]*)/, `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; let discoveryStatus = null;
const discovery = props.discovery.find(d => d.deviceState === selected.state && d.network === location.state.network); 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 { } else {
discoveryStatus = ( discoveryStatus = (
<Tooltip <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" placement="bottom"
> >
<Row> <Row>

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

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

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

@ -24,15 +24,15 @@ const TransitionContentWrapper = styled.div`
`; `;
const Footer = styled.div` const Footer = styled.div`
position: fixed; position: relative;
width: 320px;
bottom: 0; bottom: 0;
background: ${colors.MAIN}; background: ${colors.MAIN};
border-right: 1px solid ${colors.DIVIDER}; border-right: 1px solid ${colors.DIVIDER};
`; `;
const Body = styled.div` const Body = styled.div`
overflow: auto; width: 320px;
background: ${colors.LANDING};
`; `;
const Help = styled.div` 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 { class LeftNavigation extends Component {
constructor(props) { constructor(props) {
super(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() { shouldRenderAccounts() {
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
return selectedDevice return selectedDevice
@ -148,6 +134,22 @@ class LeftNavigation extends Component {
} }
render() { 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 ( return (
<StickyContainer <StickyContainer
location={this.props.location.pathname} location={this.props.location.pathname}
@ -163,8 +165,7 @@ class LeftNavigation extends Component {
/> />
<Body> <Body>
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />} {this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />}
{this.shouldRenderAccounts() && this.getMenuTransition(<AccountMenu {...this.props} />)} {menu}
{this.shouldRenderCoins() && this.getMenuTransition(<CoinMenu {...this.props} />)}
</Body> </Body>
<Footer className="sticky-bottom"> <Footer className="sticky-bottom">
<Help> <Help>

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

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

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

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

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

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

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

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

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

@ -255,6 +255,10 @@
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" 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": "@webassemblyjs/ast@1.5.13":
version "1.5.13" version "1.5.13"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.13.tgz#81155a570bd5803a30ec31436bc2c9c0ede38f25" 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" version "3.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" 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: dom-serializer@0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" 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" version "0.72.0"
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.72.0.tgz#6c8041e76ac7d0be1a71ce29c00cd1435fb6013c" 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: flush-write-stream@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" 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" got "^7.0.0"
is-plain-obj "^1.1.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: github-username@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417" 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" version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" 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" version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies: dependencies:
@ -5814,6 +5830,14 @@ jest-environment-node@^23.4.0:
jest-mock "^23.2.0" jest-mock "^23.2.0"
jest-util "^23.4.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: jest-get-type@^22.1.0:
version "22.4.3" version "22.4.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" 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" version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" 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: promise@^7.1.1:
version "7.3.1" version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" 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" loose-envify "^1.3.1"
object-assign "^4.1.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: prop-types@^15.6.1:
version "15.6.1" version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" 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" loose-envify "^1.3.1"
object-assign "^4.1.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: proxy-addr@~2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" 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-input-autosize "^2.2.1"
react-transition-group "^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: react-transition-group@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" 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" prop-types "^15.5.8"
warning "^3.0.0" 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": "react@^15.4.2 || ^16.0.0":
version "16.2.0" version "16.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"

Loading…
Cancel
Save