mirror of
https://github.com/trezor/trezor-wallet
synced 2024-11-24 09:18:09 +00:00
merge
This commit is contained in:
commit
151101a61c
@ -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
|
||||||
|
@ -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`
|
||||||
|
|
||||||
|
@ -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
|
@ -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",
|
||||||
|
BIN
public/unsupported-browsers/browser-chrome.png
Normal file
BIN
public/unsupported-browsers/browser-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
public/unsupported-browsers/browser-firefox.png
Normal file
BIN
public/unsupported-browsers/browser-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
64
public/unsupported-browsers/style.css
Normal file
64
public/unsupported-browsers/style.css
Normal 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;
|
||||||
|
}
|
@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
};
|
@ -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');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -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;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -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) {
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
@ -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 {
|
||||||
|
@ -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';
|
@ -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};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
56
src/components/Notification/NotificationGroups/index.js
Normal file
56
src/components/Notification/NotificationGroups/index.js
Normal 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;
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 = {
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
0
src/components/notifications/App/Container.js
Normal file
0
src/components/notifications/App/Container.js
Normal file
0
src/components/notifications/App/index.js
Normal file
0
src/components/notifications/App/index.js
Normal 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;
|
||||||
|
};
|
@ -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}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
55
src/components/notifications/Context/index.js
Normal file
55
src/components/notifications/Context/index.js
Normal 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);
|
@ -32,4 +32,5 @@ export default {
|
|||||||
ERROR_SECONDARY: '#FFE9E9',
|
ERROR_SECONDARY: '#FFE9E9',
|
||||||
|
|
||||||
LABEL_COLOR: '#A9A9A9',
|
LABEL_COLOR: '#A9A9A9',
|
||||||
|
TOOLTIP_BACKGROUND: '#333333',
|
||||||
};
|
};
|
8
src/constants/notifications.js
Normal file
8
src/constants/notifications.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
PRIORITY: {
|
||||||
|
error: 0,
|
||||||
|
warning: 1,
|
||||||
|
info: 2,
|
||||||
|
success: 3,
|
||||||
|
},
|
||||||
|
};
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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:
|
||||||
|
@ -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 => {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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
31
src/utils/notification.js
Normal 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,
|
||||||
|
};
|
@ -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;
|
||||||
|
23
src/views/Landing/components/InitializationError/index.js
Normal file
23
src/views/Landing/components/InitializationError/index.js
Normal 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;
|
@ -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} />}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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> & 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> & 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)}
|
||||||
/>
|
/>
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
|
||||||
|
@ -25,6 +25,7 @@ module.exports = {
|
|||||||
path: BUILD,
|
path: BUILD,
|
||||||
},
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
|
// host: '0.0.0.0',
|
||||||
contentBase: [
|
contentBase: [
|
||||||
SRC,
|
SRC,
|
||||||
PUBLIC,
|
PUBLIC,
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user