1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-24 09:18:09 +00:00
This commit is contained in:
Vladimir Volek 2018-10-03 12:59:03 +02:00
commit 151101a61c
57 changed files with 1519 additions and 772 deletions

View File

@ -10,6 +10,8 @@
.*/node_modules/oboe/test/.* .*/node_modules/oboe/test/.*
.*/_old/.* .*/_old/.*
.*/public/solidity/.* .*/public/solidity/.*
.*/build/.*
.*/build-devel/.*
[libs] [libs]
./src/flowtype/npm/redux_v3.x.x.js ./src/flowtype/npm/redux_v3.x.x.js

View File

@ -1,4 +1,4 @@
# TREZOR Wallet # Trezor Ethereum wallet
To install dependencies run `npm install` or `yarn` To install dependencies run `npm install` or `yarn`

View File

@ -1,4 +1,4 @@
# Browsers that we support # Browsers that we support
> 5% > 5%
IE > 9 # sorry IE > 8
last 10 versions last 5 versions

View File

@ -60,8 +60,8 @@
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-raven-middleware": "^1.2.0", "redux-raven-middleware": "^1.2.0",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"styled-components": "^3.4.9",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"styled-components": "^3.3.3",
"styled-media-query": "^2.0.2", "styled-media-query": "^2.0.2",
"styled-normalize": "^8.0.0", "styled-normalize": "^8.0.0",
"trezor-connect": "^5.0.32", "trezor-connect": "^5.0.32",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,64 @@
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
background: #ebebeb;
vertical-align: baseline;
text-align: center;
font-family: Roboto;
}
.header {
height: 40px;
width: 100%;
background: #1A1A1A;
margin-bottom: 100px;
}
.unsupported-browsers {
width: 100%;
font-size: 30px;
}
.main {
width: 100%;
text-align: center;
margin: 0 auto;
}
.box {
margin-top: 20px;
}
p {
width: 100%;
clear: both;
font-size: 15px;
}
img {
clear: both;
}
a {
clear: both;
color: #01B757;
width: 100%;
text-decoration: none;
display: block;
font-size: 20px;
}

View File

@ -2,8 +2,13 @@
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import type {
Action, ThunkAction, GetState, Dispatch,
} from 'flowtype';
import type { State, PendingTx } from 'reducers/PendingTxReducer'; import type { State, PendingTx } from 'reducers/PendingTxReducer';
export type PendingTxAction = { export type PendingTxAction = {
type: typeof PENDING.FROM_STORAGE, type: typeof PENDING.FROM_STORAGE,
payload: State payload: State
@ -15,9 +20,34 @@ export type PendingTxAction = {
tx: PendingTx, tx: PendingTx,
receipt?: Object, receipt?: Object,
} | { } | {
type: typeof PENDING.TX_NOT_FOUND, type: typeof PENDING.TX_REJECTED,
tx: PendingTx, tx: PendingTx,
} | { } | {
type: typeof PENDING.TX_TOKEN_ERROR, type: typeof PENDING.TX_TOKEN_ERROR,
tx: PendingTx, tx: PendingTx,
} }
export const reject = (tx: PendingTx): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
/*
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'warning',
title: 'Pending transaction rejected',
message: `Transaction with id: ${tx.id} not found.`,
cancelable: true,
actions: [
{
label: 'OK',
callback: () => {
dispatch({
type: PENDING.TX_RESOLVED,
tx,
});
},
},
],
},
});
*/
};

View File

@ -152,7 +152,7 @@ export const getValidUrl = (action: RouterAction): PayloadAction<string> => (dis
// Disallow displaying landing page // Disallow displaying landing page
// redirect to previous url // redirect to previous url
if (!shouldBeLandingPage && landingPageUrl) { if (!shouldBeLandingPage && landingPageUrl) {
return location.pathname; return dispatch(getFirstAvailableDeviceUrl()) || location.pathname;
} }
// Regular url change during application live cycle // Regular url change during application live cycle
@ -170,22 +170,10 @@ export const getValidUrl = (action: RouterAction): PayloadAction<string> => (dis
return composedUrl || location.pathname; return composedUrl || location.pathname;
}; };
/* /*
* Utility used in "selectDevice" and "selectFirstAvailableDevice" * Compose url from requested device object and returns url
* sorting device array by "ts" (timestamp) field
*/ */
const sortDevices = (devices: Array<TrezorDevice>): Array<TrezorDevice> => devices.sort((a, b) => { const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction<?string> => (dispatch: Dispatch, getState: GetState): ?string => {
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; let url: ?string;
if (!device.features) { if (!device.features) {
url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`; url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`;
@ -208,12 +196,7 @@ export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dis
} }
} }
} }
return url;
const currentParams: RouterLocationState = getState().router.location.state;
const requestedParams = dispatch(pathToParams(url));
if (currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) {
dispatch(goto(url));
}
}; };
/* /*
@ -223,17 +206,54 @@ export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dis
* 3. Saved with latest timestamp * 3. Saved with latest timestamp
* OR redirect to landing page * OR redirect to landing page
*/ */
export const selectFirstAvailableDevice = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const getFirstAvailableDeviceUrl = (): PayloadAction<?string> => (dispatch: Dispatch, getState: GetState): ?string => {
const { devices } = getState(); const { devices } = getState();
let url: ?string;
if (devices.length > 0) { if (devices.length > 0) {
const unacquired = devices.find(d => !d.features); const unacquired = devices.find(d => !d.features);
if (unacquired) { if (unacquired) {
dispatch(selectDevice(unacquired)); url = dispatch(getDeviceUrl(unacquired));
} else { } else {
const latest: Array<TrezorDevice> = sortDevices(devices); const latest: Array<TrezorDevice> = sortDevices(devices);
const firstConnected: ?TrezorDevice = latest.find(d => d.connected); const firstConnected: ?TrezorDevice = latest.find(d => d.connected);
dispatch(selectDevice(firstConnected || latest[0])); url = dispatch(getDeviceUrl(firstConnected || latest[0]));
} }
}
return url;
};
/*
* Utility used in "getDeviceUrl" and "getFirstAvailableDeviceUrl"
* 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;
});
/*
* Redirect to requested device
*/
export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const url: ?string = dispatch(getDeviceUrl(device));
if (!url) return;
const currentParams: RouterLocationState = getState().router.location.state;
const requestedParams = dispatch(pathToParams(url));
if (currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) {
dispatch(goto(url));
}
};
/*
* Redirect to first device or landing page
*/
export const selectFirstAvailableDevice = (): ThunkAction => (dispatch: Dispatch): void => {
const url = dispatch(getFirstAvailableDeviceUrl());
if (url) {
dispatch(goto(url));
} else { } else {
dispatch(gotoLandingPage()); dispatch(gotoLandingPage());
} }
@ -256,8 +276,7 @@ export const isLandingPageUrl = ($url?: string): PayloadAction<boolean> => (disp
if (typeof url !== 'string') { if (typeof url !== 'string') {
url = getState().router.location.pathname; url = getState().router.location.pathname;
} }
// TODO: add more landing page cases/urls to config.json (like /tools etc) return url === '/';
return (url === '/' || url === '/bridge');
}; };
/* /*

View File

@ -1,100 +1,210 @@
/* @flow */ /* @flow */
import { LOCATION_CHANGE } from 'react-router-redux'; import { LOCATION_CHANGE } from 'react-router-redux';
import * as WALLET from 'actions/constants/wallet';
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import * as NOTIFICATION from 'actions/constants/notification'; import * as DISCOVERY from 'actions/constants/discovery';
import * as TOKEN from 'actions/constants/token';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as stateUtils from 'reducers/utils'; import * as reducerUtils from 'reducers/utils';
import type { import type {
AsyncAction, PayloadAction,
Action, Action,
GetState, GetState,
Dispatch, Dispatch,
State, State,
} from 'flowtype'; } from 'flowtype';
type SelectedAccountState = $ElementType<State, 'selectedAccount'>;
export type SelectedAccountAction = { export type SelectedAccountAction = {
type: typeof ACCOUNT.DISPOSE, type: typeof ACCOUNT.DISPOSE,
} | { } | {
type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT, type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT,
payload: $ElementType<State, 'selectedAccount'> payload: SelectedAccountState,
}; };
type AccountStatus = {
type: string; // notification type
title: string; // notification title
message?: string; // notification message
shouldRender: boolean; // should render account page
}
export const dispose = (): Action => ({ export const dispose = (): Action => ({
type: ACCOUNT.DISPOSE, type: ACCOUNT.DISPOSE,
}); });
export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { const getAccountStatus = (state: State, selectedAccount: SelectedAccountState): ?AccountStatus => {
const locationChange: boolean = action.type === LOCATION_CHANGE; const device = state.wallet.selectedDevice;
if (!device || !device.state) {
return {
type: 'info',
title: 'Loading device...',
shouldRender: false,
};
}
const {
account,
discovery,
network,
} = selectedAccount;
// corner case: accountState didn't finish loading state after LOCATION_CHANGE action
if (!network) {
return {
type: 'info',
title: 'Loading account state...',
shouldRender: false,
};
}
const blockchain = state.blockchain.find(b => b.name === network.network);
if (blockchain && !blockchain.connected) {
return {
type: 'backend',
title: `${network.name} backend is not connected`,
shouldRender: false,
};
}
// account not found (yet). checking why...
if (!account) {
if (!discovery || discovery.waitingForDevice) {
if (device.connected) {
// case 1: device is connected but discovery not started yet (probably waiting for auth)
if (device.available) {
return {
type: 'info',
title: 'Loading accounts...',
shouldRender: false,
};
}
// case 2: device is unavailable (created with different passphrase settings) account cannot be accessed
return {
type: 'info',
title: `Device ${device.instanceLabel} is unavailable`,
message: 'Change passphrase settings to use this device',
shouldRender: false,
};
}
// case 3: device is disconnected
return {
type: 'info',
title: `Device ${device.instanceLabel} is disconnected`,
message: 'Connect device to load accounts',
shouldRender: false,
};
}
if (discovery.completed) {
// case 4: account not found and discovery is completed
return {
type: 'warning',
title: 'Account does not exist',
shouldRender: false,
};
}
// case 6: discovery is not completed yet
return {
type: 'info',
title: 'Loading accounts...',
shouldRender: false,
};
}
// Additional status: account does exists and it's visible but shouldn't be active
if (!device.connected) {
return {
type: 'info',
title: `Device ${device.instanceLabel} is disconnected`,
shouldRender: true,
};
}
if (!device.available) {
return {
type: 'info',
title: `Device ${device.instanceLabel} is unavailable`,
message: 'Change passphrase settings to use this device',
shouldRender: true,
};
}
// Additional status: account does exists, but waiting for discovery to complete
if (discovery && !discovery.completed) {
return {
type: 'info',
title: 'Loading accounts...',
shouldRender: true,
};
}
return null;
};
// list of all actions which has influence on "selectedAccount" reducer
// other actions will be ignored
const actions = [
LOCATION_CHANGE,
WALLET.SET_SELECTED_DEVICE,
WALLET.UPDATE_SELECTED_DEVICE,
...Object.values(ACCOUNT).filter(v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT), // exported values got unwanted "__esModule: true" as first element
...Object.values(DISCOVERY).filter(v => typeof v === 'string'),
...Object.values(TOKEN).filter(v => typeof v === 'string'),
...Object.values(PENDING).filter(v => typeof v === 'string'),
];
/*
* Called from WalletService
*/
export const observe = (prevState: State, action: Action): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
// ignore not listed actions
if (actions.indexOf(action.type) < 0) return false;
const state: State = getState(); const state: State = getState();
const { location } = state.router; const { location } = state.router;
// displayed route is not an account route
if (!location.state.account) return false;
// handle devices state change (from trezor-connect events or location change) // get new values for selected account
if (locationChange const account = reducerUtils.getSelectedAccount(state);
|| prevState.accounts !== state.accounts const network = reducerUtils.getSelectedNetwork(state);
|| prevState.discovery !== state.discovery const discovery = reducerUtils.getDiscoveryProcess(state);
|| prevState.tokens !== state.tokens const tokens = reducerUtils.getAccountTokens(state, account);
|| prevState.pending !== state.pending) { const pending = reducerUtils.getAccountPendingTx(state.pending, account);
const account = stateUtils.getSelectedAccount(state);
const network = stateUtils.getSelectedNetwork(state);
const discovery = stateUtils.getDiscoveryProcess(state);
const tokens = stateUtils.getAccountTokens(state, account);
const pending = stateUtils.getAccountPendingTx(state.pending, account);
const payload: $ElementType<State, 'selectedAccount'> = { // prepare new state for "selectedAccount" reducer
location: location.pathname, const newState: SelectedAccountState = {
location: state.router.location.pathname,
account, account,
network, network,
discovery, discovery,
tokens, tokens,
pending, pending,
notification: null,
shouldRender: false,
}; };
let needUpdate: boolean = false; // get "selectedAccount" status from newState
Object.keys(payload).forEach((key) => { const status = getAccountStatus(state, newState);
if (Array.isArray(payload[key])) { newState.notification = status || null;
if (Array.isArray(state.selectedAccount[key]) && payload[key].length !== state.selectedAccount[key].length) { newState.shouldRender = status ? status.shouldRender : true;
needUpdate = true; // check if newState is different than previous state
} const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, {
} else if (payload[key] !== state.selectedAccount[key]) { account: ['balance', 'nonce'],
needUpdate = true; discovery: ['accountIndex', 'interrupted', 'completed', 'waitingForBlockchain', 'waitingForDevice'],
}
}); });
if (needUpdate) { if (stateChanged) {
// update values in reducer
dispatch({ dispatch({
type: ACCOUNT.UPDATE_SELECTED_ACCOUNT, type: ACCOUNT.UPDATE_SELECTED_ACCOUNT,
payload, payload: newState,
});
if (location.state.send) {
const rejectedTxs = pending.filter(tx => tx.rejected);
rejectedTxs.forEach((tx) => {
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'warning',
title: 'Pending transaction rejected',
message: `Transaction with id: ${tx.id} not found.`,
cancelable: true,
actions: [
{
label: 'OK',
callback: () => {
dispatch({
type: PENDING.TX_RESOLVED,
tx,
});
},
},
],
},
});
}); });
} }
} return stateChanged;
}
}; };

View File

@ -2,6 +2,7 @@
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as ACCOUNT from 'actions/constants/account';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as SEND from 'actions/constants/send'; import * as SEND from 'actions/constants/send';
import * as WEB3 from 'actions/constants/web3'; import * as WEB3 from 'actions/constants/web3';
@ -45,10 +46,21 @@ export type SendFormAction = {
type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR, type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR,
} | SendTxAction; } | SendTxAction;
// list of all actions which has influence on "sendForm" reducer
// other actions will be ignored
const actions = [
ACCOUNT.UPDATE_SELECTED_ACCOUNT,
WEB3.GAS_PRICE_UPDATED,
...Object.values(SEND).filter(v => typeof v === 'string'),
];
/* /*
* Called from WalletService on EACH action * Called from WalletService
*/ */
export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
// ignore not listed actions
if (actions.indexOf(action.type) < 0) return;
const currentState = getState(); const currentState = getState();
// do not proceed if it's not "send" url // do not proceed if it's not "send" url
if (!currentState.router.location.state.send) return; if (!currentState.router.location.state.send) return;
@ -75,16 +87,9 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
let shouldUpdate: boolean = false; let shouldUpdate: boolean = false;
// check if "selectedAccount" reducer changed // check if "selectedAccount" reducer changed
const selectedAccountChanged = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, ['account', 'tokens', 'pending']); shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, {
if (selectedAccountChanged) { account: ['balance', 'nonce'],
// double check });
// there are only few fields that we are interested in
// check them to avoid unnecessary calculation and validation
const accountChanged = reducerUtils.observeChanges(prevState.selectedAccount.account, currentState.selectedAccount.account, ['balance', 'nonce']);
const tokensChanged = reducerUtils.observeChanges(prevState.selectedAccount.tokens, currentState.selectedAccount.tokens);
const pendingChanged = reducerUtils.observeChanges(prevState.selectedAccount.pending, currentState.selectedAccount.pending);
shouldUpdate = accountChanged || tokensChanged || pendingChanged;
}
// check if "sendForm" reducer changed // check if "sendForm" reducer changed
if (!shouldUpdate) { if (!shouldUpdate) {

View File

@ -71,9 +71,7 @@ export type TrezorConnectAction = {
type: typeof CONNECT.DEVICE_FROM_STORAGE, type: typeof CONNECT.DEVICE_FROM_STORAGE,
payload: Array<TrezorDevice> payload: Array<TrezorDevice>
} | { } | {
type: typeof CONNECT.START_ACQUIRING, type: typeof CONNECT.START_ACQUIRING | typeof CONNECT.STOP_ACQUIRING,
} | {
type: typeof CONNECT.STOP_ACQUIRING,
}; };
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
@ -128,10 +126,10 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
pendingTransportEvent: (getState().devices.length < 1), pendingTransportEvent: (getState().devices.length < 1),
}); });
} catch (error) { } catch (error) {
// dispatch({ dispatch({
// type: CONNECT.INITIALIZATION_ERROR, type: CONNECT.INITIALIZATION_ERROR,
// error error,
// }) });
} }
}; };
@ -206,7 +204,7 @@ export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispat
export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected: ?TrezorDevice = getState().wallet.selectedDevice; const selected: ?TrezorDevice = getState().wallet.selectedDevice;
if (device && device.features) { if (device.features) {
if (selected && selected.features && selected.features.device_id === device.features.device_id) { if (selected && selected.features && selected.features.device_id === device.features.device_id) {
dispatch(DiscoveryActions.stop(selected)); dispatch(DiscoveryActions.stop(selected));
} }
@ -218,7 +216,11 @@ export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch
device: instances[0], device: instances[0],
instances, instances,
}); });
} else {
dispatch(RouterActions.selectFirstAvailableDevice());
} }
} else {
dispatch(RouterActions.selectFirstAvailableDevice());
} }
}; };

View File

@ -1,16 +1,16 @@
/* @flow */ /* @flow */
import { LOCATION_CHANGE } from 'react-router-redux'; import { LOCATION_CHANGE } from 'react-router-redux';
import { DEVICE } from 'trezor-connect';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as stateUtils from 'reducers/utils'; import * as reducerUtils from 'reducers/utils';
import type { import type {
Device, Device,
TrezorDevice, TrezorDevice,
RouterLocationState, RouterLocationState,
ThunkAction, ThunkAction,
AsyncAction, PayloadAction,
Action, Action,
Dispatch, Dispatch,
GetState, GetState,
@ -80,16 +80,29 @@ export const clearUnavailableDevicesData = (prevState: State, device: Device): T
} }
}; };
// list of all actions which has influence on "selectedDevice" field in "wallet" reducer
// other actions will be ignored
const actions = [
LOCATION_CHANGE,
...Object.values(DEVICE).filter(v => typeof v === 'string'),
];
/*
* Called from WalletService
*/
export const observe = (prevState: State, action: Action): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
// ignore not listed actions
if (actions.indexOf(action.type) < 0) return false;
export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const locationChange: boolean = action.type === LOCATION_CHANGE;
const state: State = getState(); const state: State = getState();
const locationChanged = reducerUtils.observeChanges(prevState.router.location, state.router.location);
const device = reducerUtils.getSelectedDevice(state);
const selectedDeviceChanged = reducerUtils.observeChanges(state.wallet.selectedDevice, device);
// handle devices state change (from trezor-connect events or location change) // handle devices state change (from trezor-connect events or location change)
if (locationChange || prevState.devices !== state.devices) { if (locationChanged || selectedDeviceChanged) {
const device = stateUtils.getSelectedDevice(state); if (device && reducerUtils.isSelectedDevice(state.wallet.selectedDevice, device)) {
if (state.wallet.selectedDevice !== device) {
if (device && stateUtils.isSelectedDevice(state.wallet.selectedDevice, device)) {
dispatch({ dispatch({
type: WALLET.UPDATE_SELECTED_DEVICE, type: WALLET.UPDATE_SELECTED_DEVICE,
device, device,
@ -100,6 +113,7 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
device, device,
}); });
} }
return true;
} }
} return false;
}; };

View File

@ -139,7 +139,7 @@ export const resolvePendingTransactions = (network: string): PromiseAction<void>
const status = await instance.web3.eth.getTransaction(tx.id); const status = await instance.web3.eth.getTransaction(tx.id);
if (!status) { if (!status) {
dispatch({ dispatch({
type: PENDING.TX_NOT_FOUND, type: PENDING.TX_REJECTED,
tx, tx,
}); });
} else { } else {

View File

@ -4,5 +4,5 @@
export const FROM_STORAGE: 'pending__from_storage' = 'pending__from_storage'; export const FROM_STORAGE: 'pending__from_storage' = 'pending__from_storage';
export const ADD: 'pending__add' = 'pending__add'; export const ADD: 'pending__add' = 'pending__add';
export const TX_RESOLVED: 'pending__tx_resolved' = 'pending__tx_resolved'; export const TX_RESOLVED: 'pending__tx_resolved' = 'pending__tx_resolved';
export const TX_NOT_FOUND: 'pending__tx_not_found' = 'pending__tx_not_found'; export const TX_REJECTED: 'pending__tx_rejected' = 'pending__tx_rejected';
export const TX_TOKEN_ERROR: 'pending__tx_token_error' = 'pending__tx_token_error'; export const TX_TOKEN_ERROR: 'pending__tx_token_error' = 'pending__tx_token_error';

View File

@ -2,13 +2,13 @@ import React from 'react';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
import { TRANSITION } from 'config/variables'; import { TRANSITION, FONT_WEIGHT, FONT_SIZE } from 'config/variables';
const Wrapper = styled.button` const Wrapper = styled.button`
padding: ${props => (props.icon ? '4px 24px 4px 15px' : '11px 24px')}; padding: ${props => (props.icon ? '4px 24px 4px 15px' : '11px 24px')};
border-radius: 3px; border-radius: 3px;
font-size: 14px; font-size: ${FONT_SIZE.SMALL};
font-weight: 300; font-weight: ${FONT_WEIGHT.SMALLEST};
cursor: pointer; cursor: pointer;
background: ${colors.GREEN_PRIMARY}; background: ${colors.GREEN_PRIMARY};
color: ${colors.WHITE}; color: ${colors.WHITE};

View File

@ -26,18 +26,18 @@ const IconWrapper = styled.div`
border-radius: 2px; border-radius: 2px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: ${props => (props.checked ? colors.WHITE : colors.GREEN_PRIMARY)}; color: ${props => (props.isChecked ? colors.WHITE : colors.GREEN_PRIMARY)};
background: ${props => (props.checked ? colors.GREEN_PRIMARY : colors.WHITE)}; background: ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.WHITE)};
border: 1px solid ${props => (props.checked ? colors.GREEN_PRIMARY : colors.DIVIDER)}; border: 1px solid ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.DIVIDER)};
width: 24px; width: 24px;
height: 24px; height: 24px;
&:hover, &:hover,
&:focus { &:focus {
${props => !props.checked && css` ${props => !props.isChecked && css`
border: 1px solid ${colors.GREEN_PRIMARY}; border: 1px solid ${colors.GREEN_PRIMARY};
`} `}
background: ${props => (props.checked ? colors.GREEN_PRIMARY : colors.WHITE)}; background: ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.WHITE)};
} }
`; `;
@ -50,7 +50,7 @@ const Label = styled.div`
&:hover, &:hover,
&:focus { &:focus {
color: ${props => (props.checked ? colors.TEXT_PRIMARY : colors.TEXT_PRIMARY)}; color: ${props => (props.isChecked ? colors.TEXT_PRIMARY : colors.TEXT_PRIMARY)};
} }
`; `;
@ -63,7 +63,7 @@ class Checkbox extends PureComponent {
render() { render() {
const { const {
checked, isChecked,
children, children,
onClick, onClick,
} = this.props; } = this.props;
@ -73,20 +73,20 @@ class Checkbox extends PureComponent {
onKeyUp={e => this.handleKeyboard(e)} onKeyUp={e => this.handleKeyboard(e)}
tabIndex={0} tabIndex={0}
> >
<IconWrapper checked={checked}> <IconWrapper isChecked={isChecked}>
{checked && ( {isChecked && (
<Tick> <Tick>
<Icon <Icon
hoverColor={colors.WHITE} hoverColor={colors.WHITE}
size={26} size={26}
color={checked ? colors.WHITE : colors.GREEN_PRIMARY} color={isChecked ? colors.WHITE : colors.GREEN_PRIMARY}
icon={icons.SUCCESS} icon={icons.SUCCESS}
/> />
</Tick> </Tick>
) )
} }
</IconWrapper> </IconWrapper>
<Label checked={checked}>{children}</Label> <Label isChecked={isChecked}>{children}</Label>
</Wrapper> </Wrapper>
); );
} }
@ -94,7 +94,7 @@ class Checkbox extends PureComponent {
Checkbox.propTypes = { Checkbox.propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
checked: PropTypes.bool, isChecked: PropTypes.bool,
children: PropTypes.string, children: PropTypes.string,
}; };

View File

@ -1,7 +1,7 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
import colors from 'config/colors'; import colors from 'config/colors';
const Wrapper = styled.header` const Wrapper = styled.header`
@ -13,12 +13,9 @@ const Wrapper = styled.header`
fill: ${colors.WHITE}; fill: ${colors.WHITE};
height: 28px; height: 28px;
width: 100px; width: 100px;
flex: 1;
} }
`; `;
const LinkWrapper = styled.div``;
const LayoutWrapper = styled.div` const LayoutWrapper = styled.div`
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -26,12 +23,21 @@ const LayoutWrapper = styled.div`
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
@media screen and (max-width: 1170px) { @media screen and (max-width: 1170px) {
padding: 0 25px; padding: 0 25px;
} }
`; `;
const Left = styled.div`
flex: 1;
display: flex;
justify-content: flex-start;
`;
const Right = styled.div``;
const A = styled.a` const A = styled.a`
color: ${colors.WHITE}; color: ${colors.WHITE};
margin-left: 24px; margin-left: 24px;
@ -55,6 +61,8 @@ const A = styled.a`
const Header = (): React$Element<string> => ( const Header = (): React$Element<string> => (
<Wrapper> <Wrapper>
<LayoutWrapper> <LayoutWrapper>
<Left>
<NavLink to="/">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 163.7 41.9" width="100%" height="100%" preserveAspectRatio="xMinYMin meet"> <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 163.7 41.9" width="100%" height="100%" preserveAspectRatio="xMinYMin meet">
<polygon points="101.1,12.8 118.2,12.8 118.2,17.3 108.9,29.9 118.2,29.9 118.2,35.2 101.1,35.2 101.1,30.7 110.4,18.1 101.1,18.1" /> <polygon points="101.1,12.8 118.2,12.8 118.2,17.3 108.9,29.9 118.2,29.9 118.2,35.2 101.1,35.2 101.1,30.7 110.4,18.1 101.1,18.1" />
<path d="M158.8,26.9c2.1-0.8,4.3-2.9,4.3-6.6c0-4.5-3.1-7.4-7.7-7.4h-10.5v22.3h5.8v-7.5h2.2l4.1,7.5h6.7L158.8,26.9z M154.7,22.5 h-4V18h4c1.5,0,2.5,0.9,2.5,2.2C157.2,21.6,156.2,22.5,154.7,22.5z" /> <path d="M158.8,26.9c2.1-0.8,4.3-2.9,4.3-6.6c0-4.5-3.1-7.4-7.7-7.4h-10.5v22.3h5.8v-7.5h2.2l4.1,7.5h6.7L158.8,26.9z M154.7,22.5 h-4V18h4c1.5,0,2.5,0.9,2.5,2.2C157.2,21.6,156.2,22.5,154.7,22.5z" />
@ -64,12 +72,14 @@ const Header = (): React$Element<string> => (
<path d="M79.4,20.3c0-4.5-3.1-7.4-7.7-7.4H61.2v22.3H67v-7.5h2.2l4.1,7.5H80l-4.9-8.3C77.2,26.1,79.4,24,79.4,20.3z M71,22.5h-4V18 h4c1.5,0,2.5,0.9,2.5,2.2C73.5,21.6,72.5,22.5,71,22.5z" /> <path d="M79.4,20.3c0-4.5-3.1-7.4-7.7-7.4H61.2v22.3H67v-7.5h2.2l4.1,7.5H80l-4.9-8.3C77.2,26.1,79.4,24,79.4,20.3z M71,22.5h-4V18 h4c1.5,0,2.5,0.9,2.5,2.2C73.5,21.6,72.5,22.5,71,22.5z" />
<polygon points="40.5,12.8 58.6,12.8 58.6,18.1 52.4,18.1 52.4,35.2 46.6,35.2 46.6,18.1 40.5,18.1 " /> <polygon points="40.5,12.8 58.6,12.8 58.6,18.1 52.4,18.1 52.4,35.2 46.6,35.2 46.6,18.1 40.5,18.1 " />
</svg> </svg>
<LinkWrapper> </NavLink>
</Left>
<Right>
<A href="https://trezor.io/" target="_blank" rel="noreferrer noopener">TREZOR</A> <A href="https://trezor.io/" target="_blank" rel="noreferrer noopener">TREZOR</A>
<A href="https://doc.satoshilabs.com/trezor-user/" target="_blank" rel="noreferrer noopener">Docs</A> <A href="https://doc.satoshilabs.com/trezor-user/" target="_blank" rel="noreferrer noopener">Docs</A>
<A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener">Blog</A> <A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener">Blog</A>
<A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener">Support</A> <A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener">Support</A>
</LinkWrapper> </Right>
</LayoutWrapper> </LayoutWrapper>
</Wrapper> </Wrapper>
); );

View File

@ -3,6 +3,16 @@ import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
import styled, { keyframes } from 'styled-components'; import styled, { keyframes } from 'styled-components';
const chooseIconAnimationType = (canAnimate, isActive) => {
if (canAnimate) {
if (isActive) {
return rotate180up;
}
return rotate180down;
}
return null;
};
// TODO: make animation of icons better // TODO: make animation of icons better
const rotate180up = keyframes` const rotate180up = keyframes`
from { from {
@ -23,7 +33,7 @@ const rotate180down = keyframes`
`; `;
const SvgWrapper = styled.svg` const SvgWrapper = styled.svg`
animation: ${props => (props.canAnimate ? (props.isActive ? rotate180up : rotate180down) : null)} 0.2s linear 1 forwards; animation: ${props => chooseIconAnimationType(props.canAnimate, props.isActive)} 0.2s linear 1 forwards;
:hover { :hover {
path { path {

View File

@ -0,0 +1,113 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import Icon from 'components/Icon';
import ICONS from 'config/icons';
import colors from 'config/colors';
import { Notification } from 'components/Notification';
import { getIcon, getColor } from 'utils/notification';
const Wrapper = styled.div``;
const Header = styled.div`
display: flex;
cursor: pointer;
justify-content: space-between;
background: ${colors.WHITE};
align-items: center;
padding: 5px 10px;
border-bottom: 1px solid ${colors.BACKGROUND};
`;
const Left = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
`;
const Right = styled.div``;
const Body = styled.div``;
const Title = styled.div`
color: ${props => props.color};
`;
class Group extends Component {
constructor() {
super();
this.state = {
visibleCount: 1,
visible: true,
};
}
toggle = () => {
if (this.state.visible) {
this.setState({
visible: false,
visibleCount: 0,
});
} else {
this.setState({
visible: true,
visibleCount: this.props.groupNotifications.length,
});
}
}
render() {
const { type, groupNotifications } = this.props;
const color = getColor(type);
return (
<Wrapper>
{groupNotifications.length > 1 && (
<Header onClick={this.toggle}>
<Left>
<Icon
color={color}
size={30}
icon={getIcon(type)}
/>
<Title color={color}>
{groupNotifications.length} {groupNotifications.length > 1 ? `${type}s` : type}
</Title>
</Left>
<Right>
<Icon
icon={ICONS.ARROW_DOWN}
color={colors.TEXT_SECONDARY}
size={24}
isActive={this.state.visible}
canAnimate
/>
</Right>
</Header>
)}
<Body>
{groupNotifications
.slice(0, this.state.visibleCount)
.map(notification => (
<Notification
key={notification.title}
type={notification.type}
title={notification.title}
message={notification.message}
/>
))}
</Body>
</Wrapper>
);
}
}
Group.propTypes = {
type: PropTypes.string,
groupNotifications: PropTypes.arrayOf({
key: PropTypes.string,
type: PropTypes.string,
title: PropTypes.string,
message: PropTypes.string,
}),
};
export default Group;

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import styled from 'styled-components';
import { PRIORITY } from 'constants/notifications';
import Group from './components/Group';
const Wrapper = styled.div``;
class NotificationsGroup extends Component {
constructor() {
super();
this.notifications = [
{ type: 'warning', title: 'adddaa', message: 'aaaa' },
{ type: 'error', title: 'aaddda', message: 'aaaa' },
{ type: 'info', title: 'aafffa', message: 'aaaa' },
{ type: 'error', title: 'aggaa', message: 'aaaa' },
{ type: 'warning', title: 'aasssa', message: 'aaaa' },
{ type: 'success', title: 'afaa', message: 'aaaa' },
{ type: 'error', title: 'aada', message: 'aaaa' },
{ type: 'error', title: 'aafffa', message: 'aaaa' },
];
}
groupNotifications = notifications => notifications
.reduce((acc, obj) => {
const key = obj.type;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj);
return acc;
}, {});
sortByPriority(notifications) {
return notifications;
}
render() {
const { notifications } = this;
const notificationGroups = this.groupNotifications(notifications);
const sortedNotifications = this.sortByPriority(notificationGroups);
return (
<Wrapper>
{Object.keys(sortedNotifications).map(group => (
<Group
groupNotifications={notificationGroups[group]}
type={group}
/>
))}
</Wrapper>
);
}
}
export default NotificationsGroup;

View File

@ -1,22 +1,44 @@
/* @flow */
import React from 'react'; import React from 'react';
import media from 'styled-media-query';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { getColor, getIcon } from 'utils/notification';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import icons from 'config/icons'; import icons from 'config/icons';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables'; import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
import * as NotificationActions from 'actions/NotificationActions'; import * as NotificationActions from 'actions/NotificationActions';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import type { Action, State } from 'flowtype';
import NotificationButton from './components/NotificationButton'; import NotificationButton from './components/NotificationButton';
type Props = {
key?: number,
notifications: $ElementType<State, 'notifications'>,
close: (notif?: any) => Action,
};
type NProps = {
type: string,
cancelable?: boolean;
title: string;
message?: string;
actions?: Array<any>;
close?: typeof NotificationActions.close,
loading?: boolean
};
const Wrapper = styled.div` const Wrapper = styled.div`
position: relative; position: relative;
color: ${colors.TEXT_PRIMARY}; color: ${colors.TEXT_PRIMARY};
background: ${colors.TEXT_SECONDARY}; background: ${colors.TEXT_SECONDARY};
padding: 24px 48px 24px 24px; padding: 24px 48px 24px 24px;
display: flex; display: flex;
min-height: 90px;
flex-direction: row; flex-direction: row;
text-align: left; text-align: left;
@ -39,11 +61,19 @@ const Wrapper = styled.div`
color: ${colors.ERROR_PRIMARY}; color: ${colors.ERROR_PRIMARY};
background: ${colors.ERROR_SECONDARY}; background: ${colors.ERROR_SECONDARY};
`} `}
${media.lessThan('610px')`
flex-direction: column;
`}
`; `;
const Body = styled.div` const Body = styled.div`
display: flex; display: flex;
margin-right: 40px; width: 60%;
${media.lessThan('610px')`
width: 100%;
`}
`; `;
const Title = styled.div` const Title = styled.div`
@ -65,54 +95,41 @@ const Message = styled.div`
const StyledIcon = styled(Icon)` const StyledIcon = styled(Icon)`
position: relative; position: relative;
top: -7px; top: -7px;
min-width: 20px;
`; `;
const MessageContent = styled.div` const IconWrapper = styled.div`
height: 20px; min-width: 20px;
display: flex;
`; `;
const Texts = styled.div` const Texts = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
`; `;
const AdditionalContent = styled.div` const AdditionalContent = styled.div`
flex: 1;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: flex-end; align-items: flex-end;
flex: 1;
`; `;
const ActionContent = styled.div` const ActionContent = styled.div`
display: flex;
justify-content: right; justify-content: right;
align-items: flex-end; align-items: flex-end;
${media.lessThan('610px')`
width: 100%;
padding: 10px 0 0 30px;
align-items: flex-start;
`}
`; `;
export const Notification = (props: NProps): React$Element<string> => { export const Notification = (props: NProps): React$Element<string> => {
const close: Function = typeof props.close === 'function' ? props.close : () => {}; // TODO: add default close action const close: Function = typeof props.close === 'function' ? props.close : () => {}; // TODO: add default close action
const getIconColor = (type) => {
let color;
switch (type) {
case 'info':
color = colors.INFO_PRIMARY;
break;
case 'error':
color = colors.ERROR_PRIMARY;
break;
case 'warning':
color = colors.WARNING_PRIMARY;
break;
case 'success':
color = colors.SUCCESS_PRIMARY;
break;
default:
color = null;
}
return color;
};
return ( return (
<Wrapper type={props.type}> <Wrapper type={props.type}>
@ -120,18 +137,19 @@ export const Notification = (props: NProps): React$Element<string> => {
{props.cancelable && ( {props.cancelable && (
<CloseClick onClick={() => close()}> <CloseClick onClick={() => close()}>
<Icon <Icon
color={getIconColor(props.type)} color={getColor(props.type)}
icon={icons.CLOSE} icon={icons.CLOSE}
size={20} size={20}
/> />
</CloseClick> </CloseClick>
)} )}
<Body> <Body>
<MessageContent> <IconWrapper>
<StyledIcon <StyledIcon
color={getIconColor(props.type)} color={getColor(props.type)}
icon={icons[props.type.toUpperCase()]} icon={getIcon(props.type)}
/> />
</IconWrapper>
<Texts> <Texts>
<Title>{ props.title }</Title> <Title>{ props.title }</Title>
{ props.message && ( { props.message && (
@ -140,7 +158,6 @@ export const Notification = (props: NProps): React$Element<string> => {
</Message> </Message>
) } ) }
</Texts> </Texts>
</MessageContent>
</Body> </Body>
<AdditionalContent> <AdditionalContent>
{props.actions && props.actions.length > 0 && ( {props.actions && props.actions.length > 0 && (
@ -161,7 +178,7 @@ export const Notification = (props: NProps): React$Element<string> => {
); );
}; };
export const NotificationGroup = (props) => { export const NotificationGroup = (props: Props) => {
const { notifications, close } = props; const { notifications, close } = props;
return notifications.map(n => ( return notifications.map(n => (
<Notification <Notification

View File

@ -15,10 +15,11 @@ const disabledColor = colors.TEXT_PRIMARY;
const StyledTextarea = styled.textarea` const StyledTextarea = styled.textarea`
width: 100%; width: 100%;
padding: 12px; min-height: 85px;
margin-bottom: 10px;
padding: 6px 12px;
box-sizing: border-box; box-sizing: border-box;
min-height: 25px; border: 1px solid ${props => (props.borderColor ? props.borderColor : colors.DIVIDER)};
border: ${props => (props.isError ? `1px solid ${colors.ERROR_PRIMARY}` : `1px solid ${colors.DIVIDER}`)};
border-radius: 2px; border-radius: 2px;
resize: none; resize: none;
outline: none; outline: none;
@ -56,8 +57,9 @@ const StyledTextarea = styled.textarea`
} }
&:disabled { &:disabled {
border: 1px solid ${disabledColor}; pointer-events: none;
cursor: not-allowed; background: ${colors.GRAY_LIGHT};
color: ${colors.TEXT_SECONDARY};
&::-webkit-input-placeholder { &::-webkit-input-placeholder {
color: ${disabledColor}; color: ${disabledColor};
@ -86,6 +88,23 @@ const TopLabel = styled.span`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
`; `;
const BottomText = styled.span`
font-size: ${FONT_SIZE.SMALLER};
color: ${props => (props.color ? props.color : colors.TEXT_SECONDARY)};
`;
const getColor = (inputState) => {
let color = '';
if (inputState === 'success') {
color = colors.SUCCESS_PRIMARY;
} else if (inputState === 'warning') {
color = colors.WARNING_PRIMARY;
} else if (inputState === 'error') {
color = colors.ERROR_PRIMARY;
}
return color;
};
const Textarea = ({ const Textarea = ({
className, className,
placeholder = '', placeholder = '',
@ -95,10 +114,13 @@ const Textarea = ({
onBlur, onBlur,
isDisabled, isDisabled,
onChange, onChange,
isError,
topLabel, topLabel,
state = '',
bottomText = '',
}) => ( }) => (
<Wrapper> <Wrapper
className={className}
>
{topLabel && ( {topLabel && (
<TopLabel>{topLabel}</TopLabel> <TopLabel>{topLabel}</TopLabel>
)} )}
@ -111,14 +133,20 @@ const Textarea = ({
value={value} value={value}
placeholder={placeholder} placeholder={placeholder}
onChange={onChange} onChange={onChange}
isError={isError} borderColor={getColor(state)}
/> />
{bottomText && (
<BottomText
color={getColor(state)}
>
{bottomText}
</BottomText>
)}
</Wrapper> </Wrapper>
); );
Textarea.propTypes = { Textarea.propTypes = {
className: PropTypes.string, className: PropTypes.string,
isError: PropTypes.bool,
onFocus: PropTypes.func, onFocus: PropTypes.func,
onBlur: PropTypes.func, onBlur: PropTypes.func,
onChange: PropTypes.func, onChange: PropTypes.func,
@ -127,6 +155,8 @@ Textarea.propTypes = {
value: PropTypes.string, value: PropTypes.string,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
topLabel: PropTypes.node, topLabel: PropTypes.node,
state: PropTypes.string,
bottomText: PropTypes.string,
}; };
export default Textarea; export default Textarea;

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import RcTooltip from 'rc-tooltip'; import RcTooltip from 'rc-tooltip';
import colors from 'config/colors'; import colors from 'config/colors';
import Link from 'components/Link';
import styled from 'styled-components'; import styled from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -11,7 +12,7 @@ const Wrapper = styled.div`
position: absolute; position: absolute;
z-index: 1070; z-index: 1070;
visibility: visible; visibility: visible;
border: 1px solid ${colors.DIVIDER}; border: none;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
} }
@ -22,15 +23,15 @@ const Wrapper = styled.div`
.rc-tooltip-inner { .rc-tooltip-inner {
padding: 8px 10px; padding: 8px 10px;
color: ${colors.TEXT_SECONDARY}; color: ${colors.WHITE};
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
text-align: left; text-align: left;
text-decoration: none; text-decoration: none;
background-color: ${colors.WHITE}; background-color: ${colors.TOOLTIP_BACKGROUND};
border-radius: 3px; border-radius: 3px;
min-height: 34px; min-height: 34px;
border: 1px solid ${colors.WHITE}; border: none;
} }
.rc-tooltip-arrow, .rc-tooltip-arrow,
@ -48,7 +49,7 @@ const Wrapper = styled.div`
bottom: -6px; bottom: -6px;
margin-left: -6px; margin-left: -6px;
border-width: 6px 6px 0; border-width: 6px 6px 0;
border-top-color: ${colors.DIVIDER}; border-top-color: ${colors.BLACK};
} }
.rc-tooltip-placement-top .rc-tooltip-arrow-inner, .rc-tooltip-placement-top .rc-tooltip-arrow-inner,
@ -57,7 +58,7 @@ const Wrapper = styled.div`
bottom: 2px; bottom: 2px;
margin-left: -6px; margin-left: -6px;
border-width: 6px 6px 0; border-width: 6px 6px 0;
border-top-color: ${colors.WHITE}; border-top-color: ${colors.TOOLTIP_BACKGROUND};
} }
.rc-tooltip-placement-top .rc-tooltip-arrow { .rc-tooltip-placement-top .rc-tooltip-arrow {
@ -78,7 +79,7 @@ const Wrapper = styled.div`
left: -5px; left: -5px;
margin-top: -6px; margin-top: -6px;
border-width: 6px 6px 6px 0; border-width: 6px 6px 6px 0;
border-right-color: ${colors.DIVIDER}; border-right-color: ${colors.TOOLTIP_BACKGROUND};
} }
.rc-tooltip-placement-right .rc-tooltip-arrow-inner, .rc-tooltip-placement-right .rc-tooltip-arrow-inner,
@ -87,7 +88,7 @@ const Wrapper = styled.div`
left: 1px; left: 1px;
margin-top: -6px; margin-top: -6px;
border-width: 6px 6px 6px 0; border-width: 6px 6px 6px 0;
border-right-color: ${colors.WHITE}; border-right-color: ${colors.TOOLTIP_BACKGROUND};
} }
.rc-tooltip-placement-right .rc-tooltip-arrow { .rc-tooltip-placement-right .rc-tooltip-arrow {
@ -109,7 +110,7 @@ const Wrapper = styled.div`
right: -5px; right: -5px;
margin-top: -6px; margin-top: -6px;
border-width: 6px 0 6px 6px; border-width: 6px 0 6px 6px;
border-left-color: ${colors.DIVIDER}; border-left-color: ${colors.TOOLTIP_BACKGROUND};
} }
.rc-tooltip-placement-left .rc-tooltip-arrow-inner, .rc-tooltip-placement-left .rc-tooltip-arrow-inner,
@ -118,7 +119,7 @@ const Wrapper = styled.div`
right: 1px; right: 1px;
margin-top: -6px; margin-top: -6px;
border-width: 6px 0 6px 6px; border-width: 6px 0 6px 6px;
border-left-color: ${colors.WHITE}; border-left-color: ${colors.TOOLTIP_BACKGROUND};
} }
.rc-tooltip-placement-left .rc-tooltip-arrow { .rc-tooltip-placement-left .rc-tooltip-arrow {
@ -140,7 +141,7 @@ const Wrapper = styled.div`
top: -5px; top: -5px;
margin-left: -6px; margin-left: -6px;
border-width: 0 6px 6px; border-width: 0 6px 6px;
border-bottom-color: ${colors.DIVIDER}; border-bottom-color: ${colors.TOOLTIP_BACKGROUND};
} }
.rc-tooltip-placement-bottom .rc-tooltip-arrow-inner, .rc-tooltip-placement-bottom .rc-tooltip-arrow-inner,
@ -149,7 +150,7 @@ const Wrapper = styled.div`
top: 1px; top: 1px;
margin-left: -6px; margin-left: -6px;
border-width: 0 6px 6px; border-width: 0 6px 6px;
border-bottom-color: ${colors.WHITE}; border-bottom-color: ${colors.TOOLTIP_BACKGROUND};
} }
.rc-tooltip-placement-bottom .rc-tooltip-arrow { .rc-tooltip-placement-bottom .rc-tooltip-arrow {
@ -165,6 +166,20 @@ const Wrapper = styled.div`
} }
`; `;
const Content = styled.div`
padding-bottom: 10px;
text-align: justify;
`;
const ContentWrapper = styled.div``;
const ReadMore = styled.div`
padding: 10px 0 5px 0;
text-align: center;
width: 100%;
color: ${colors.WHITE};
border-top: 1px solid ${colors.TEXT_SECONDARY};
`;
class Tooltip extends Component { class Tooltip extends Component {
render() { render() {
const { const {
@ -172,6 +187,7 @@ class Tooltip extends Component {
placement, placement,
content, content,
children, children,
readMoreLink,
maxWidth, maxWidth,
} = this.props; } = this.props;
return ( return (
@ -184,7 +200,16 @@ 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={content} overlay={() => (
<ContentWrapper>
<Content>{content}</Content>
{readMoreLink && (
<Link target="_blank" href={readMoreLink}>
<ReadMore>Read more</ReadMore>
</Link>
)
}
</ContentWrapper>)}
> >
{children} {children}
</RcTooltip> </RcTooltip>
@ -205,6 +230,7 @@ Tooltip.propTypes = {
PropTypes.element, PropTypes.element,
PropTypes.string, PropTypes.string,
]), ]),
readMoreLink: PropTypes.string,
}; };
export default Tooltip; export default Tooltip;

View File

@ -115,20 +115,21 @@ class Input extends Component {
/> />
)} )}
<StyledInput <StyledInput
hasIcon={!!this.props.state} hasIcon={this.getIcon(this.props.state).length > 0}
innerRef={this.props.innerRef} innerRef={this.props.innerRef}
hasAddon={!!this.props.sideAddons} hasAddon={!!this.props.sideAddons}
type={this.props.type} type={this.props.type}
color={this.getColor(this.props.state)} color={this.getColor(this.props.state)}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
autoComplete={this.props.autoComplete} autocomplete={this.props.autocomplete}
autoCorrect={this.props.autoCorrect} autocorrect={this.props.autocorrect}
autoCapitalize={this.props.autoCapitalize} autocapitalize={this.props.autocapitalize}
spellCheck={this.props.spellCheck} spellCheck={this.props.spellCheck}
value={this.props.value} value={this.props.value}
onChange={this.props.onChange} onChange={this.props.onChange}
borderColor={this.getColor(this.props.state)} borderColor={this.getColor(this.props.state)}
disabled={this.props.isDisabled} disabled={this.props.isDisabled}
name={this.props.name}
/> />
</InputIconWrapper> </InputIconWrapper>
{this.props.sideAddons && this.props.sideAddons.map(sideAddon => sideAddon)} {this.props.sideAddons && this.props.sideAddons.map(sideAddon => sideAddon)}
@ -150,9 +151,9 @@ Input.propTypes = {
innerRef: PropTypes.func, 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, value: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
@ -161,6 +162,7 @@ Input.propTypes = {
topLabel: PropTypes.node, topLabel: PropTypes.node,
sideAddons: PropTypes.arrayOf(PropTypes.node), sideAddons: PropTypes.arrayOf(PropTypes.node),
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
name: PropTypes.string,
}; };
Input.defaultProps = { Input.defaultProps = {

View File

@ -63,12 +63,6 @@ const ErrorMessage = styled.div`
`; `;
export default class DuplicateDevice extends Component<Props, State> { export default class DuplicateDevice extends Component<Props, State> {
state: State;
input: ?HTMLInputElement;
keyboardHandler: (event: KeyboardEvent) => void;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -85,6 +79,12 @@ export default class DuplicateDevice extends Component<Props, State> {
}; };
} }
state: State;
input: ?HTMLInputElement;
keyboardHandler: (event: KeyboardEvent) => void;
componentDidMount(): void { componentDidMount(): void {
// one time autofocus // one time autofocus
if (this.input) this.input.focus(); if (this.input) this.input.focus();

View File

@ -1,18 +1,25 @@
/* @flow */ /* @flow */
import React, { Component } from 'react'; import React, { Component } from 'react';
import raf from 'raf'; import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE, TRANSITION } from 'config/variables';
import { H2 } from 'components/Heading'; import { H2 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import { FONT_SIZE } from 'config/variables';
import Link from 'components/Link';
import Checkbox from 'components/Checkbox'; import Checkbox from 'components/Checkbox';
import Button from 'components/Button'; import Button from 'components/Button';
import Input from 'components/inputs/Input'; import Input from 'components/inputs/Input';
import styled from 'styled-components';
import type { Props } from '../../index'; import type { Props } from '../../index';
type State = {
deviceLabel: string,
shouldShowSingleInput: boolean,
passphraseInputValue: string,
passphraseCheckInputValue: string,
doPassphraseInputsMatch: boolean,
isPassphraseHidden: boolean,
};
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 24px 48px; padding: 24px 48px;
max-width: 390px; max-width: 390px;
@ -45,315 +52,229 @@ const Footer = styled.div`
justify-content: center; justify-content: center;
`; `;
type State = { const LinkButton = styled(Button)`
deviceLabel: string; padding: 0;
singleInput: boolean; margin: 0;
passphrase: string; text-decoration: none;
passphraseRevision: string; cursor: pointer;
passphraseFocused: boolean; transition: ${TRANSITION.HOVER};
passphraseRevisionFocused: boolean; font-size: ${FONT_SIZE.SMALLER};
passphraseRevisionTouched: boolean; border-radius: 0;
match: boolean; border-bottom: 1px solid ${colors.GREEN_PRIMARY};
visible: boolean; background: transparent;
}
export default class PinModal extends Component<Props, State> { &,
&:visited,
&:active,
&:hover {
color: ${colors.GREEN_PRIMARY};
}
&:hover {
border-color: transparent;
background: transparent;
}
`;
class Passphrase extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const device = props.modal.opened ? props.modal.device : null; const device = props.modal.opened ? props.modal.device : null;
if (!device) return; if (!device) {
return;
}
// check if this device is already known // Check if this device is already known
const selected = props.wallet.selectedDevice; // if device is already known then only one input is presented
const { selectedDevice } = props.wallet;
let deviceLabel = device.label; let deviceLabel = device.label;
let singleInput = false; let shouldShowSingleInput = false;
if (selected && selected.path === device.path) { if (selectedDevice && selectedDevice.path === device.path) {
deviceLabel = selected.instanceLabel; deviceLabel = selectedDevice.instanceLabel;
singleInput = selected.remember || selected.state !== null; shouldShowSingleInput = selectedDevice.remember || selectedDevice.state !== null;
} }
this.state = { this.state = {
deviceLabel, deviceLabel,
singleInput, shouldShowSingleInput,
passphrase: '', passphraseInputValue: '',
passphraseRevision: '', passphraseCheckInputValue: '',
passphraseFocused: false, doPassphraseInputsMatch: true,
passphraseRevisionFocused: false, isPassphraseHidden: true,
passphraseRevisionTouched: false,
match: true,
visible: false,
}; };
} }
keyboardHandler: (event: KeyboardEvent) => void;
state: State; state: State;
passphraseInput: ?HTMLInputElement; componentDidMount() {
this.passphraseInput.focus();
passphraseRevisionInput: ?HTMLInputElement; this.handleKeyPress = this.handleKeyPress.bind(this);
window.addEventListener('keypress', this.handleKeyPress, false);
keyboardHandler(event: KeyboardEvent): void {
if (event.keyCode === 13) {
event.preventDefault();
//this.passphraseInput.blur();
//this.passphraseRevisionInput.blur();
//this.passphraseInput.type = 'text';
//this.passphraseRevisionInput.type = 'text';
this.submit();
// TODO: set timeout, or wait for blur event
//onPassphraseSubmit(passphrase, passphraseCached);
//raf(() => onPassphraseSubmit(passphrase));
}
} }
componentWillUnmount() {
componentDidMount(): void { window.removeEventListener('keypress', this.handleKeyPress, false);
// one time autofocus
if (this.passphraseInput) this.passphraseInput.focus();
this.keyboardHandler = this.keyboardHandler.bind(this);
window.addEventListener('keydown', this.keyboardHandler, false);
// document.oncontextmenu = (event) => {
// const el = window.event.srcElement || event.target;
// const type = el.tagName.toLowerCase() || '';
// if (type === 'input') {
// return false;
// }
// };
} }
// we don't want to keep password inside "value" attribute, handleKeyPress: (event: KeyboardEvent) => void;
// so we need to replace it thru javascript
componentDidUpdate() {
const {
passphrase,
passphraseRevision,
passphraseFocused,
passphraseRevisionFocused,
visible,
} = this.state;
// } = this.props.modal;
const passphraseInputValue: string = passphrase; passphraseInput: HTMLInputElement;
const passphraseRevisionInputValue: string = passphraseRevision;
// if (!visible && !passphraseFocused) {
// passphraseInputValue = passphrase.replace(/./g, '•');
// }
// if (!visible && !passphraseRevisionFocused) {
// passphraseRevisionInputValue = passphraseRevision.replace(/./g, '•');
// }
handleInputChange(event: Event) {
const { target } = event;
if (target instanceof HTMLInputElement) {
const inputValue = target.value;
const inputName = target.name;
if (this.passphraseInput) { let doPassphraseInputsMatch = false;
// this.passphraseInput.value = passphraseInputValue; if (inputName === 'passphraseInputValue') {
// this.passphraseInput.setAttribute('type', visible || (!visible && !passphraseFocused) ? 'text' : 'password'); // If passphrase is not hidden the second input should get filled automatically
this.passphraseInput.setAttribute('type', visible ? 'text' : 'password'); // and should be disabled
} if (this.state.isPassphraseHidden) {
if (this.passphraseRevisionInput) { doPassphraseInputsMatch = inputValue === this.state.passphraseCheckInputValue;
// this.passphraseRevisionInput.value = passphraseRevisionInputValue;
// this.passphraseRevisionInput.setAttribute('type', visible || (!visible && !passphraseRevisionFocused) ? 'text' : 'password');
this.passphraseRevisionInput.setAttribute('type', visible ? 'text' : 'password');
}
}
componentWillUnmount(): void {
window.removeEventListener('keydown', this.keyboardHandler, false);
// this.passphraseInput.type = 'text';
// this.passphraseInput.style.display = 'none';
// this.passphraseRevisionInput.type = 'text';
// this.passphraseRevisionInput.style.display = 'none';
}
onPassphraseChange = (input: string, value: string): void => {
// https://codepen.io/MiDri/pen/PGqvrO
// or
// https://github.com/zakangelle/react-password-mask/blob/master/src/index.js
if (input === 'passphrase') {
this.setState(previousState => ({
match: previousState.singleInput || previousState.passphraseRevision === value,
passphrase: value,
}));
if (this.state.visible && this.passphraseRevisionInput) {
this.setState({
match: true,
passphraseRevision: value,
});
this.passphraseRevisionInput.value = value;
}
} else { } else {
this.setState(previousState => ({ // Since both inputs are same they really do match
match: previousState.passphrase === value, // see: comment above
passphraseRevision: value, this.setState({
passphraseRevisionTouched: true, passphraseCheckInputValue: inputValue,
})); });
doPassphraseInputsMatch = true;
}
} else if (inputName === 'passphraseCheckInputValue') {
doPassphraseInputsMatch = inputValue === this.state.passphraseInputValue;
}
if (this.state.shouldShowSingleInput) {
doPassphraseInputsMatch = true;
}
this.setState({
[inputName]: inputValue,
doPassphraseInputsMatch,
});
} }
} }
onPassphraseFocus = (input: string): void => { handleCheckboxClick() {
if (input === 'passphrase') { // If passphrase was visible and now shouldn't be --> delete the value of passphraseCheckInputValue
this.setState({ // doPassphraseInputsMatch
passphraseFocused: true, // - if passphrase was visible and now shouldn't be --> doPassphraseInputsMatch = false
}); // - because passphraseCheckInputValue will be empty string
// - if passphrase wasn't visibe and now should be --> doPassphraseInputsMatch = true
// - because passphraseCheckInputValue will be same as passphraseInputValue
let doInputsMatch = false;
if (this.state.shouldShowSingleInput || this.state.passphraseInputValue === this.state.passphraseCheckInputValue) {
doInputsMatch = true;
} else { } else {
this.setState({ doInputsMatch = !!this.state.isPassphraseHidden;
passphraseRevisionFocused: true,
});
}
} }
onPassphraseBlur = (input: string): void => {
if (input === 'passphrase') {
this.setState({
passphraseFocused: false,
});
} else {
this.setState({
passphraseRevisionFocused: false,
});
}
}
onPassphraseShow = (): void => {
this.setState({
visible: true,
});
if (this.passphraseRevisionInput) {
this.passphraseRevisionInput.disabled = true;
this.passphraseRevisionInput.value = this.state.passphrase;
this.setState(previousState => ({ this.setState(previousState => ({
passphraseRevision: previousState.passphrase, isPassphraseHidden: !previousState.isPassphraseHidden,
match: true, passphraseInputValue: previousState.isPassphraseHidden ? previousState.passphraseInputValue : '',
passphraseCheckInputValue: previousState.isPassphraseHidden ? previousState.passphraseInputValue : '',
doPassphraseInputsMatch: doInputsMatch,
})); }));
} }
}
onPassphraseHide = (): void => { submitPassphrase(shouldLeavePassphraseBlank: boolean = false) {
this.setState({
visible: false,
});
if (this.passphraseRevisionInput) {
const emptyPassphraseRevisionValue = '';
this.passphraseRevisionInput.value = emptyPassphraseRevisionValue;
this.setState(previousState => ({
passphraseRevision: emptyPassphraseRevisionValue,
match: emptyPassphraseRevisionValue === previousState.passphrase,
}));
this.passphraseRevisionInput.disabled = false;
}
}
submit = (empty: boolean = false): void => {
const { onPassphraseSubmit } = this.props.modalActions; const { onPassphraseSubmit } = this.props.modalActions;
const { passphrase, match } = this.state; const passphrase = this.state.passphraseInputValue;
if (!match) return;
//this.passphraseInput.type = 'text';
// this.passphraseInput.style.display = 'none';
//this.passphraseInput.setAttribute('readonly', 'readonly');
// this.passphraseRevisionInput.type = 'text';
//this.passphraseRevisionInput.style.display = 'none';
//this.passphraseRevisionInput.setAttribute('readonly', 'readonly');
// const p = passphrase;
// Reset state so same passphrase isn't filled when the modal will be visible again
this.setState({ this.setState({
passphrase: '', passphraseInputValue: '',
passphraseRevision: '', passphraseCheckInputValue: '',
passphraseFocused: false, doPassphraseInputsMatch: true,
passphraseRevisionFocused: false, isPassphraseHidden: true,
visible: false,
}); });
raf(() => onPassphraseSubmit(empty ? '' : passphrase)); onPassphraseSubmit(shouldLeavePassphraseBlank ? '' : passphrase);
}
handleKeyPress(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
console.warn('ENTER', this.state);
if (this.state.doPassphraseInputsMatch) {
this.submitPassphrase();
}
}
} }
render() { render() {
if (!this.props.modal.opened) return null; if (!this.props.modal.opened) {
return null;
}
const {
device,
} = this.props.modal;
const {
deviceLabel,
singleInput,
passphrase,
passphraseRevision,
passphraseFocused,
passphraseRevisionFocused,
visible,
match,
passphraseRevisionTouched,
} = this.state;
let passphraseInputType: string = visible || (!visible && !passphraseFocused) ? 'text' : 'password';
let passphraseRevisionInputType: string = visible || (!visible && !passphraseRevisionFocused) ? 'text' : 'password';
passphraseInputType = passphraseRevisionInputType = 'text';
//let passphraseInputType: string = visible || passphraseFocused ? "text" : "password";
//let passphraseRevisionInputType: string = visible || passphraseRevisionFocused ? "text" : "password";
const showPassphraseCheckboxFn: Function = visible ? this.onPassphraseHide : this.onPassphraseShow;
return ( return (
<Wrapper> <Wrapper>
<H2>Enter { deviceLabel } passphrase</H2> <H2>Enter {this.state.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
innerRef={(element) => { this.passphraseInput = element; }} innerRef={(input) => { this.passphraseInput = input; }}
onChange={event => this.onPassphraseChange('passphrase', event.currentTarget.value)} name="passphraseInputValue"
type={passphraseInputType} type={this.state.isPassphraseHidden ? 'password' : 'text'}
autoComplete="off" autocorrect="off"
autoCorrect="off" autocapitalize="off"
autoCapitalize="off" autocomplete="off"
spellCheck="false" value={this.state.passphraseInputValue}
data-lpignore="true" onChange={event => this.handleInputChange(event)}
onFocus={() => this.onPassphraseFocus('passphrase')}
onBlur={() => this.onPassphraseBlur('passphrase')}
tabIndex="0"
/> />
</Row> </Row>
{!singleInput && ( {!this.state.shouldShowSingleInput && (
<Row> <Row>
<Label>Re-enter passphrase</Label> <Label>Re-enter passphrase</Label>
<Input <Input
innerRef={(element) => { this.passphraseRevisionInput = element; }} name="passphraseCheckInputValue"
onChange={event => this.onPassphraseChange('revision', event.currentTarget.value)} type={this.state.isPassphraseHidden ? 'password' : 'text'}
type={passphraseRevisionInputType} autocorrect="off"
autoComplete="off" autocapitalize="off"
autoCorrect="off" autocomplete="off"
autoCapitalize="off" value={this.state.passphraseCheckInputValue}
spellCheck="false" onChange={event => this.handleInputChange(event)}
data-lpignore="true" isDisabled={!this.state.isPassphraseHidden}
onFocus={() => this.onPassphraseFocus('revision')}
onBlur={() => this.onPassphraseBlur('revision')}
/> />
{!match && passphraseRevisionTouched && <PassphraseError>Passphrases do not match</PassphraseError> }
</Row> </Row>
) } )}
{!this.state.doPassphraseInputsMatch && (
<PassphraseError>Passphrases do not match</PassphraseError>
)}
<Row> <Row>
<Checkbox onClick={showPassphraseCheckboxFn} checked={visible}>Show passphrase</Checkbox> <Checkbox
isChecked={!this.state.isPassphraseHidden}
onClick={() => this.handleCheckboxClick()}
>
Show passphrase
</Checkbox>
</Row> </Row>
<Row> <Row>
<Button type="button" disabled={!match} onClick={() => this.submit()}>Enter</Button> <Button
isDisabled={!this.state.doPassphraseInputsMatch}
onClick={() => this.submitPassphrase()}
>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>
<P isSmaller> <P isSmaller>
<Link isGreen onClick={() => this.submit(true)}>Leave passphrase blank</Link> <LinkButton
isGreen
onClick={() => this.submitPassphrase(true)}
>Leave passphrase blank
</LinkButton>
</P> </P>
</Footer> </Footer>
</Wrapper> </Wrapper>
); );
} }
} }
export default Passphrase;

View File

@ -0,0 +1,36 @@
/* @flow */
import * as React from 'react';
import { Notification } from 'components/Notification';
import type { Props } from '../../index';
// There could be only one account notification
export default (props: Props) => {
const { notification } = props.selectedAccount;
if (notification) {
if (notification.type === 'backend') {
// special case: backend is down
// TODO: this is a different component with "auto resolve" button
return (
<Notification
type="error"
title={notification.title}
message={notification.message}
actions={
[{
label: 'Connect',
callback: async () => {
await props.blockchainReconnect('trop');
},
}]
}
/>
);
// return (<Notification type="error" title={notification.title} message={notification.message} />);
}
return (<Notification type={notification.type} title={notification.title} message={notification.message} />);
}
return null;
};

View File

@ -0,0 +1,20 @@
/* @flow */
import * as React from 'react';
import { Notification } from 'components/Notification';
import type { Props } from '../../index';
export default (props: Props) => {
const { notifications, close } = props;
return notifications.map(n => (
<Notification
key={n.title}
type={n.type}
title={n.title}
message={n.message}
cancelable={n.cancelable}
actions={n.actions}
close={close}
/>
));
};

View File

@ -0,0 +1,17 @@
/* @flow */
import * as React from 'react';
import { Notification } from 'components/Notification';
import type { Props } from '../../index';
export default (props: Props) => {
const { location } = props.router;
if (!location) return null;
const notifications: Array<Notification> = [];
// if (location.state.device) {
// notifications.push(<Notification key="example" type="info" title="Static example" />);
// }
return notifications;
};

View File

@ -0,0 +1,55 @@
/* @flow */
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype';
import { reconnect } from 'actions/DiscoveryActions';
import * as NotificationActions from 'actions/NotificationActions';
import StaticNotifications from './components/Static';
import AccountNotifications from './components/Account';
import ActionNotifications from './components/Action';
export type StateProps = {
router: $ElementType<State, 'router'>;
notifications: $ElementType<State, 'notifications'>;
selectedAccount: $ElementType<State, 'selectedAccount'>;
wallet: $ElementType<State, 'wallet'>;
blockchain: $ElementType<State, 'blockchain'>;
children?: React.Node;
}
export type DispatchProps = {
close: typeof NotificationActions.close;
blockchainReconnect: typeof reconnect;
}
export type Props = StateProps & DispatchProps;
type OwnProps = {};
const Notifications = (props: Props) => (
<React.Fragment>
<StaticNotifications {...props} />
<AccountNotifications {...props} />
<ActionNotifications {...props} />
</React.Fragment>
);
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
router: state.router,
notifications: state.notifications,
selectedAccount: state.selectedAccount,
wallet: state.wallet,
blockchain: state.blockchain,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
close: bindActionCreators(NotificationActions.close, dispatch),
blockchainReconnect: bindActionCreators(reconnect, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Notifications);

View File

@ -32,4 +32,5 @@ export default {
ERROR_SECONDARY: '#FFE9E9', ERROR_SECONDARY: '#FFE9E9',
LABEL_COLOR: '#A9A9A9', LABEL_COLOR: '#A9A9A9',
TOOLTIP_BACKGROUND: '#333333',
}; };

View File

@ -0,0 +1,8 @@
export default {
PRIORITY: {
error: 0,
warning: 1,
info: 2,
success: 3,
},
};

View File

@ -10,8 +10,29 @@
<meta name="keywords" content="trezor wallet" /> <meta name="keywords" content="trezor wallet" />
<meta name="author" content="satoshilabs s.r.o." /> <meta name="author" content="satoshilabs s.r.o." />
<meta name="viewport" content="width=1024, initial-scale=1"> <meta name="viewport" content="width=1024, initial-scale=1">
<!--[if lt IE 8]>
<link media="all" rel="stylesheet" href="/unsupported-browsers/style.css" />
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<![endif]-->
</head> </head>
<body> <body>
<!--[if lt IE 8]>
<div class="unsupported-browsers">
<div class="header"></div>
<h1>Your browser is not supported</h1>
<p>Please choose one of the supported browsers</p>
<div class="main">
<div class="box left">
<img src="/unsupported-browsers/browser-chrome.png">
<a href="https://www.google.com/chrome/" target="_blank" rel="noreferrer noopener">Get Chrome</a>
</div>
<div class="box right">
<img src="/unsupported-browsers/browser-firefox.png">
<a href="https://www.mozilla.org/en-US/firefox/new/" target="_blank" rel="noreferrer noopener">Get Firefox</a>
</div>
</div>
</div>
<![endif]-->
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View File

@ -94,7 +94,6 @@ const addDevice = (state: State, device: Device): State => {
const newDevice: TrezorDevice = device.type === 'acquired' ? { const newDevice: TrezorDevice = device.type === 'acquired' ? {
...device, ...device,
// acquiring: false,
...extended, ...extended,
} : { } : {
...device, ...device,
@ -132,8 +131,8 @@ const addDevice = (state: State, device: Device): State => {
const changedDevices: Array<TrezorDevice> = affectedDevices.filter(d => d.features && device.features const changedDevices: Array<TrezorDevice> = affectedDevices.filter(d => d.features && device.features
&& d.features.passphrase_protection === device.features.passphrase_protection).map((d) => { && d.features.passphrase_protection === device.features.passphrase_protection).map((d) => {
const extended: Object = { connected: true, available: true }; const extended2: Object = { connected: true, available: true };
return mergeDevices(d, { ...device, ...extended }); return mergeDevices(d, { ...device, ...extended2 });
}); });
if (changedDevices.length !== affectedDevices.length) { if (changedDevices.length !== affectedDevices.length) {
changedDevices.push(newDevice); changedDevices.push(newDevice);
@ -154,7 +153,6 @@ const duplicate = (state: State, device: TrezorDevice): State => {
const newDevice: TrezorDevice = { const newDevice: TrezorDevice = {
...device, ...device,
// acquiring: false,
remember: false, remember: false,
state: null, state: null,
// instance, (instance is already part of device - added in modal) // instance, (instance is already part of device - added in modal)
@ -199,7 +197,7 @@ const authDevice = (state: State, device: TrezorDevice, deviceState: string): St
// Transform JSON form local storage into State // Transform JSON form local storage into State
const devicesFromStorage = (devices: Array<TrezorDevice>): State => devices.map((device: TrezorDevice) => { const devicesFromStorage = ($devices: Array<TrezorDevice>): State => $devices.map((device: TrezorDevice) => {
const extended = { const extended = {
connected: false, connected: false,
available: false, available: false,
@ -233,14 +231,14 @@ const disconnectDevice = (state: State, device: Device): State => {
const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1); const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1);
if (affectedDevices.length > 0) { if (affectedDevices.length > 0) {
const acquiredDevices = affectedDevices.filter(d => d.features && d.state).map((d) => { const acquiredDevices = affectedDevices.filter(d => d.features && d.state).map((d) => { // eslint-disable-line arrow-body-style
if (d.type === 'acquired') { return d.type === 'acquired' ? {
d.connected = false; ...d,
d.available = false; connected: false,
d.status = 'available'; available: false,
d.path = ''; status: 'available',
} path: '',
return d; } : d;
}); });
return otherDevices.concat(acquiredDevices); return otherDevices.concat(acquiredDevices);
} }
@ -249,12 +247,18 @@ const disconnectDevice = (state: State, device: Device): State => {
}; };
const onSelectedDevice = (state: State, device: ?TrezorDevice): State => { const onSelectedDevice = (state: State, device: ?TrezorDevice): State => {
if (device) { if (!device) return state;
const otherDevices: Array<TrezorDevice> = state.filter(d => d !== device); const otherDevices: Array<TrezorDevice> = state.filter(d => d !== device);
device.ts = new Date().getTime(); const extended = device.type === 'acquired' ? {
return otherDevices.concat([device]); ...device,
} ts: new Date().getTime(),
return state; } : {
...device,
features: null,
ts: new Date().getTime(),
};
return otherDevices.concat([extended]);
}; };
export default function devices(state: State = initialState, action: Action): State { export default function devices(state: State = initialState, action: Action): State {

View File

@ -65,7 +65,7 @@ export default function pending(state: State = initialState, action: Action): St
// return add(state, action.payload); // return add(state, action.payload);
case PENDING.TX_RESOLVED: case PENDING.TX_RESOLVED:
return remove(state, action.tx.id); return remove(state, action.tx.id);
case PENDING.TX_NOT_FOUND: case PENDING.TX_REJECTED:
return reject(state, action.tx.id); return reject(state, action.tx.id);
case PENDING.FROM_STORAGE: case PENDING.FROM_STORAGE:

View File

@ -11,12 +11,18 @@ import type {
} from 'flowtype'; } from 'flowtype';
export type State = { export type State = {
location?: string; location: string;
account: ?Account; account: ?Account;
network: ?Coin; network: ?Coin;
tokens: Array<Token>, tokens: Array<Token>,
pending: Array<PendingTx>, pending: Array<PendingTx>,
discovery: ?Discovery discovery: ?Discovery,
notification: ?{
type: string,
title: string,
message?: string,
},
shouldRender: boolean,
}; };
export const initialState: State = { export const initialState: State = {
@ -26,6 +32,8 @@ export const initialState: State = {
tokens: [], tokens: [],
pending: [], pending: [],
discovery: null, discovery: null,
notification: null,
shouldRender: false,
}; };
export default (state: State = initialState, action: Action): State => { export default (state: State = initialState, action: Action): State => {

View File

@ -10,9 +10,7 @@ export type SelectedDevice = {
} }
export type State = { export type State = {
// devices: Array<TrezorDevice>; initialized: boolean;
// selectedDevice: ?SelectedDevice;
discoveryComplete: boolean;
error: ?string; error: ?string;
transport: ?{ transport: ?{
type: string; type: string;
@ -26,54 +24,42 @@ export type State = {
// mobile: boolean; // mobile: boolean;
// } | {}; // } | {};
browserState: any; browserState: any;
acquiring: boolean; acquiringDevice: boolean;
} }
const initialState: State = { const initialState: State = {
// devices: [], initialized: false,
//selectedDevice: null,
discoveryComplete: false,
error: null, error: null,
transport: null, transport: null,
browserState: {}, browserState: {},
acquiring: false, acquiringDevice: false,
}; };
export default function connect(state: State = initialState, action: Action): State { export default function connect(state: State = initialState, action: Action): State {
switch (action.type) { switch (action.type) {
case UI.IFRAME_HANDSHAKE: // trezor-connect iframe didn't loaded properly
return {
...state,
browserState: action.payload.browser,
};
case CONNECT.START_ACQUIRING:
return {
...state,
acquiring: true,
};
case CONNECT.STOP_ACQUIRING:
return {
...state,
acquiring: false,
};
case CONNECT.INITIALIZATION_ERROR: case CONNECT.INITIALIZATION_ERROR:
return { return {
...state, ...state,
error: action.error, error: action.error,
}; };
// trezor-connect iframe loaded
case UI.IFRAME_HANDSHAKE:
return {
...state,
initialized: true,
browserState: action.payload.browser,
};
// trezor-connect (trezor-link) initialized
case TRANSPORT.START: case TRANSPORT.START:
return { return {
...state, ...state,
transport: action.payload, transport: action.payload,
error: null, error: null,
}; };
// trezor-connect (trezor-link)
// will be called continuously in interval until connection (bridge/webusb) will be established
case TRANSPORT.ERROR: case TRANSPORT.ERROR:
return { return {
...state, ...state,
@ -82,6 +68,17 @@ export default function connect(state: State = initialState, action: Action): St
transport: null, transport: null,
}; };
case CONNECT.START_ACQUIRING:
return {
...state,
acquiringDevice: true,
};
case CONNECT.STOP_ACQUIRING:
return {
...state,
acquiringDevice: false,
};
default: default:
return state; return state;

View File

@ -116,21 +116,58 @@ export const getWeb3 = (state: State): ?Web3Instance => {
return state.web3.find(w3 => w3.network === locationState.network); return state.web3.find(w3 => w3.network === locationState.network);
}; };
export const observeChanges = (prev: ?(Object | Array<any>), current: ?(Object | Array<any>), fields?: Array<string>): boolean => { export const observeChanges = (prev: ?Object, current: ?Object, filter?: {[k: string]: Array<string>}): boolean => {
if (prev !== current) { // 1. both objects are the same (solves simple types like string, boolean and number)
// 1. one of the objects is null/undefined if (prev === current) return false;
// 2. one of the objects is null/undefined
if (!prev || !current) return true; if (!prev || !current) return true;
// 2. object are Arrays and they have different length
if (Array.isArray(prev) && Array.isArray(current)) return prev.length !== current.length; const prevType = Object.prototype.toString.call(current);
// 3. no nested field to check const currentType = Object.prototype.toString.call(current);
if (!Array.isArray(fields)) return true; // 3. one of the objects has different type then other
// 4. validate nested field if (prevType !== currentType) return true;
if (prev instanceof Object && current instanceof Object) {
for (let i = 0; i < fields.length; i++) { if (currentType === '[object Array]') {
const key = fields[i]; // 4. Array length is different
if (prev[key] !== current[key]) return true; if (prev.length !== current.length) return true;
// observe array recursive
for (let i = 0; i < current.length; i++) {
if (observeChanges(prev[i], current[i], filter)) return true;
}
} else if (currentType === '[object Object]') {
const prevKeys = Object.keys(prev);
const currentKeys = Object.keys(prev);
// 5. simple validation of keys length
if (prevKeys.length !== currentKeys.length) return true;
// 6. "prev" has keys which "current" doesn't have
const prevDifference = prevKeys.find(k => currentKeys.indexOf(k) < 0);
if (prevDifference) return true;
// 7. "current" has keys which "prev" doesn't have
const currentDifference = currentKeys.find(k => prevKeys.indexOf(k) < 0);
if (currentDifference) return true;
// 8. observe every key recursive
for (let i = 0; i < currentKeys.length; i++) {
const key = currentKeys[i];
if (filter && filter.hasOwnProperty(key) && prev[key] && current[key]) {
const prevFiltered = {};
const currentFiltered = {};
for (let i2 = 0; i2 < filter[key].length; i2++) {
const field = filter[key][i2];
prevFiltered[field] = prev[key][field];
currentFiltered[field] = current[key][field];
}
if (observeChanges(prevFiltered, currentFiltered)) return true;
} else if (observeChanges(prev[key], current[key])) {
return true;
} }
} }
} else if (prev !== current) {
// solve simple types like string, boolean and number
return true;
} }
return false; return false;
}; };

View File

@ -110,9 +110,12 @@ const LocalStorageService: Middleware = (api: MiddlewareAPI) => (next: Middlewar
case SEND.TX_COMPLETE: case SEND.TX_COMPLETE:
case PENDING.TX_RESOLVED: case PENDING.TX_RESOLVED:
case PENDING.TX_NOT_FOUND: case PENDING.TX_REJECTED:
save(api.dispatch, api.getState); save(api.dispatch, api.getState);
break; break;
default:
return action;
} }
return action; return action;

View File

@ -5,7 +5,6 @@ import * as WALLET from 'actions/constants/wallet';
import * as CONNECT from 'actions/constants/TrezorConnect'; 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 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';
@ -22,7 +21,7 @@ import type {
/** /**
* Middleware * Middleware
*/ */
const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => async (action: Action): Promise<Action> => {
const prevState = api.getState(); const prevState = api.getState();
// Application live cycle starts HERE! // Application live cycle starts HERE!
@ -52,9 +51,6 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
if (action.device) { if (action.device) {
// try to authorize device // try to authorize device
api.dispatch(TrezorConnectActions.getSelectedDeviceState()); api.dispatch(TrezorConnectActions.getSelectedDeviceState());
} else {
// try select different device
api.dispatch(RouterActions.selectFirstAvailableDevice());
} }
break; break;
case DEVICE.CONNECT: case DEVICE.CONNECT:
@ -92,14 +88,14 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state)); api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
} }
// observe send form props changes // observe common values in WallerReducer
if (!await api.dispatch(WalletActions.observe(prevState, action))) {
// if "selectedDevice" didn't change observe common values in SelectedAccountReducer
if (!await api.dispatch(SelectedAccountActions.observe(prevState, action))) {
// if "selectedAccount" didn't change observe send form props changes
api.dispatch(SendFormActionActions.observe(prevState, action)); api.dispatch(SendFormActionActions.observe(prevState, action));
}
// update common values in WallerReducer }
api.dispatch(WalletActions.updateSelectedValues(prevState, action));
// update common values in SelectedAccountReducer
api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action));
return action; return action;
}; };

View File

@ -41,6 +41,10 @@ const baseStyles = () => injectGlobal`
outline: 0; outline: 0;
} }
#root {
height: 100%;
}
/* /*
custom Roboto with Zero without the thing inside, so it's more readable as number custom Roboto with Zero without the thing inside, so it's more readable as number
since 0 doesn't look too similar to 8 since 0 doesn't look too similar to 8

31
src/utils/notification.js Normal file
View File

@ -0,0 +1,31 @@
import colors from 'config/colors';
import icons from 'config/icons';
const getColor = (type) => {
let color;
switch (type) {
case 'info':
color = colors.INFO_PRIMARY;
break;
case 'error':
color = colors.ERROR_PRIMARY;
break;
case 'warning':
color = colors.WARNING_PRIMARY;
break;
case 'success':
color = colors.SUCCESS_PRIMARY;
break;
default:
color = null;
}
return color;
};
const getIcon = type => icons[type.toUpperCase()];
export {
getColor,
getIcon,
};

View File

@ -3,9 +3,19 @@
export const getViewportHeight = (): number => ( export const getViewportHeight = (): number => (
// $FlowIssue // $FlowIssue
window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight // $FlowIssue document.documentElement.clientHeight || document.body.clientHeight // $FlowIssue
); );
export const getScrollX = (): number => {
if (window.pageXOffset !== undefined) {
return window.pageXOffset;
} if (window.scrollLeft !== undefined) {
return window.scrollLeft;
}
// $FlowIssue
return (document.documentElement || document.body.parentNode || document.body).scrollLeft; // $FlowIssue
};
export const getScrollY = (): number => { export const getScrollY = (): number => {
if (window.pageYOffset !== undefined) { if (window.pageYOffset !== undefined) {
return window.pageYOffset; return window.pageYOffset;

View File

@ -0,0 +1,23 @@
/* @flow */
import React from 'react';
import styled from 'styled-components';
import { Notification } from 'components/Notification';
const Wrapper = styled.div`
min-width: 720px;
width: 100%;
`;
const InitializationError = (props: { error: ?string }) => (
<Wrapper>
<Notification
title="Initialization error"
message={props.error || ''}
type="error"
cancelable={false}
/>
</Wrapper>
);
export default InitializationError;

View File

@ -14,6 +14,7 @@ import { H2 } from 'components/Heading';
import { isWebUSB } from 'utils/device'; import { isWebUSB } from 'utils/device';
import { FONT_SIZE } from 'config/variables'; import { FONT_SIZE } from 'config/variables';
import InitializationError from './components/InitializationError';
import BrowserNotSupported from './components/BrowserNotSupported'; import BrowserNotSupported from './components/BrowserNotSupported';
import ConnectDevice from './components/ConnectDevice'; import ConnectDevice from './components/ConnectDevice';
import InstallBridge from './components/InstallBridge'; import InstallBridge from './components/InstallBridge';
@ -21,7 +22,7 @@ import InstallBridge from './components/InstallBridge';
import type { Props } from './Container'; import type { Props } from './Container';
const LandingWrapper = styled.div` const LandingWrapper = styled.div`
min-height: 100vh; min-height: 100%;
min-width: 720px; min-width: 720px;
display: flex; display: flex;
@ -77,13 +78,13 @@ export default (props: Props) => {
const bridgeRoute: boolean = props.router.location.state.hasOwnProperty('bridge'); const bridgeRoute: boolean = props.router.location.state.hasOwnProperty('bridge');
const deviceLabel = props.wallet.disconnectRequest ? props.wallet.disconnectRequest.label : ''; const deviceLabel = props.wallet.disconnectRequest ? props.wallet.disconnectRequest.label : '';
const shouldShowInstallBridge = connectError || bridgeRoute; const shouldShowInitializationError = connectError && !props.connect.initialized;
const shouldShowInstallBridge = props.connect.initialized && (connectError || bridgeRoute);
const shouldShowConnectDevice = props.wallet.ready && devices.length < 1; const shouldShowConnectDevice = props.wallet.ready && devices.length < 1;
const shouldShowDisconnectDevice = !!props.wallet.disconnectRequest; const shouldShowDisconnectDevice = !!props.wallet.disconnectRequest;
const shouldShowUnsupportedBrowser = browserState.supported === false; const shouldShowUnsupportedBrowser = browserState.supported === false;
const isLoading = !shouldShowInstallBridge && !shouldShowConnectDevice && !shouldShowUnsupportedBrowser && !localStorageError; const isLoading = !shouldShowInitializationError && !shouldShowInstallBridge && !shouldShowConnectDevice && !shouldShowUnsupportedBrowser && !localStorageError;
return ( return (
<LandingWrapper> <LandingWrapper>
{isLoading && <LandingLoader text="Loading" size={100} />} {isLoading && <LandingLoader text="Loading" size={100} />}
@ -98,10 +99,9 @@ export default (props: Props) => {
/> />
)} )}
<Notifications /> <Notifications />
{shouldShowInitializationError && <InitializationError error={connectError} />}
<Log /> <Log />
<LandingContent> <LandingContent>
{shouldShowUnsupportedBrowser && <BrowserNotSupported />} {shouldShowUnsupportedBrowser && <BrowserNotSupported />}
{shouldShowInstallBridge && <InstallBridge browserState={browserState} />} {shouldShowInstallBridge && <InstallBridge browserState={browserState} />}

View File

@ -17,7 +17,7 @@ type OwnProps = {
} }
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State/* , own: OwnProps */): StateProps => ({ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
connect: state.connect, connect: state.connect,
accounts: state.accounts, accounts: state.accounts,
router: state.router, router: state.router,
@ -31,7 +31,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 => ({
//onAccountSelect: bindActionCreators(AccountActions.onAccountSelect, dispatch),
toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch), toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch),
addAccount: bindActionCreators(TrezorConnectActions.addAccount, dispatch), addAccount: bindActionCreators(TrezorConnectActions.addAccount, dispatch),
acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch),

View File

@ -1,151 +1,74 @@
/* @flow */ /* @flow */
// https://github.com/KyleAMathews/react-headroom/blob/master/src/shouldUpdate.js
import * as React from 'react'; import * as React from 'react';
import raf from 'raf'; import raf from 'raf';
import { getViewportHeight, getScrollY } from 'utils/windowUtils'; import { getViewportHeight, getScrollX, getScrollY } from 'utils/windowUtils';
import styled from 'styled-components'; import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
type Props = { type Props = {
location: string,
deviceSelection: boolean,
children?: React.Node, children?: React.Node,
} }
const AsideWrapper = styled.aside` type State = {
prevScrollY: number;
asideMinHeight: number,
wrapperTopOffset: number,
wrapperLeftOffset: number,
wrapperBottomPadding: number,
footerFixed: boolean,
}
const AsideWrapper = styled.aside.attrs({
style: ({ minHeight }) => ({
minHeight,
}),
})`
position: relative; position: relative;
top: 0; top: 0px;
width: 320px; width: 320px;
min-width: 320px; min-width: 320px;
overflow: hidden; overflow: hidden;
background: ${colors.MAIN}; background: ${colors.MAIN};
border-right: 1px solid ${colors.BACKGROUND}; border-right: 1px solid ${colors.DIVIDER};
.fixed {
border-right: 1px solid ${colors.BACKGROUND};
position: fixed;
}
.fixed-bottom {
padding-bottom: 60px;
.sticky-bottom {
position: fixed;
bottom: 0;
background: ${colors.MAIN};
}
}
`; `;
const StickyContainerWrapper = styled.div` const StickyContainerWrapper = styled.div.attrs({
position: relative; style: ({ top, left, paddingBottom }) => ({
top: 0; top,
left,
paddingBottom,
}),
})`
position: fixed;
border-right: 1px solid ${colors.DIVIDER};
width: 320px; width: 320px;
overflow: hidden; overflow: hidden;
`; `;
export default class StickyContainer extends React.PureComponent<Props> { export default class StickyContainer extends React.PureComponent<Props, State> {
// Class variables. constructor() {
currentScrollY: number = 0; super();
this.state = {
lastKnownScrollY: number = 0; prevScrollY: 0,
asideMinHeight: 0,
topOffset: number = 0; wrapperTopOffset: 0,
wrapperLeftOffset: 0,
firstRender: boolean = false; wrapperBottomPadding: 0,
footerFixed: false,
framePending: boolean = false; };
stickToBottom: boolean = false;
top: number = 0;
aside: ?HTMLElement;
wrapper: ?HTMLElement;
subscribers = [];
// handleResize = (event: Event) => {
// }
shouldUpdate = () => {
const { wrapper, aside }: {wrapper: ?HTMLElement, aside: ?HTMLElement} = this;
if (!wrapper || !aside) return;
const bottom: ?HTMLElement = wrapper.querySelector('.sticky-bottom');
if (!bottom) return;
const viewportHeight: number = getViewportHeight();
const bottomBounds = bottom.getBoundingClientRect();
const asideBounds = aside.getBoundingClientRect();
const wrapperBounds = wrapper.getBoundingClientRect();
const scrollDirection = this.currentScrollY >= this.lastKnownScrollY ? 'down' : 'up';
const distanceScrolled = Math.abs(this.currentScrollY - this.lastKnownScrollY);
if (asideBounds.top < 0) {
wrapper.classList.add('fixed');
let maxTop: number = 1;
if (wrapperBounds.height > viewportHeight) {
const bottomOutOfBounds: boolean = (bottomBounds.bottom <= viewportHeight && scrollDirection === 'down');
const topOutOfBounds: boolean = (wrapperBounds.top > 0 && scrollDirection === 'up');
if (!bottomOutOfBounds && !topOutOfBounds) {
this.topOffset += scrollDirection === 'down' ? -distanceScrolled : distanceScrolled;
}
maxTop = viewportHeight - wrapperBounds.height;
}
if (this.topOffset > 0) this.topOffset = 0;
if (maxTop < 0 && this.topOffset < maxTop) this.topOffset = maxTop;
wrapper.style.top = `${this.topOffset}px`;
} else {
wrapper.classList.remove('fixed');
wrapper.style.top = '0px';
this.topOffset = 0;
}
if (wrapperBounds.height > viewportHeight) {
wrapper.classList.remove('fixed-bottom');
} else if (wrapper.classList.contains('fixed-bottom')) {
if (bottomBounds.top < wrapperBounds.bottom - bottomBounds.height) {
wrapper.classList.remove('fixed-bottom');
}
} else if (bottomBounds.bottom < viewportHeight) {
wrapper.classList.add('fixed-bottom');
}
aside.style.minHeight = `${wrapperBounds.height}px`;
}
update = () => {
this.currentScrollY = getScrollY();
this.shouldUpdate();
this.framePending = false;
this.lastKnownScrollY = this.currentScrollY;
} }
componentDidMount() { componentDidMount() {
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleScroll); window.addEventListener('resize', this.handleScroll);
raf(this.update); this.update();
} }
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props, newState: State) {
if (this.props.location !== prevProps.location && this.aside) { // recalculate view only if props was changed
const asideBounds = this.aside.getBoundingClientRect(); // ignore when state is changed
if (asideBounds.top < 0) { if (this.state === newState) raf(this.update);
window.scrollTo(0, getScrollY() + asideBounds.top);
this.topOffset = 0;
}
raf(this.update);
} else if (this.props.deviceSelection !== prevProps.deviceSelection) {
raf(this.update);
} else if (!this.firstRender) {
raf(this.update);
this.firstRender = true;
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -153,26 +76,106 @@ export default class StickyContainer extends React.PureComponent<Props> {
window.removeEventListener('resize', this.handleScroll); window.removeEventListener('resize', this.handleScroll);
} }
handleScroll = (/* event: ?Event */) => { update = () => {
if (!this.framePending) { this.recalculatePosition();
this.framePending = true;
raf(this.update);
} }
handleScroll = () => raf(this.update);
asideRefCallback = (element: ?HTMLElement) => {
this.aside = element;
}
wrapperRefCallback = (element: ?HTMLElement) => {
this.wrapper = element;
}
footerRefCallback = (element: ?HTMLElement) => {
this.footer = element;
}
aside: ?HTMLElement;
wrapper: ?HTMLElement;
footer: ?HTMLElement;
recalculatePosition() {
const { aside, wrapper, footer } = this;
if (!aside || !wrapper || !footer) return;
const viewportHeight = getViewportHeight();
const asideBounds = aside.getBoundingClientRect();
const wrapperBounds = wrapper.getBoundingClientRect();
const footerBounds = footer.getBoundingClientRect();
const isHeaderFixed = asideBounds.top < 0;
const isWrapperBiggerThanViewport = wrapperBounds.height > viewportHeight;
const state = { ...this.state };
const scrollX = getScrollX();
const scrollY = getScrollY();
if (isHeaderFixed) {
if (isWrapperBiggerThanViewport) {
const scrollDirection = scrollY >= state.prevScrollY ? 'down' : 'up';
const topOutOfBounds: boolean = (wrapperBounds.top > 0 && scrollDirection === 'up');
const bottomOutOfBounds: boolean = (footerBounds.bottom <= viewportHeight && scrollDirection === 'down');
if (!topOutOfBounds && !bottomOutOfBounds) {
// neither "top" or "bottom" was reached
// scroll whole wrapper
const distanceScrolled = Math.abs(scrollY - state.prevScrollY);
state.wrapperTopOffset += scrollDirection === 'down' ? -distanceScrolled : distanceScrolled;
}
}
// make sure that wrapper will not be over scrolled
if (state.wrapperTopOffset > 0) state.wrapperTopOffset = 0;
} else {
// update wrapper "top" to be same as "aside" element
state.wrapperTopOffset = asideBounds.top;
}
if (isWrapperBiggerThanViewport) {
state.footerFixed = false;
} else if (state.footerFixed) {
if (footerBounds.top < wrapperBounds.bottom - footerBounds.height) {
state.footerFixed = false;
}
} else if (footerBounds.bottom < viewportHeight) {
state.footerFixed = true;
}
state.prevScrollY = scrollY;
state.asideMinHeight = wrapperBounds.height;
state.wrapperBottomPadding = state.footerFixed ? footerBounds.height : 0;
// update wrapper "left" position
state.wrapperLeftOffset = scrollX > 0 ? -scrollX : asideBounds.left;
this.setState(state);
} }
render() { render() {
return ( return (
<AsideWrapper <AsideWrapper
innerRef={(node) => { this.aside = node; }} footerFixed={this.state.footerFixed}
minHeight={this.state.asideMinHeight}
innerRef={this.asideRefCallback}
onScroll={this.handleScroll} onScroll={this.handleScroll}
onTouchStart={this.handleScroll} onTouchStart={this.handleScroll}
onTouchMove={this.handleScroll} onTouchMove={this.handleScroll}
onTouchEnd={this.handleScroll} onTouchEnd={this.handleScroll}
> >
<StickyContainerWrapper <StickyContainerWrapper
innerRef={(node) => { this.wrapper = node; }} paddingBottom={this.state.wrapperBottomPadding}
top={this.state.wrapperTopOffset}
left={this.state.wrapperLeftOffset}
innerRef={this.wrapperRefCallback}
> >
{this.props.children} {React.Children.map(this.props.children, (child) => { // eslint-disable-line arrow-body-style
return child.key === 'sticky-footer' ? React.cloneElement(child, {
innerRef: this.footerRefCallback,
position: this.state.footerFixed ? 'fixed' : 'relative',
}) : child;
})}
</StickyContainerWrapper> </StickyContainerWrapper>
</AsideWrapper> </AsideWrapper>
); );

View File

@ -1,4 +1,6 @@
import React, { Component } from 'react'; /* @flow */
import * as React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
@ -10,6 +12,7 @@ import AccountMenu from './components/AccountMenu';
import CoinMenu from './components/CoinMenu'; import CoinMenu from './components/CoinMenu';
import DeviceMenu from './components/DeviceMenu'; import DeviceMenu from './components/DeviceMenu';
import StickyContainer from './components/StickyContainer'; import StickyContainer from './components/StickyContainer';
import type { Props } from './components/common';
const Header = styled(DeviceHeader)` const Header = styled(DeviceHeader)`
border-right: 1px solid ${colors.BACKGROUND}; border-right: 1px solid ${colors.BACKGROUND};
@ -25,8 +28,11 @@ const TransitionContentWrapper = styled.div`
vertical-align: top; vertical-align: top;
`; `;
const Footer = styled.div` const Footer = styled.div.attrs({
position: relative; style: ({ position }) => ({
position,
}),
})`
width: 320px; width: 320px;
bottom: 0; bottom: 0;
background: ${colors.MAIN}; background: ${colors.MAIN};
@ -60,6 +66,13 @@ const A = styled.a`
} }
`; `;
type TransitionMenuProps = {
animationType: ?string;
children?: React.Node;
}
// TransitionMenu needs to dispatch window.resize event
// in order to StickyContainer be recalculated
const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGroup> => ( const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGroup> => (
<TransitionGroupWrapper component="div" className="transition-container"> <TransitionGroupWrapper component="div" className="transition-container">
<CSSTransition <CSSTransition
@ -79,28 +92,27 @@ const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGro
</TransitionGroupWrapper> </TransitionGroupWrapper>
); );
class LeftNavigation extends Component { type State = {
constructor(props) { animationType: ?string;
shouldRenderDeviceSelection: boolean;
}
class LeftNavigation extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props); super(props);
const { location } = this.props.router;
const hasNetwork = location && location.state && location.state.network;
this.state = { this.state = {
animationType: null, animationType: hasNetwork ? 'slide-left' : null,
shouldRenderDeviceSelection: false, shouldRenderDeviceSelection: false,
}; };
} }
componentDidMount() { componentWillReceiveProps(nextProps: Props) {
this.setState({
animationType: null,
shouldRenderDeviceSelection: false,
});
}
componentWillReceiveProps(nextProps) {
const { deviceDropdownOpened } = nextProps; const { deviceDropdownOpened } = nextProps;
const { selectedDevice } = nextProps.wallet; const { selectedDevice } = nextProps.wallet;
const hasNetwork = nextProps.location.state && nextProps.location.state.network; const hasNetwork = nextProps.router.location.state && nextProps.router.location.state.network;
const hasFeatures = selectedDevice && selectedDevice.features; const deviceReady = selectedDevice && selectedDevice.features && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized;
const deviceReady = hasFeatures && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized;
if (deviceDropdownOpened) { if (deviceDropdownOpened) {
this.setState({ shouldRenderDeviceSelection: true }); this.setState({ shouldRenderDeviceSelection: true });
@ -119,10 +131,11 @@ class LeftNavigation extends Component {
shouldRenderAccounts() { shouldRenderAccounts() {
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
const { location } = this.props.router;
return selectedDevice return selectedDevice
&& this.props.location && location
&& this.props.location.state && location.state
&& this.props.location.state.network && location.state.network
&& !this.state.shouldRenderDeviceSelection && !this.state.shouldRenderDeviceSelection
&& this.state.animationType === 'slide-left'; && this.state.animationType === 'slide-left';
} }
@ -132,7 +145,7 @@ class LeftNavigation extends Component {
} }
shouldRenderCoins() { shouldRenderCoins() {
return !this.state.shouldRenderDeviceSelection && this.state.animationType === 'slide-right'; return !this.state.shouldRenderDeviceSelection && this.state.animationType !== 'slide-left';
} }
render() { render() {
@ -154,7 +167,7 @@ class LeftNavigation extends Component {
return ( return (
<StickyContainer <StickyContainer
location={this.props.location.pathname} location={this.props.router.location.pathname}
deviceSelection={this.props.deviceDropdownOpened} deviceSelection={this.props.deviceDropdownOpened}
> >
<Header <Header
@ -169,7 +182,7 @@ class LeftNavigation extends Component {
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />} {this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />}
{menu} {menu}
</Body> </Body>
<Footer className="sticky-bottom"> <Footer key="sticky-footer">
<Help> <Help>
<A <A
href="https://trezor.io/support/" href="https://trezor.io/support/"
@ -186,9 +199,18 @@ class LeftNavigation extends Component {
} }
LeftNavigation.propTypes = { LeftNavigation.propTypes = {
selectedDevice: PropTypes.object, connect: PropTypes.object,
wallet: PropTypes.object, accounts: PropTypes.array,
router: PropTypes.object,
deviceDropdownOpened: PropTypes.bool, deviceDropdownOpened: PropTypes.bool,
fiat: PropTypes.array,
localStorage: PropTypes.object,
discovery: PropTypes.array,
wallet: PropTypes.object,
devices: PropTypes.array,
pending: PropTypes.array,
toggleDeviceDropdown: PropTypes.func,
}; };

View File

@ -12,13 +12,15 @@ import type { State } from 'flowtype';
import Header from 'components/Header'; import Header from 'components/Header';
import Footer from 'components/Footer'; import Footer from 'components/Footer';
import ModalContainer from 'components/modals'; import ModalContainer from 'components/modals';
import Notifications from 'components/Notification'; import ContextNotifications from 'components/notifications/Context';
import Log from 'components/Log'; import Log from 'components/Log';
import LeftNavigation from './components/LeftNavigation/Container'; import LeftNavigation from './components/LeftNavigation/Container';
import TopNavigationAccount from './components/TopNavigationAccount'; import TopNavigationAccount from './components/TopNavigationAccount';
import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings'; import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings';
type WalletContainerProps = { type WalletContainerProps = {
wallet: $ElementType<State, 'wallet'>, wallet: $ElementType<State, 'wallet'>,
children?: React.Node children?: React.Node
@ -30,7 +32,7 @@ type WalletContainerProps = {
const AppWrapper = styled.div` const AppWrapper = styled.div`
position: relative; position: relative;
min-height: 100vh; min-height: 100%;
min-width: 720px; min-width: 720px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -89,7 +91,7 @@ const Wallet = (props: WalletContainerProps) => (
<Route path="/device/:device/network/:network/account/:account" component={TopNavigationAccount} /> <Route path="/device/:device/network/:network/account/:account" component={TopNavigationAccount} />
<Route path="/device/:device/device-settings" component={TopNavigationDeviceSettings} /> <Route path="/device/:device/device-settings" component={TopNavigationDeviceSettings} />
</Navigation> </Navigation>
<Notifications /> <ContextNotifications />
<Log /> <Log />
<Body> <Body>
{ props.children } { props.children }

View File

@ -13,6 +13,11 @@ import ICONS from 'config/icons';
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
// same variable also in "AccountSend/index.js"
const SmallScreenWidth = '850px';
const InputRow = styled.div` const InputRow = styled.div`
margin-bottom: 20px; margin-bottom: 20px;
`; `;
@ -38,17 +43,29 @@ const AdvancedSettingsWrapper = styled.div`
const GasInputRow = styled(InputRow)` const GasInputRow = styled(InputRow)`
width: 100%; width: 100%;
display: flex; display: flex;
@media screen and (max-width: ${SmallScreenWidth}) {
flex-direction: column;
}
`; `;
const GasInput = styled(Input)` const GasInput = styled(Input)`
min-height: 85px;
&:first-child { &:first-child {
padding-right: 20px; padding-right: 20px;
} }
@media screen and (max-width: ${SmallScreenWidth}) {
&:first-child {
padding-right: 0;
margin-bottom: 2px;
}
}
`; `;
const StyledTextarea = styled(Textarea)` const StyledTextarea = styled(Textarea)`
margin-bottom: 20px; margin-bottom: 20px;
height: 80px; min-height: 80px;
`; `;
const AdvancedSettingsSendButtonWrapper = styled.div` const AdvancedSettingsSendButtonWrapper = styled.div`
@ -79,6 +96,17 @@ const getGasPriceInputState = (gasPriceErrors: string, gasPriceWarnings: string)
return state; return state;
}; };
const getDataTextareaState = (dataError: string, dataWarning: string): string => {
let state = '';
if (dataWarning) {
state = 'warning';
}
if (dataError) {
state = 'error';
}
return state;
};
// stateless component // stateless component
const AdvancedForm = (props: Props) => { const AdvancedForm = (props: Props) => {
const { const {
@ -89,6 +117,7 @@ const AdvancedForm = (props: Props) => {
networkSymbol, networkSymbol,
currency, currency,
recommendedGasPrice, recommendedGasPrice,
calculatingGasLimit,
errors, errors,
warnings, warnings,
infos, infos,
@ -127,12 +156,13 @@ const AdvancedForm = (props: Props) => {
<Tooltip <Tooltip
content={( content={(
<React.Fragment> <React.Fragment>
Gas limit is the amount of gas to send with your transaction.<br /> Gas limit refers to the maximum amount of gas user is willing to spendon a particular transaction.{' '}
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> &amp; is paid to miners for including your TX in a block.<br /> <GreenSpan>Transaction fee = gas limit * gas price</GreenSpan>.{' '}Increasing the gas limit will not get the transaction confirmed sooner.
Increasing this number will not get your TX mined faster.<br /> Default value for sending {gasLimitTooltipCurrency} is <GreenSpan>{gasLimitTooltipValue}</GreenSpan>.
Default value for sending {gasLimitTooltipCurrency} is <GreenSpan>{gasLimitTooltipValue}</GreenSpan>
</React.Fragment> </React.Fragment>
)} )}
maxWidth={410}
readMoreLink="https://wiki.trezor.io/Ethereum_Wallet#Gas_limit"
placement="top" placement="top"
> >
<Icon <Icon
@ -143,7 +173,7 @@ const AdvancedForm = (props: Props) => {
</Tooltip> </Tooltip>
</InputLabelWrapper> </InputLabelWrapper>
)} )}
bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit} bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit || (calculatingGasLimit ? 'Calculating...' : '')}
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)}
@ -161,12 +191,13 @@ const AdvancedForm = (props: Props) => {
<Tooltip <Tooltip
content={( content={(
<React.Fragment> <React.Fragment>
Gas Price is the amount you pay per unit of gas.<br /> Gas price refers to the amount of ether you are willing to pay for every
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> &amp; is paid to miners for including your TX in a block.<br /> unit of gas, and is usually measured in Gwei. <GreenSpan>Transaction fee = gas limit * gas price</GreenSpan>. Increasing the gas price will get the transaction confirmed sooner but
Higher the gas price = faster transaction, but more expensive. Recommended is <GreenSpan>{recommendedGasPrice} GWEI.</GreenSpan><br /> makes it more expensive. The recommended gas price is <GreenSpan>{recommendedGasPrice} GWEI</GreenSpan>.
<Link href="https://myetherwallet.github.io/knowledge-base/gas/what-is-gas-ethereum.html" target="_blank" rel="noreferrer noopener" isGreen>Read more</Link>
</React.Fragment> </React.Fragment>
)} )}
maxWidth={400}
readMoreLink="https://wiki.trezor.io/Ethereum_Wallet#Gas_price"
placement="top" placement="top"
> >
<Icon <Icon
@ -203,8 +234,9 @@ const AdvancedForm = (props: Props) => {
</Tooltip> </Tooltip>
</InputLabelWrapper> </InputLabelWrapper>
)} )}
state={getDataTextareaState(errors.data, warnings.data)}
bottomText={errors.data || warnings.data || infos.data} bottomText={errors.data || warnings.data || infos.data}
disabled={networkSymbol !== currency} isDisabled={networkSymbol !== currency}
value={networkSymbol !== currency ? '' : data} value={networkSymbol !== currency ? '' : data}
onChange={event => onDataChange(event.target.value)} onChange={event => onDataChange(event.target.value)}
/> />

View File

@ -46,7 +46,7 @@ const Acquire = (props: Props) => {
export default connect( export default connect(
(state: State) => ({ (state: State) => ({
acquiring: state.connect.acquiring, acquiring: state.connect.acquiringDevice,
}), }),
(dispatch: Dispatch) => ({ (dispatch: Dispatch) => ({
acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch),

View File

@ -1,14 +1,8 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { Notification } from 'components/Notification'; import { Notification } from 'components/Notification';
import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import type { State, Dispatch } from 'flowtype';
const Wrapper = styled.div``; const Wrapper = styled.div``;
@ -23,11 +17,4 @@ const UnreadableDevice = () => (
</Wrapper> </Wrapper>
); );
export default connect( export default UnreadableDevice;
(state: State) => ({
acquiring: state.connect.acquiring,
}),
(dispatch: Dispatch) => ({
acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch),
}),
)(UnreadableDevice);

View File

@ -25,6 +25,7 @@ module.exports = {
path: BUILD, path: BUILD,
}, },
devServer: { devServer: {
// host: '0.0.0.0',
contentBase: [ contentBase: [
SRC, SRC,
PUBLIC, PUBLIC,

View File

@ -9816,15 +9816,14 @@ style-search@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
styled-components@^3.3.3: styled-components@^3.4.9:
version "3.3.3" version "3.4.9"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.3.3.tgz#09e702055ab11f7a8eab8229b1c0d0b855095686" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.4.9.tgz#519abeb351b37be5b7de6a15ff9e4efeb9d772da"
dependencies: dependencies:
buffer "^5.0.3" buffer "^5.0.3"
css-to-react-native "^2.0.3" css-to-react-native "^2.0.3"
fbjs "^0.8.16" fbjs "^0.8.16"
hoist-non-react-statics "^2.5.0" hoist-non-react-statics "^2.5.0"
is-plain-object "^2.0.1"
prop-types "^15.5.4" prop-types "^15.5.4"
react-is "^16.3.1" react-is "^16.3.1"
stylis "^3.5.0" stylis "^3.5.0"