lint all files

pull/420/head
Vladimir Volek 5 years ago
parent b13cbb1365
commit 749224dc66

@ -3,10 +3,10 @@
"eslint-config-airbnb", "eslint-config-airbnb",
"prettier", "prettier",
"prettier/babel", "prettier/babel",
"plugin:flowtype/recommended",
"prettier/flowtype", "prettier/flowtype",
"prettier/react", "prettier/react",
"plugin:jest/recommended", "plugin:jest/recommended",
"plugin:flowtype/recommended",
"plugin:jest/recommended" "plugin:jest/recommended"
], ],
"globals": { "globals": {

@ -21,7 +21,8 @@
"lint": "run-s lint:*", "lint": "run-s lint:*",
"lint:js": "npx eslint ./src ./webpack", "lint:js": "npx eslint ./src ./webpack",
"lint:css": "npx stylelint './src/**/*.js'", "lint:css": "npx stylelint './src/**/*.js'",
"lint:prettier": "pretty-quick", "lint:prettier": "npx pretty-quick",
"lint-fix": "npx eslint ./src ./webpack --fix",
"prettier:check": "eslint --print-config ./src | eslint-config-prettier-check", "prettier:check": "eslint --print-config ./src | eslint-config-prettier-check",
"test": "run-s test:*", "test": "run-s test:*",
"test:unit": "npx jest", "test:unit": "npx jest",
@ -142,4 +143,4 @@
"optionalDependencies": { "optionalDependencies": {
"fsevents": "1.2.7" "fsevents": "1.2.7"
} }
} }

@ -4,13 +4,15 @@ import * as ACCOUNT from 'actions/constants/account';
import type { Action } from 'flowtype'; import type { Action } from 'flowtype';
import type { Account, State } from 'reducers/AccountsReducer'; import type { Account, State } from 'reducers/AccountsReducer';
export type AccountAction = { export type AccountAction =
type: typeof ACCOUNT.FROM_STORAGE, | {
payload: State type: typeof ACCOUNT.FROM_STORAGE,
} | { payload: State,
type: typeof ACCOUNT.CREATE | typeof ACCOUNT.UPDATE, }
payload: Account, | {
}; type: typeof ACCOUNT.CREATE | typeof ACCOUNT.UPDATE,
payload: Account,
};
export const update = (account: Account): Action => ({ export const update = (account: Account): Action => ({
type: ACCOUNT.UPDATE, type: ACCOUNT.UPDATE,

@ -4,35 +4,35 @@ import * as BLOCKCHAIN from 'actions/constants/blockchain';
import * as EthereumBlockchainActions from 'actions/ethereum/BlockchainActions'; import * as EthereumBlockchainActions from 'actions/ethereum/BlockchainActions';
import * as RippleBlockchainActions from 'actions/ripple/BlockchainActions'; import * as RippleBlockchainActions from 'actions/ripple/BlockchainActions';
import type { import type { Dispatch, GetState, PromiseAction, BlockchainFeeLevel } from 'flowtype';
Dispatch,
GetState,
PromiseAction,
BlockchainFeeLevel,
} from 'flowtype';
import type { BlockchainBlock, BlockchainNotification, BlockchainError } from 'trezor-connect'; import type { BlockchainBlock, BlockchainNotification, BlockchainError } from 'trezor-connect';
export type BlockchainAction =
export type BlockchainAction = { | {
type: typeof BLOCKCHAIN.READY, type: typeof BLOCKCHAIN.READY,
} | { }
type: typeof BLOCKCHAIN.UPDATE_FEE, | {
shortcut: string, type: typeof BLOCKCHAIN.UPDATE_FEE,
feeLevels: Array<BlockchainFeeLevel>, shortcut: string,
} | { feeLevels: Array<BlockchainFeeLevel>,
type: typeof BLOCKCHAIN.START_SUBSCRIBE, }
shortcut: string, | {
} type: typeof BLOCKCHAIN.START_SUBSCRIBE,
shortcut: string,
};
// Conditionally subscribe to blockchain backend // Conditionally subscribe to blockchain backend
// called after TrezorConnect.init successfully emits TRANSPORT.START event // called after TrezorConnect.init successfully emits TRANSPORT.START event
// checks if there are discovery processes loaded from LocalStorage // checks if there are discovery processes loaded from LocalStorage
// if so starts subscription to proper networks // if so starts subscription to proper networks
export const init = (): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const init = (): PromiseAction<void> => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
if (getState().discovery.length > 0) { if (getState().discovery.length > 0) {
// get unique networks // get unique networks
const networks: Array<string> = []; const networks: Array<string> = [];
getState().discovery.forEach((discovery) => { getState().discovery.forEach(discovery => {
if (networks.indexOf(discovery.network) < 0) { if (networks.indexOf(discovery.network) < 0) {
networks.push(discovery.network); networks.push(discovery.network);
} }
@ -50,7 +50,10 @@ export const init = (): PromiseAction<void> => async (dispatch: Dispatch, getSta
}); });
}; };
export const subscribe = (networkName: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const subscribe = (networkName: string): PromiseAction<void> => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === networkName); const network = config.networks.find(c => c.shortcut === networkName);
if (!network) return; if (!network) return;
@ -67,11 +70,14 @@ export const subscribe = (networkName: string): PromiseAction<void> => async (di
case 'ripple': case 'ripple':
await dispatch(RippleBlockchainActions.subscribe(networkName)); await dispatch(RippleBlockchainActions.subscribe(networkName));
break; break;
default: break; default:
break;
} }
}; };
export const onBlockMined = (payload: $ElementType<BlockchainBlock, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onBlockMined = (
payload: $ElementType<BlockchainBlock, 'payload'>
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const shortcut = payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
if (getState().router.location.state.network !== shortcut) return; if (getState().router.location.state.network !== shortcut) return;
@ -86,11 +92,14 @@ export const onBlockMined = (payload: $ElementType<BlockchainBlock, 'payload'>):
case 'ripple': case 'ripple':
await dispatch(RippleBlockchainActions.onBlockMined(shortcut)); await dispatch(RippleBlockchainActions.onBlockMined(shortcut));
break; break;
default: break; default:
break;
} }
}; };
export const onNotification = (payload: $ElementType<BlockchainNotification, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onNotification = (
payload: $ElementType<BlockchainNotification, 'payload'>
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const shortcut = payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === shortcut); const network = config.networks.find(c => c.shortcut === shortcut);
@ -104,13 +113,16 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
case 'ripple': case 'ripple':
await dispatch(RippleBlockchainActions.onNotification(payload)); await dispatch(RippleBlockchainActions.onNotification(payload));
break; break;
default: break; default:
break;
} }
}; };
// Handle BLOCKCHAIN.ERROR event from TrezorConnect // Handle BLOCKCHAIN.ERROR event from TrezorConnect
// disconnect and remove Web3 websocket instance if exists // disconnect and remove Web3 websocket instance if exists
export const onError = (payload: $ElementType<BlockchainError, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onError = (
payload: $ElementType<BlockchainError, 'payload'>
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const shortcut = payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === shortcut); const network = config.networks.find(c => c.shortcut === shortcut);
@ -124,6 +136,7 @@ export const onError = (payload: $ElementType<BlockchainError, 'payload'>): Prom
// this error is handled in BlockchainReducer // this error is handled in BlockchainReducer
// await dispatch(RippleBlockchainActions.onBlockMined(shortcut)); // await dispatch(RippleBlockchainActions.onBlockMined(shortcut));
break; break;
default: break; default:
break;
} }
}; };

@ -20,63 +20,85 @@ import * as BlockchainActions from './BlockchainActions';
import * as EthereumDiscoveryActions from './ethereum/DiscoveryActions'; import * as EthereumDiscoveryActions from './ethereum/DiscoveryActions';
import * as RippleDiscoveryActions from './ripple/DiscoveryActions'; import * as RippleDiscoveryActions from './ripple/DiscoveryActions';
export type DiscoveryStartAction = EthereumDiscoveryActions.DiscoveryStartAction | RippleDiscoveryActions.DiscoveryStartAction; export type DiscoveryStartAction =
| EthereumDiscoveryActions.DiscoveryStartAction
| RippleDiscoveryActions.DiscoveryStartAction;
export type DiscoveryWaitingAction = { export type DiscoveryWaitingAction = {
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN | typeof DISCOVERY.FIRMWARE_NOT_SUPPORTED | typeof DISCOVERY.FIRMWARE_OUTDATED, type:
| typeof DISCOVERY.WAITING_FOR_DEVICE
| typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN
| typeof DISCOVERY.FIRMWARE_NOT_SUPPORTED
| typeof DISCOVERY.FIRMWARE_OUTDATED,
device: TrezorDevice, device: TrezorDevice,
network: string, network: string,
} };
export type DiscoveryCompleteAction = { export type DiscoveryCompleteAction = {
type: typeof DISCOVERY.COMPLETE, type: typeof DISCOVERY.COMPLETE,
device: TrezorDevice, device: TrezorDevice,
network: string, network: string,
} };
export type DiscoveryAction = { export type DiscoveryAction =
type: typeof DISCOVERY.FROM_STORAGE, | {
payload: State type: typeof DISCOVERY.FROM_STORAGE,
} | { payload: State,
type: typeof DISCOVERY.STOP, }
device: TrezorDevice | {
} | DiscoveryStartAction type: typeof DISCOVERY.STOP,
| DiscoveryWaitingAction device: TrezorDevice,
| DiscoveryCompleteAction; }
| DiscoveryStartAction
| DiscoveryWaitingAction
| DiscoveryCompleteAction;
// There are multiple async methods during discovery process (trezor-connect and blockchain actions) // There are multiple async methods during discovery process (trezor-connect and blockchain actions)
// This method will check after each of async action if process was interrupted (for example by network change or device disconnect) // This method will check after each of async action if process was interrupted (for example by network change or device disconnect)
const isProcessInterrupted = (process?: Discovery): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => { const isProcessInterrupted = (process?: Discovery): PayloadAction<boolean> => (
dispatch: Dispatch,
getState: GetState
): boolean => {
if (!process) { if (!process) {
return false; return false;
} }
const { deviceState, network } = process; const { deviceState, network } = process;
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === deviceState && d.network === network); const discoveryProcess: ?Discovery = getState().discovery.find(
d => d.deviceState === deviceState && d.network === network
);
if (!discoveryProcess) return false; if (!discoveryProcess) return false;
return discoveryProcess.interrupted; return discoveryProcess.interrupted;
}; };
// Private action // Private action
// Called from "this.begin", "this.restore", "this.addAccount" // Called from "this.begin", "this.restore", "this.addAccount"
const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (!selected) { if (!selected) {
// TODO: throw error // TODO: throw error
console.error('Start discovery: no selected device', device); console.error('Start discovery: no selected device', device);
return; return;
} if (selected.path !== device.path) { }
if (selected.path !== device.path) {
console.error('Start discovery: requested device is not selected', device, selected); console.error('Start discovery: requested device is not selected', device, selected);
return; return;
} if (!selected.state) { }
if (!selected.state) {
console.warn("Start discovery: Selected device wasn't authenticated yet..."); console.warn("Start discovery: Selected device wasn't authenticated yet...");
return; return;
} if (selected.connected && !selected.available) { }
if (selected.connected && !selected.available) {
console.warn('Start discovery: Selected device is unavailable...'); console.warn('Start discovery: Selected device is unavailable...');
return; return;
} }
const { discovery } = getState(); const { discovery } = getState();
const discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network); const discoveryProcess: ?Discovery = discovery.find(
d => d.deviceState === device.state && d.network === network
);
if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) { if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) {
dispatch({ dispatch({
@ -105,7 +127,11 @@ const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean)
device, device,
network, network,
}); });
} else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice || discoveryProcess.waitingForBlockchain) { } else if (
discoveryProcess.interrupted ||
discoveryProcess.waitingForDevice ||
discoveryProcess.waitingForBlockchain
) {
// discovery cycle was interrupted // discovery cycle was interrupted
// start from beginning // start from beginning
dispatch(begin(device, network)); dispatch(begin(device, network));
@ -117,7 +143,10 @@ const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean)
// first iteration // first iteration
// generate public key for this account // generate public key for this account
// start discovery process // start discovery process
const begin = (device: TrezorDevice, networkName: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { const begin = (device: TrezorDevice, networkName: string): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === networkName); const network = config.networks.find(c => c.shortcut === networkName);
if (!network) return; if (!network) return;
@ -165,7 +194,9 @@ const begin = (device: TrezorDevice, networkName: string): AsyncAction => async
// check for interruption // check for interruption
// corner case: DISCOVERY.START wasn't called yet, but Discovery exists in reducer created by DISCOVERY.WAITING_FOR_DEVICE action // corner case: DISCOVERY.START wasn't called yet, but Discovery exists in reducer created by DISCOVERY.WAITING_FOR_DEVICE action
// this is why we need to get process instance directly from reducer // this is why we need to get process instance directly from reducer
const discoveryProcess = getState().discovery.find(d => d.deviceState === device.state && d.network === network); const discoveryProcess = getState().discovery.find(
d => d.deviceState === device.state && d.network === network
);
if (dispatch(isProcessInterrupted(discoveryProcess))) return; if (dispatch(isProcessInterrupted(discoveryProcess))) return;
// send data to reducer // send data to reducer
@ -175,7 +206,10 @@ const begin = (device: TrezorDevice, networkName: string): AsyncAction => async
dispatch(start(device, networkName)); dispatch(start(device, networkName));
}; };
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network); const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
if (!network) return; if (!network) return;
@ -185,13 +219,19 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
try { try {
switch (network.type) { switch (network.type) {
case 'ethereum': case 'ethereum':
account = await dispatch(EthereumDiscoveryActions.discoverAccount(device, discoveryProcess)); account = await dispatch(
EthereumDiscoveryActions.discoverAccount(device, discoveryProcess)
);
break; break;
case 'ripple': case 'ripple':
account = await dispatch(RippleDiscoveryActions.discoverAccount(device, discoveryProcess)); account = await dispatch(
RippleDiscoveryActions.discoverAccount(device, discoveryProcess)
);
break; break;
default: default:
throw new Error(`DiscoveryActions.discoverAccount: Unknown network type: ${network.type}`); throw new Error(
`DiscoveryActions.discoverAccount: Unknown network type: ${network.type}`
);
} }
} catch (error) { } catch (error) {
// handle not supported firmware error // handle not supported firmware error
@ -242,7 +282,11 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
if (dispatch(isProcessInterrupted(discoveryProcess))) return; if (dispatch(isProcessInterrupted(discoveryProcess))) return;
const accountIsEmpty = account.empty; const accountIsEmpty = account.empty;
if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && accountIndex === 0)) { if (
!accountIsEmpty ||
(accountIsEmpty && completed) ||
(accountIsEmpty && accountIndex === 0)
) {
dispatch({ dispatch({
type: ACCOUNT.CREATE, type: ACCOUNT.CREATE,
payload: account, payload: account,
@ -256,7 +300,9 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
} }
}; };
const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise<void> => { const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (
dispatch: Dispatch
): Promise<void> => {
await TrezorConnect.getFeatures({ await TrezorConnect.getFeatures({
device: { device: {
path: device.path, path: device.path,
@ -279,7 +325,9 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction
}); });
}; };
export const reconnect = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => { export const reconnect = (network: string): PromiseAction<void> => async (
dispatch: Dispatch
): Promise<void> => {
await dispatch(BlockchainActions.subscribe(network)); await dispatch(BlockchainActions.subscribe(network));
dispatch(restore()); dispatch(restore());
}; };
@ -296,10 +344,16 @@ export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetStat
if (!selected) return; if (!selected) return;
// find discovery process for requested network // find discovery process for requested network
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && d.network === urlParams.network); const discoveryProcess: ?Discovery = getState().discovery.find(
d => d.deviceState === selected.state && d.network === urlParams.network
);
// if there was no process befor OR process was interrupted/waiting // if there was no process befor OR process was interrupted/waiting
const shouldStart = !discoveryProcess || (discoveryProcess.interrupted || discoveryProcess.waitingForDevice || discoveryProcess.waitingForBlockchain); const shouldStart =
!discoveryProcess ||
(discoveryProcess.interrupted ||
discoveryProcess.waitingForDevice ||
discoveryProcess.waitingForBlockchain);
if (shouldStart) { if (shouldStart) {
dispatch(start(selected, urlParams.network)); dispatch(start(selected, urlParams.network));
} }
@ -310,7 +364,9 @@ export const stop = (): ThunkAction => (dispatch: Dispatch, getState: GetState):
if (!device) return; if (!device) return;
// get all uncompleted discovery processes which assigned to selected device // get all uncompleted discovery processes which assigned to selected device
const discoveryProcesses = getState().discovery.filter(d => d.deviceState === device.state && !d.completed); const discoveryProcesses = getState().discovery.filter(
d => d.deviceState === device.state && !d.completed
);
if (discoveryProcesses.length > 0) { if (discoveryProcesses.length > 0) {
dispatch({ dispatch({
type: DISCOVERY.STOP, type: DISCOVERY.STOP,

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import * as TOKEN from 'actions/constants/token'; import * as TOKEN from 'actions/constants/token';
@ -32,18 +31,21 @@ import type { Config, Network, TokensCollection } from 'reducers/LocalStorageRed
import Erc20AbiJSON from 'public/data/ERC20Abi.json'; import Erc20AbiJSON from 'public/data/ERC20Abi.json';
import AppConfigJSON from 'public/data/appConfig.json'; import AppConfigJSON from 'public/data/appConfig.json';
export type StorageAction = { export type StorageAction =
type: typeof STORAGE.READY, | {
config: Config, type: typeof STORAGE.READY,
tokens: TokensCollection, config: Config,
ERC20Abi: Array<TokensCollection> tokens: TokensCollection,
} | { ERC20Abi: Array<TokensCollection>,
type: typeof STORAGE.SAVE, }
network: string, | {
} | { type: typeof STORAGE.SAVE,
type: typeof STORAGE.ERROR, network: string,
error: string, }
}; | {
type: typeof STORAGE.ERROR,
error: string,
};
const TYPE: 'local' = 'local'; const TYPE: 'local' = 'local';
const { STORAGE_PATH } = storageUtils; const { STORAGE_PATH } = storageUtils;
@ -60,16 +62,39 @@ const KEY_LANGUAGE: string = `${STORAGE_PATH}language`;
// or // or
// https://www.npmjs.com/package/redux-react-session // https://www.npmjs.com/package/redux-react-session
const findAccounts = (devices: Array<TrezorDevice>, accounts: Array<Account>): Array<Account> => devices.reduce((arr, dev) => arr.concat(accounts.filter(a => a.deviceState === dev.state)), []); const findAccounts = (devices: Array<TrezorDevice>, accounts: Array<Account>): Array<Account> =>
devices.reduce((arr, dev) => arr.concat(accounts.filter(a => a.deviceState === dev.state)), []);
const findTokens = (accounts: Array<Account>, tokens: Array<Token>): Array<Token> => accounts.reduce((arr, account) => arr.concat(getAccountTokens(tokens, account)), []);
const findTokens = (accounts: Array<Account>, tokens: Array<Token>): Array<Token> =>
const findDiscovery = (devices: Array<TrezorDevice>, discovery: Array<Discovery>): Array<Discovery> => devices.reduce((arr, dev) => arr.concat(discovery.filter(d => d.deviceState === dev.state && d.completed)), []); accounts.reduce((arr, account) => arr.concat(getAccountTokens(tokens, account)), []);
const findPendingTxs = (accounts: Array<Account>, pending: Array<Transaction>): Array<Transaction> => accounts.reduce((result, account) => result.concat(pending.filter(p => p.descriptor === account.descriptor && p.network === account.network)), []); const findDiscovery = (
devices: Array<TrezorDevice>,
discovery: Array<Discovery>
): Array<Discovery> =>
devices.reduce(
(arr, dev) => arr.concat(discovery.filter(d => d.deviceState === dev.state && d.completed)),
[]
);
const findPendingTxs = (
accounts: Array<Account>,
pending: Array<Transaction>
): Array<Transaction> =>
accounts.reduce(
(result, account) =>
result.concat(
pending.filter(
p => p.descriptor === account.descriptor && p.network === account.network
)
),
[]
);
export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const devices: Array<TrezorDevice> = getState().devices.filter(d => d.features && d.remember === true); const devices: Array<TrezorDevice> = getState().devices.filter(
d => d.features && d.remember === true
);
const accounts: Array<Account> = findAccounts(devices, getState().accounts); const accounts: Array<Account> = findAccounts(devices, getState().accounts);
const tokens: Array<Token> = findTokens(accounts, getState().tokens); const tokens: Array<Token> = findTokens(accounts, getState().tokens);
const pending: Array<Transaction> = findPendingTxs(accounts, getState().pending); const pending: Array<Transaction> = findPendingTxs(accounts, getState().pending);
@ -98,14 +123,12 @@ export const update = (event: StorageEvent): ThunkAction => (dispatch: Dispatch)
// check if device was added/ removed // check if device was added/ removed
// const newDevices: Array<TrezorDevice> = JSON.parse(event.newValue); // const newDevices: Array<TrezorDevice> = JSON.parse(event.newValue);
// const myDevices: Array<TrezorDevice> = getState().connect.devices.filter(d => d.features); // const myDevices: Array<TrezorDevice> = getState().connect.devices.filter(d => d.features);
// if (newDevices.length !== myDevices.length) { // if (newDevices.length !== myDevices.length) {
// const diff = myDevices.filter(d => newDevices.indexOf(d) < 0) // const diff = myDevices.filter(d => newDevices.indexOf(d) < 0)
// console.warn("DEV LIST CHANGED!", newDevices.length, myDevices.length, diff) // console.warn("DEV LIST CHANGED!", newDevices.length, myDevices.length, diff)
// // check if difference is caused by local device which is not saved // // check if difference is caused by local device which is not saved
// // or device which was saved in other tab // // or device which was saved in other tab
// } // }
// const diff = oldDevices.filter(d => newDevices.indexOf()) // const diff = oldDevices.filter(d => newDevices.indexOf())
} }
@ -151,19 +174,25 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise<void> =>
const ERC20Abi = await httpRequest(Erc20AbiJSON, 'json'); const ERC20Abi = await httpRequest(Erc20AbiJSON, 'json');
window.addEventListener('storage', (event) => { window.addEventListener('storage', event => {
dispatch(update(event)); dispatch(update(event));
}); });
// load tokens // load tokens
const tokens = await config.networks.reduce(async (promise: Promise<TokensCollection>, network: Network): Promise<TokensCollection> => { const tokens = await config.networks.reduce(
const collection: TokensCollection = await promise; async (
if (network.tokens) { promise: Promise<TokensCollection>,
const json = await httpRequest(network.tokens, 'json'); network: Network
collection[network.shortcut] = json; ): Promise<TokensCollection> => {
} const collection: TokensCollection = await promise;
return collection; if (network.tokens) {
}, Promise.resolve({})); const json = await httpRequest(network.tokens, 'json');
collection[network.shortcut] = json;
}
return collection;
},
Promise.resolve({})
);
dispatch({ dispatch({
type: STORAGE.READY, type: STORAGE.READY,
@ -230,7 +259,9 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => {
} }
if (buildUtils.isDev() || buildUtils.isBeta()) { if (buildUtils.isDev() || buildUtils.isBeta()) {
const betaModal = Object.keys(window.localStorage).find(key => key.indexOf(KEY_BETA_MODAL) >= 0); const betaModal = Object.keys(window.localStorage).find(
key => key.indexOf(KEY_BETA_MODAL) >= 0
);
if (!betaModal) { if (!betaModal) {
dispatch({ dispatch({
type: WALLET.SHOW_BETA_DISCLAIMER, type: WALLET.SHOW_BETA_DISCLAIMER,

@ -2,19 +2,20 @@
import * as LOG from 'actions/constants/log'; import * as LOG from 'actions/constants/log';
import type { import type { Action, ThunkAction, GetState, Dispatch } from 'flowtype';
Action, ThunkAction, GetState, Dispatch,
} from 'flowtype';
import type { LogEntry } from 'reducers/LogReducer'; import type { LogEntry } from 'reducers/LogReducer';
export type LogAction = { export type LogAction =
type: typeof LOG.OPEN, | {
} | { type: typeof LOG.OPEN,
type: typeof LOG.CLOSE, }
} | { | {
type: typeof LOG.ADD, type: typeof LOG.CLOSE,
payload: LogEntry }
}; | {
type: typeof LOG.ADD,
payload: LogEntry,
};
export const toggle = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const toggle = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
if (!getState().log.opened) { if (!getState().log.opened) {

@ -6,25 +6,25 @@ import type { Device } from 'trezor-connect';
import * as MODAL from 'actions/constants/modal'; import * as MODAL from 'actions/constants/modal';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import type { import type { ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice } from 'flowtype';
ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice,
} from 'flowtype';
import type { State } from 'reducers/ModalReducer'; import type { State } from 'reducers/ModalReducer';
import type { parsedURI } from 'utils/cryptoUriParser'; import type { parsedURI } from 'utils/cryptoUriParser';
import sendEthereumFormActions from './ethereum/SendFormActions'; import sendEthereumFormActions from './ethereum/SendFormActions';
import sendRippleFormActions from './ripple/SendFormActions'; import sendRippleFormActions from './ripple/SendFormActions';
export type ModalAction = { export type ModalAction =
type: typeof MODAL.CLOSE | {
} | { type: typeof MODAL.CLOSE,
type: typeof MODAL.OPEN_EXTERNAL_WALLET, }
id: string, | {
url: string, type: typeof MODAL.OPEN_EXTERNAL_WALLET,
} | { id: string,
type: typeof MODAL.OPEN_SCAN_QR, url: string,
}; }
| {
type: typeof MODAL.OPEN_SCAN_QR,
};
export const onPinSubmit = (value: string): Action => { export const onPinSubmit = (value: string): Action => {
TrezorConnect.uiResponse({ type: UI.RECEIVE_PIN, payload: value }); TrezorConnect.uiResponse({ type: UI.RECEIVE_PIN, payload: value });
@ -33,7 +33,10 @@ export const onPinSubmit = (value: string): Action => {
}; };
}; };
export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { modal } = getState(); const { modal } = getState();
if (modal.context !== MODAL.CONTEXT_DEVICE) return; if (modal.context !== MODAL.CONTEXT_DEVICE) return;
@ -59,7 +62,9 @@ export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (di
}); });
}; };
export const onReceiveConfirmation = (confirmation: any): AsyncAction => async (dispatch: Dispatch): Promise<void> => { export const onReceiveConfirmation = (confirmation: any): AsyncAction => async (
dispatch: Dispatch
): Promise<void> => {
await TrezorConnect.uiResponse({ await TrezorConnect.uiResponse({
type: UI.RECEIVE_CONFIRMATION, type: UI.RECEIVE_CONFIRMATION,
payload: confirmation, payload: confirmation,
@ -89,7 +94,9 @@ export const onCancel = (): Action => ({
type: MODAL.CLOSE, type: MODAL.CLOSE,
}); });
export const onDuplicateDevice = (device: TrezorDevice): ThunkAction => (dispatch: Dispatch): void => { export const onDuplicateDevice = (device: TrezorDevice): ThunkAction => (
dispatch: Dispatch
): void => {
dispatch(onCancel()); dispatch(onCancel());
dispatch({ dispatch({
@ -98,11 +105,17 @@ export const onDuplicateDevice = (device: TrezorDevice): ThunkAction => (dispatc
}); });
}; };
export const onRememberRequest = (prevState: State): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onRememberRequest = (prevState: State): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().modal; const state: State = getState().modal;
// handle case where forget modal is already opened // handle case where forget modal is already opened
// TODO: 2 modals at once (two devices disconnected in the same time) // TODO: 2 modals at once (two devices disconnected in the same time)
if (prevState.context === MODAL.CONTEXT_DEVICE && prevState.windowType === CONNECT.REMEMBER_REQUEST) { if (
prevState.context === MODAL.CONTEXT_DEVICE &&
prevState.windowType === CONNECT.REMEMBER_REQUEST
) {
// forget current (new) // forget current (new)
if (state.context === MODAL.CONTEXT_DEVICE) { if (state.context === MODAL.CONTEXT_DEVICE) {
dispatch({ dispatch({
@ -119,12 +132,20 @@ export const onRememberRequest = (prevState: State): ThunkAction => (dispatch: D
} }
}; };
export const onDeviceConnect = (device: Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onDeviceConnect = (device: Device): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
// interrupt process of remembering device (force forget) // interrupt process of remembering device (force forget)
// TODO: the same for disconnect more than 1 device at once // TODO: the same for disconnect more than 1 device at once
const { modal } = getState(); const { modal } = getState();
if (modal.context === MODAL.CONTEXT_DEVICE && modal.windowType === CONNECT.REMEMBER_REQUEST) { if (modal.context === MODAL.CONTEXT_DEVICE && modal.windowType === CONNECT.REMEMBER_REQUEST) {
if (device.features && modal.device && modal.device.features && modal.device.features.device_id === device.features.device_id) { if (
device.features &&
modal.device &&
modal.device.features &&
modal.device.features.device_id === device.features.device_id
) {
dispatch({ dispatch({
type: MODAL.CLOSE, type: MODAL.CLOSE,
}); });
@ -137,7 +158,10 @@ export const onDeviceConnect = (device: Device): ThunkAction => (dispatch: Dispa
} }
}; };
export const onWalletTypeRequest = (hidden: boolean): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onWalletTypeRequest = (hidden: boolean): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const { modal } = getState(); const { modal } = getState();
if (modal.context !== MODAL.CONTEXT_DEVICE) return; if (modal.context !== MODAL.CONTEXT_DEVICE) return;
dispatch({ dispatch({
@ -150,7 +174,9 @@ export const onWalletTypeRequest = (hidden: boolean): ThunkAction => (dispatch:
}); });
}; };
export const gotoExternalWallet = (id: string, url: string): ThunkAction => (dispatch: Dispatch): void => { export const gotoExternalWallet = (id: string, url: string): ThunkAction => (
dispatch: Dispatch
): void => {
dispatch({ dispatch({
type: MODAL.OPEN_EXTERNAL_WALLET, type: MODAL.OPEN_EXTERNAL_WALLET,
id, id,
@ -164,7 +190,9 @@ export const openQrModal = (): ThunkAction => (dispatch: Dispatch): void => {
}); });
}; };
export const onQrScan = (parsedUri: parsedURI, networkType: string): ThunkAction => (dispatch: Dispatch): void => { export const onQrScan = (parsedUri: parsedURI, networkType: string): ThunkAction => (
dispatch: Dispatch
): void => {
const { address = '', amount } = parsedUri; const { address = '', amount } = parsedUri;
switch (networkType) { switch (networkType) {
case 'ethereum': case 'ethereum':
@ -180,7 +208,6 @@ export const onQrScan = (parsedUri: parsedURI, networkType: string): ThunkAction
} }
}; };
export default { export default {
onPinSubmit, onPinSubmit,
onPassphraseSubmit, onPassphraseSubmit,
@ -194,4 +221,4 @@ export default {
gotoExternalWallet, gotoExternalWallet,
openQrModal, openQrModal,
onQrScan, onQrScan,
}; };

@ -2,41 +2,48 @@
import * as React from 'react'; import * as React from 'react';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import type { import type { Action, AsyncAction, GetState, Dispatch, RouterLocationState } from 'flowtype';
Action, AsyncAction, GetState, Dispatch, RouterLocationState,
} from 'flowtype';
import type { CallbackAction } from 'reducers/NotificationReducer'; import type { CallbackAction } from 'reducers/NotificationReducer';
export type NotificationAction = { export type NotificationAction =
type: typeof NOTIFICATION.ADD, | {
payload: { type: typeof NOTIFICATION.ADD,
+type: string, payload: {
+title: React.Node | string, +type: string,
+message?: ?(React.Node | string), +title: React.Node | string,
+cancelable: boolean, +message?: ?(React.Node | string),
actions?: Array<CallbackAction> +cancelable: boolean,
} actions?: Array<CallbackAction>,
} | { },
type: typeof NOTIFICATION.CLOSE, }
payload?: { | {
id?: string; type: typeof NOTIFICATION.CLOSE,
devicePath?: string payload?: {
} id?: string,
} devicePath?: string,
},
};
export const close = (payload: any = {}): Action => ({ export const close = (payload: any = {}): Action => ({
type: NOTIFICATION.CLOSE, type: NOTIFICATION.CLOSE,
payload, payload,
}); });
// called from RouterService // called from RouterService
export const clear = (currentParams: RouterLocationState, requestedParams: RouterLocationState): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const clear = (
currentParams: RouterLocationState,
requestedParams: RouterLocationState
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
// if route has been changed from device view into something else (like other device, settings...) // if route has been changed from device view into something else (like other device, settings...)
// try to remove all Notifications which are linked to previous device (they are not cancelable by user) // try to remove all Notifications which are linked to previous device (they are not cancelable by user)
if (currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) { if (
const entries = getState().notifications.filter(entry => typeof entry.devicePath === 'string'); currentParams.device !== requestedParams.device ||
entries.forEach((entry) => { currentParams.deviceInstance !== requestedParams.deviceInstance
) {
const entries = getState().notifications.filter(
entry => typeof entry.devicePath === 'string'
);
entries.forEach(entry => {
if (typeof entry.devicePath === 'string') { if (typeof entry.devicePath === 'string') {
dispatch({ dispatch({
type: NOTIFICATION.CLOSE, type: NOTIFICATION.CLOSE,

@ -1,24 +1,28 @@
/* @flow */ /* @flow */
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import type { Transaction } from 'flowtype'; import type { Transaction } from 'flowtype';
import type { State } from 'reducers/PendingTxReducer'; import type { State } from 'reducers/PendingTxReducer';
export type PendingTxAction = { export type PendingTxAction =
type: typeof PENDING.FROM_STORAGE, | {
payload: State type: typeof PENDING.FROM_STORAGE,
} | { payload: State,
type: typeof PENDING.ADD, }
payload: Transaction | {
} | { type: typeof PENDING.ADD,
type: typeof PENDING.TX_RESOLVED, payload: Transaction,
hash: string, }
} | { | {
type: typeof PENDING.TX_REJECTED, type: typeof PENDING.TX_RESOLVED,
hash: string, hash: string,
} | { }
type: typeof PENDING.TX_TOKEN_ERROR, | {
hash: string, type: typeof PENDING.TX_REJECTED,
} hash: string,
}
| {
type: typeof PENDING.TX_TOKEN_ERROR,
hash: string,
};

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import * as RECEIVE from 'actions/constants/receive'; import * as RECEIVE from 'actions/constants/receive';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
@ -8,25 +7,29 @@ import * as NOTIFICATION from 'actions/constants/notification';
import { initialState } from 'reducers/ReceiveReducer'; import { initialState } from 'reducers/ReceiveReducer';
import type { State } from 'reducers/ReceiveReducer'; import type { State } from 'reducers/ReceiveReducer';
import type { import type { TrezorDevice, ThunkAction, AsyncAction, Action, GetState, Dispatch } from 'flowtype';
TrezorDevice, ThunkAction, AsyncAction, Action, GetState, Dispatch,
} from 'flowtype'; export type ReceiveAction =
| {
export type ReceiveAction = { type: typeof RECEIVE.INIT,
type: typeof RECEIVE.INIT, state: State,
state: State }
} | { | {
type: typeof RECEIVE.DISPOSE, type: typeof RECEIVE.DISPOSE,
} | { }
type: typeof RECEIVE.REQUEST_UNVERIFIED, | {
device: TrezorDevice type: typeof RECEIVE.REQUEST_UNVERIFIED,
} | { device: TrezorDevice,
type: typeof RECEIVE.SHOW_ADDRESS }
} | { | {
type: typeof RECEIVE.HIDE_ADDRESS type: typeof RECEIVE.SHOW_ADDRESS,
} | { }
type: typeof RECEIVE.SHOW_UNVERIFIED_ADDRESS | {
} type: typeof RECEIVE.HIDE_ADDRESS,
}
| {
type: typeof RECEIVE.SHOW_UNVERIFIED_ADDRESS,
};
export const init = (): ThunkAction => (dispatch: Dispatch): void => { export const init = (): ThunkAction => (dispatch: Dispatch): void => {
const state: State = { const state: State = {
@ -48,7 +51,10 @@ export const showUnverifiedAddress = (): Action => ({
}); });
//export const showAddress = (address_n: string): AsyncAction => { //export const showAddress = (address_n: string): AsyncAction => {
export const showAddress = (path: Array<number>): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const showAddress = (path: Array<number>): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
@ -81,7 +87,11 @@ export const showAddress = (path: Array<number>): AsyncAction => async (dispatch
response = await TrezorConnect.rippleGetAddress(params); response = await TrezorConnect.rippleGetAddress(params);
break; break;
default: default:
response = { payload: { error: `ReceiveActions.showAddress: Unknown network type: ${network.type}` } }; response = {
payload: {
error: `ReceiveActions.showAddress: Unknown network type: ${network.type}`,
},
};
break; break;
} }
@ -123,4 +133,4 @@ export default {
dispose, dispose,
showAddress, showAddress,
showUnverifiedAddress, showUnverifiedAddress,
}; };

@ -18,9 +18,11 @@ import type {
import type { RouterAction } from 'connected-react-router'; import type { RouterAction } from 'connected-react-router';
/* /*
* Parse url string to RouterLocationState object (key/value) * Parse url string to RouterLocationState object (key/value)
*/ */
export const pathToParams = (path: string): PayloadAction<RouterLocationState> => (): RouterLocationState => { export const pathToParams = (
path: string
): PayloadAction<RouterLocationState> => (): RouterLocationState => {
// split url into parts // split url into parts
const parts: Array<string> = path.split('/').slice(1); const parts: Array<string> = path.split('/').slice(1);
const params: RouterLocationState = {}; const params: RouterLocationState = {};
@ -46,10 +48,13 @@ export const pathToParams = (path: string): PayloadAction<RouterLocationState> =
}; };
/* /*
* RouterLocationState validation * RouterLocationState validation
* Check if requested device or network exists in reducers * Check if requested device or network exists in reducers
*/ */
export const paramsValidation = (params: RouterLocationState): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => { export const paramsValidation = (params: RouterLocationState): PayloadAction<boolean> => (
dispatch: Dispatch,
getState: GetState
): boolean => {
// validate requested device // validate requested device
if (params.hasOwnProperty('device')) { if (params.hasOwnProperty('device')) {
@ -57,9 +62,18 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction<boo
let device: ?TrezorDevice; let device: ?TrezorDevice;
if (params.hasOwnProperty('deviceInstance')) { if (params.hasOwnProperty('deviceInstance')) {
device = devices.find(d => d.features && d.features.device_id === params.device && d.instance === parseInt(params.deviceInstance, 10)); device = devices.find(
d =>
d.features &&
d.features.device_id === params.device &&
d.instance === parseInt(params.deviceInstance, 10)
);
} else { } else {
device = devices.find(d => ((!d.features || d.mode === 'bootloader') && d.path === params.device) || (d.features && d.features.device_id === params.device)); device = devices.find(
d =>
((!d.features || d.mode === 'bootloader') && d.path === params.device) ||
(d.features && d.features.device_id === params.device)
);
} }
if (!device) return false; if (!device) return false;
@ -87,12 +101,16 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction<boo
}; };
/* /*
* Composing url string from given RouterLocationState object * Composing url string from given RouterLocationState object
* Filters unrecognized fields and sorting in correct order * Filters unrecognized fields and sorting in correct order
*/ */
export const paramsToPath = (params: RouterLocationState): PayloadAction<?string> => (): ?string => { export const paramsToPath = (
params: RouterLocationState
): PayloadAction<?string> => (): ?string => {
// get patterns (fields) from routes and sort them by complexity // get patterns (fields) from routes and sort them by complexity
const patterns: Array<Array<string>> = routes.map(r => r.fields).sort((a, b) => (a.length > b.length ? -1 : 1)); const patterns: Array<Array<string>> = routes
.map(r => r.fields)
.sort((a, b) => (a.length > b.length ? -1 : 1));
// find pattern // find pattern
const keys: Array<string> = Object.keys(params); const keys: Array<string> = Object.keys(params);
@ -111,7 +129,7 @@ export const paramsToPath = (params: RouterLocationState): PayloadAction<?string
// compose url string from pattern // compose url string from pattern
let url: string = ''; let url: string = '';
patternToUse.forEach((field) => { patternToUse.forEach(field => {
if (field === params[field]) { if (field === params[field]) {
// standalone (odd) fields // standalone (odd) fields
url += `/${field}`; url += `/${field}`;
@ -127,7 +145,10 @@ export const paramsToPath = (params: RouterLocationState): PayloadAction<?string
return url; return url;
}; };
export const getValidUrl = (action: RouterAction): PayloadAction<string> => (dispatch: Dispatch, getState: GetState): string => { export const getValidUrl = (action: RouterAction): PayloadAction<string> => (
dispatch: Dispatch,
getState: GetState
): string => {
const { location } = getState().router; const { location } = getState().router;
const { firstLocationChange } = getState().wallet; const { firstLocationChange } = getState().wallet;
// redirect to landing page (loading screen) // redirect to landing page (loading screen)
@ -151,12 +172,17 @@ export const getValidUrl = (action: RouterAction): PayloadAction<string> => (dis
// example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect // example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect
const currentParams = dispatch(pathToParams(location.pathname)); const currentParams = dispatch(pathToParams(location.pathname));
const currentParamsAreValid = dispatch(paramsValidation(currentParams)); const currentParamsAreValid = dispatch(paramsValidation(currentParams));
if (currentParamsAreValid) { return location.pathname; } if (currentParamsAreValid) {
return location.pathname;
}
} }
// there are no connected devices or application isn't ready or initialization error occurred // there are no connected devices or application isn't ready or initialization error occurred
// redirect to landing page // redirect to landing page
const shouldBeLandingPage = getState().devices.length < 1 || !getState().wallet.ready || getState().connect.error !== null; const shouldBeLandingPage =
getState().devices.length < 1 ||
!getState().wallet.ready ||
getState().connect.error !== null;
const landingPageUrl = dispatch(isLandingPageUrl(requestedUrl)); const landingPageUrl = dispatch(isLandingPageUrl(requestedUrl));
if (shouldBeLandingPage) { if (shouldBeLandingPage) {
const landingPageRoute = dispatch(isLandingPageUrl(requestedUrl, getState().wallet.ready)); const landingPageRoute = dispatch(isLandingPageUrl(requestedUrl, getState().wallet.ready));
@ -185,13 +211,17 @@ export const getValidUrl = (action: RouterAction): PayloadAction<string> => (dis
}; };
/* /*
* Compose url from requested device object and returns url * Compose url from requested device object and returns url
*/ */
const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction<?string> => (dispatch: Dispatch, getState: GetState): ?string => { const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction<?string> => (
dispatch: Dispatch,
getState: GetState
): ?string => {
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'}`;
} else if (device.mode === 'bootloader') { // device in bootloader doesn't have device_id } else if (device.mode === 'bootloader') {
// device in bootloader doesn't have device_id
url = `/device/${device.path}/bootloader`; url = `/device/${device.path}/bootloader`;
} else if (device.mode === 'initialize') { } else if (device.mode === 'initialize') {
url = `/device/${device.features.device_id}/initialize`; url = `/device/${device.features.device_id}/initialize`;
@ -207,7 +237,9 @@ const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction<?string> =>
if (!device.hasOwnProperty('ts')) { if (!device.hasOwnProperty('ts')) {
// it is device from trezor-connect triggered by DEVICE.CONNECT event // it is device from trezor-connect triggered by DEVICE.CONNECT event
// need to lookup if there are unavailable instances // need to lookup if there are unavailable instances
const available: Array<TrezorDevice> = getState().devices.filter(d => d.path === device.path); const available: Array<TrezorDevice> = getState().devices.filter(
d => d.path === device.path
);
const latest: Array<TrezorDevice> = sortDevices(available); const latest: Array<TrezorDevice> = sortDevices(available);
if (latest.length > 0 && latest[0].instance) { if (latest.length > 0 && latest[0].instance) {
url += `:${latest[0].instance}`; url += `:${latest[0].instance}`;
@ -218,13 +250,16 @@ const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction<?string> =>
}; };
/* /*
* Try to find first available device using order: * Try to find first available device using order:
* 1. First unacquired * 1. First unacquired
* 2. First connected * 2. First connected
* 3. Saved with latest timestamp * 3. Saved with latest timestamp
* OR redirect to landing page * OR redirect to landing page
*/ */
export const getFirstAvailableDeviceUrl = (): PayloadAction<?string> => (dispatch: Dispatch, getState: GetState): ?string => { export const getFirstAvailableDeviceUrl = (): PayloadAction<?string> => (
dispatch: Dispatch,
getState: GetState
): ?string => {
const { devices } = getState(); const { devices } = getState();
let url: ?string; let url: ?string;
if (devices.length > 0) { if (devices.length > 0) {
@ -241,20 +276,24 @@ export const getFirstAvailableDeviceUrl = (): PayloadAction<?string> => (dispatc
}; };
/* /*
* Utility used in "getDeviceUrl" and "getFirstAvailableDeviceUrl" * Utility used in "getDeviceUrl" and "getFirstAvailableDeviceUrl"
* sorting device array by "ts" (timestamp) field * sorting device array by "ts" (timestamp) field
*/ */
const sortDevices = (devices: Array<TrezorDevice>): Array<TrezorDevice> => devices.sort((a, b) => { const sortDevices = (devices: Array<TrezorDevice>): Array<TrezorDevice> =>
if (!a.ts || !b.ts) { devices.sort((a, b) => {
return -1; if (!a.ts || !b.ts) {
} return -1;
return a.ts > b.ts ? -1 : 1; }
}); return a.ts > b.ts ? -1 : 1;
});
/* /*
* Redirect to requested device * Redirect to requested device
*/ */
export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
if (dispatch(setInitialUrl())) return; if (dispatch(setInitialUrl())) return;
const url: ?string = dispatch(getDeviceUrl(device)); const url: ?string = dispatch(getDeviceUrl(device));
@ -262,20 +301,30 @@ export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dis
const currentParams: RouterLocationState = getState().router.location.state; const currentParams: RouterLocationState = getState().router.location.state;
const requestedParams = dispatch(pathToParams(url)); const requestedParams = dispatch(pathToParams(url));
if (currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) { if (
currentParams.device !== requestedParams.device ||
currentParams.deviceInstance !== requestedParams.deviceInstance
) {
dispatch(goto(url)); dispatch(goto(url));
} }
}; };
/* /*
* Redirect to first device or landing page * Redirect to first device or landing page
*/ */
export const selectFirstAvailableDevice = (gotoRoot: boolean = false): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const selectFirstAvailableDevice = (gotoRoot: boolean = false): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const url = dispatch(getFirstAvailableDeviceUrl()); const url = dispatch(getFirstAvailableDeviceUrl());
if (url) { if (url) {
const currentParams = getState().router.location.state; const currentParams = getState().router.location.state;
const requestedParams = dispatch(pathToParams(url)); const requestedParams = dispatch(pathToParams(url));
if (gotoRoot || currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) { if (
gotoRoot ||
currentParams.device !== requestedParams.device ||
currentParams.deviceInstance !== requestedParams.deviceInstance
) {
dispatch(goto(url)); dispatch(goto(url));
} }
} else { } else {
@ -284,8 +333,8 @@ export const selectFirstAvailableDevice = (gotoRoot: boolean = false): ThunkActi
}; };
/* /*
* Internal method. redirect to given url * Internal method. redirect to given url
*/ */
const goto = (url: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const goto = (url: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
if (getState().router.location.pathname !== url) { if (getState().router.location.pathname !== url) {
dispatch(push(url)); dispatch(push(url));
@ -293,15 +342,20 @@ const goto = (url: string): ThunkAction => (dispatch: Dispatch, getState: GetSta
}; };
/* /*
* Check if requested OR current url is landing page * Check if requested OR current url is landing page
*/ */
export const isLandingPageUrl = ($url?: string, checkRoutes: boolean = false): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => { export const isLandingPageUrl = (
$url?: string,
checkRoutes: boolean = false
): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
let url: ?string = $url; let url: ?string = $url;
if (typeof url !== 'string') { if (typeof url !== 'string') {
url = getState().router.location.pathname; url = getState().router.location.pathname;
} }
if (checkRoutes) { if (checkRoutes) {
const isLandingRoute = routes.find(r => r.pattern === url && r.name.indexOf('landing') >= 0); const isLandingRoute = routes.find(
r => r.pattern === url && r.name.indexOf('landing') >= 0
);
if (isLandingRoute) { if (isLandingRoute) {
return true; return true;
} }
@ -310,8 +364,8 @@ export const isLandingPageUrl = ($url?: string, checkRoutes: boolean = false): P
}; };
/* /*
* Try to redirect to landing page * Try to redirect to landing page
*/ */
export const gotoLandingPage = (): ThunkAction => (dispatch: Dispatch): void => { export const gotoLandingPage = (): ThunkAction => (dispatch: Dispatch): void => {
const isLandingPage = dispatch(isLandingPageUrl()); const isLandingPage = dispatch(isLandingPageUrl());
if (!isLandingPage) { if (!isLandingPage) {
@ -320,61 +374,76 @@ export const gotoLandingPage = (): ThunkAction => (dispatch: Dispatch): void =>
}; };
/* /*
* Go to given device settings page * Go to given device settings page
*/ */
export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispatch: Dispatch): void => { export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (
dispatch: Dispatch
): void => {
if (device.features) { if (device.features) {
const devUrl: string = `${device.features.device_id}${device.instance ? `:${device.instance}` : ''}`; const devUrl: string = `${device.features.device_id}${
device.instance ? `:${device.instance}` : ''
}`;
dispatch(goto(`/device/${devUrl}/settings`)); dispatch(goto(`/device/${devUrl}/settings`));
} }
}; };
/* /*
* Go to UpdateBridge page * Go to UpdateBridge page
*/ */
export const gotoBridgeUpdate = (): ThunkAction => (dispatch: Dispatch): void => { export const gotoBridgeUpdate = (): ThunkAction => (dispatch: Dispatch): void => {
dispatch(goto('/bridge')); dispatch(goto('/bridge'));
}; };
/* /*
* Go to UpdateFirmware page * Go to UpdateFirmware page
* Called from App notification * Called from App notification
*/ */
export const gotoFirmwareUpdate = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const gotoFirmwareUpdate = (): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const { selectedDevice } = getState().wallet; const { selectedDevice } = getState().wallet;
if (!selectedDevice || !selectedDevice.features) return; if (!selectedDevice || !selectedDevice.features) return;
const devUrl: string = `${selectedDevice.features.device_id}${selectedDevice.instance ? `:${selectedDevice.instance}` : ''}`; const devUrl: string = `${selectedDevice.features.device_id}${
selectedDevice.instance ? `:${selectedDevice.instance}` : ''
}`;
dispatch(goto(`/device/${devUrl}/firmware-update`)); dispatch(goto(`/device/${devUrl}/firmware-update`));
}; };
/* /*
* Go to NoBackup page * Go to NoBackup page
*/ */
export const gotoBackup = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const gotoBackup = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const { selectedDevice } = getState().wallet; const { selectedDevice } = getState().wallet;
if (!selectedDevice || !selectedDevice.features) return; if (!selectedDevice || !selectedDevice.features) return;
const devUrl: string = `${selectedDevice.features.device_id}${selectedDevice.instance ? `:${selectedDevice.instance}` : ''}`; const devUrl: string = `${selectedDevice.features.device_id}${
selectedDevice.instance ? `:${selectedDevice.instance}` : ''
}`;
dispatch(goto(`/device/${devUrl}/backup`)); dispatch(goto(`/device/${devUrl}/backup`));
}; };
/* /*
* Try to redirect to initial url * Try to redirect to initial url
*/ */
export const setInitialUrl = (): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => { export const setInitialUrl = (): PayloadAction<boolean> => (
dispatch: Dispatch,
getState: GetState
): boolean => {
const { initialPathname } = getState().wallet; const { initialPathname } = getState().wallet;
if (typeof initialPathname === 'string' && !dispatch(isLandingPageUrl(initialPathname, true))) { if (typeof initialPathname === 'string' && !dispatch(isLandingPageUrl(initialPathname, true))) {
const valid = dispatch(getValidUrl({ const valid = dispatch(
type: LOCATION_CHANGE, getValidUrl({
payload: { type: LOCATION_CHANGE,
location: { payload: {
pathname: initialPathname, location: {
hash: '', pathname: initialPathname,
search: '', hash: '',
state: {}, search: '',
state: {},
},
}, },
}, })
})); );
if (valid === initialPathname) { if (valid === initialPathname) {
// reset initial url // reset initial url

@ -11,13 +11,7 @@ import * as reducerUtils from 'reducers/utils';
import { getVersion } from 'utils/device'; import { getVersion } from 'utils/device';
import { initialState } from 'reducers/SelectedAccountReducer'; import { initialState } from 'reducers/SelectedAccountReducer';
import type { import type { PayloadAction, Action, GetState, Dispatch, State } from 'flowtype';
PayloadAction,
Action,
GetState,
Dispatch,
State,
} from 'flowtype';
import type { import type {
State as SelectedAccountState, State as SelectedAccountState,
@ -26,12 +20,14 @@ import type {
ExceptionPage, ExceptionPage,
} from 'reducers/SelectedAccountReducer'; } from 'reducers/SelectedAccountReducer';
export type SelectedAccountAction = { export type SelectedAccountAction =
type: typeof ACCOUNT.DISPOSE, | {
} | { type: typeof ACCOUNT.DISPOSE,
type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT, }
payload: SelectedAccountState, | {
}; type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT,
payload: SelectedAccountState,
};
export const dispose = (): Action => ({ export const dispose = (): Action => ({
type: ACCOUNT.DISPOSE, type: ACCOUNT.DISPOSE,
@ -68,11 +64,7 @@ const getExceptionPage = (state: State, selectedAccount: SelectedAccountState):
// display loader instead of component body // display loader instead of component body
const getAccountLoader = (state: State, selectedAccount: SelectedAccountState): ?Loader => { const getAccountLoader = (state: State, selectedAccount: SelectedAccountState): ?Loader => {
const device = state.wallet.selectedDevice; const device = state.wallet.selectedDevice;
const { const { account, discovery, network } = selectedAccount;
account,
discovery,
network,
} = selectedAccount;
if (!device || !device.state) { if (!device || !device.state) {
return { return {
@ -89,7 +81,6 @@ const getAccountLoader = (state: State, selectedAccount: SelectedAccountState):
}; };
} }
if (account) return null; if (account) return null;
// account not found (yet). checking why... // account not found (yet). checking why...
@ -135,7 +126,10 @@ const getAccountLoader = (state: State, selectedAccount: SelectedAccountState):
}; };
// display notification above the component, with or without component body // display notification above the component, with or without component body
const getAccountNotification = (state: State, selectedAccount: SelectedAccountState): ?(Notification & { shouldRender: boolean }) => { const getAccountNotification = (
state: State,
selectedAccount: SelectedAccountState
): ?(Notification & { shouldRender: boolean }) => {
const device = state.wallet.selectedDevice; const device = state.wallet.selectedDevice;
const { account, network, discovery } = selectedAccount; const { account, network, discovery } = selectedAccount;
if (!device || !network) return null; if (!device || !network) return null;
@ -190,16 +184,21 @@ const actions = [
...Object.values(BLOCKCHAIN).filter(v => typeof v === 'string'), ...Object.values(BLOCKCHAIN).filter(v => typeof v === 'string'),
WALLET.SET_SELECTED_DEVICE, WALLET.SET_SELECTED_DEVICE,
WALLET.UPDATE_SELECTED_DEVICE, WALLET.UPDATE_SELECTED_DEVICE,
...Object.values(ACCOUNT).filter(v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT && v !== ACCOUNT.DISPOSE), // exported values got unwanted "__esModule: true" as first element ...Object.values(ACCOUNT).filter(
v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT && v !== ACCOUNT.DISPOSE
), // exported values got unwanted "__esModule: true" as first element
...Object.values(DISCOVERY).filter(v => typeof v === 'string'), ...Object.values(DISCOVERY).filter(v => typeof v === 'string'),
...Object.values(TOKEN).filter(v => typeof v === 'string'), ...Object.values(TOKEN).filter(v => typeof v === 'string'),
...Object.values(PENDING).filter(v => typeof v === 'string'), ...Object.values(PENDING).filter(v => typeof v === 'string'),
]; ];
/* /*
* Called from WalletService * Called from WalletService
*/ */
export const observe = (prevState: State, action: Action): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => { export const observe = (prevState: State, action: Action): PayloadAction<boolean> => (
dispatch: Dispatch,
getState: GetState
): boolean => {
// ignore not listed actions // ignore not listed actions
if (actions.indexOf(action.type) < 0) return false; if (actions.indexOf(action.type) < 0) return false;
const state: State = getState(); const state: State = getState();
@ -238,12 +237,22 @@ export const observe = (prevState: State, action: Action): PayloadAction<boolean
newState.notification = notification; newState.notification = notification;
} }
newState.shouldRender = !(loader || exceptionPage || (notification && !notification.shouldRender)); newState.shouldRender = !(
loader ||
exceptionPage ||
(notification && !notification.shouldRender)
);
// check if newState is different than previous state // check if newState is different than previous state
const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, { const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, {
account: ['balance', 'nonce'], account: ['balance', 'nonce'],
discovery: ['accountIndex', 'interrupted', 'completed', 'waitingForBlockchain', 'waitingForDevice'], discovery: [
'accountIndex',
'interrupted',
'completed',
'waitingForBlockchain',
'waitingForDevice',
],
}); });
if (stateChanged) { if (stateChanged) {

@ -4,33 +4,30 @@ import * as SEND from 'actions/constants/send';
import * as WEB3 from 'actions/constants/web3'; import * as WEB3 from 'actions/constants/web3';
import * as BLOCKCHAIN from 'actions/constants/blockchain'; import * as BLOCKCHAIN from 'actions/constants/blockchain';
import type { import type { Dispatch, GetState, State as ReducersState, Action, ThunkAction } from 'flowtype';
Dispatch,
GetState,
State as ReducersState,
Action,
ThunkAction,
} from 'flowtype';
import type { State as EthereumState } from 'reducers/SendFormEthereumReducer'; import type { State as EthereumState } from 'reducers/SendFormEthereumReducer';
import type { State as RippleState } from 'reducers/SendFormRippleReducer'; import type { State as RippleState } from 'reducers/SendFormRippleReducer';
import * as EthereumSendFormActions from './ethereum/SendFormActions'; import * as EthereumSendFormActions from './ethereum/SendFormActions';
import * as RippleSendFormActions from './ripple/SendFormActions'; import * as RippleSendFormActions from './ripple/SendFormActions';
export type SendFormAction = { export type SendFormAction =
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR, | {
networkType: 'ethereum', type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR,
state: EthereumState, networkType: 'ethereum',
} | { state: EthereumState,
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR, }
networkType: 'ripple', | {
state: RippleState, type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR,
} | { networkType: 'ripple',
type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR, state: RippleState,
} | { }
type: typeof SEND.TX_COMPLETE, | {
}; type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR,
}
| {
type: typeof SEND.TX_COMPLETE,
};
// list of all actions which has influence on "sendForm" reducer // list of all actions which has influence on "sendForm" reducer
// other actions will be ignored // other actions will be ignored
@ -42,9 +39,12 @@ const actions = [
]; ];
/* /*
* Called from WalletService * 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 // ignore not listed actions
if (actions.indexOf(action.type) < 0) return; if (actions.indexOf(action.type) < 0) return;
@ -62,6 +62,7 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
case 'ripple': case 'ripple':
dispatch(RippleSendFormActions.observe(prevState, action)); dispatch(RippleSendFormActions.observe(prevState, action));
break; break;
default: break; default:
break;
} }
}; };

@ -4,12 +4,7 @@ import { findToken } from 'reducers/utils';
import type { State as EthereumSendFormState } from 'reducers/SendFormEthereumReducer'; import type { State as EthereumSendFormState } from 'reducers/SendFormEthereumReducer';
import type { State as RippleSendFormState } from 'reducers/SendFormRippleReducer'; import type { State as RippleSendFormState } from 'reducers/SendFormRippleReducer';
import type { import type { ThunkAction, PayloadAction, GetState, Dispatch } from 'flowtype';
ThunkAction,
PayloadAction,
GetState,
Dispatch,
} from 'flowtype';
const TYPE: 'session' = 'session'; const TYPE: 'session' = 'session';
const { STORAGE_PATH } = storageUtils; const { STORAGE_PATH } = storageUtils;
@ -20,7 +15,10 @@ const getTxDraftKey = (getState: GetState): string => {
return `${KEY_TX_DRAFT}${pathname}`; return `${KEY_TX_DRAFT}${pathname}`;
}; };
export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const saveDraftTransaction = (): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state = getState().sendFormEthereum; const state = getState().sendFormEthereum;
if (state.untouched) return; if (state.untouched) return;
@ -28,7 +26,10 @@ export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getS
storageUtils.set(TYPE, key, JSON.stringify(state)); storageUtils.set(TYPE, key, JSON.stringify(state));
}; };
export const loadEthereumDraftTransaction = (): PayloadAction<?EthereumSendFormState> => (dispatch: Dispatch, getState: GetState): ?EthereumSendFormState => { export const loadEthereumDraftTransaction = (): PayloadAction<?EthereumSendFormState> => (
dispatch: Dispatch,
getState: GetState
): ?EthereumSendFormState => {
const key = getTxDraftKey(getState); const key = getTxDraftKey(getState);
const value: ?string = storageUtils.get(TYPE, key); const value: ?string = storageUtils.get(TYPE, key);
if (!value) return null; if (!value) return null;
@ -53,7 +54,10 @@ export const loadEthereumDraftTransaction = (): PayloadAction<?EthereumSendFormS
return state; return state;
}; };
export const loadRippleDraftTransaction = (): PayloadAction<?RippleSendFormState> => (dispatch: Dispatch, getState: GetState): ?RippleSendFormState => { export const loadRippleDraftTransaction = (): PayloadAction<?RippleSendFormState> => (
dispatch: Dispatch,
getState: GetState
): ?RippleSendFormState => {
const key = getTxDraftKey(getState); const key = getTxDraftKey(getState);
const value: ?string = storageUtils.get(TYPE, key); const value: ?string = storageUtils.get(TYPE, key);
if (!value) return null; if (!value) return null;

@ -1,41 +1,45 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import type { import type { GetState, Dispatch, ThunkAction, AsyncAction } from 'flowtype';
GetState, Dispatch, ThunkAction, AsyncAction,
} from 'flowtype';
import { validateAddress } from 'utils/ethUtils'; import { validateAddress } from 'utils/ethUtils';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as SIGN_VERIFY from './constants/signVerify'; import * as SIGN_VERIFY from './constants/signVerify';
export type SignVerifyAction = { export type SignVerifyAction =
type: typeof SIGN_VERIFY.SIGN_SUCCESS, | {
signSignature: string type: typeof SIGN_VERIFY.SIGN_SUCCESS,
} | { signSignature: string,
type: typeof SIGN_VERIFY.CLEAR_SIGN, }
} | { | {
type: typeof SIGN_VERIFY.CLEAR_VERIFY, type: typeof SIGN_VERIFY.CLEAR_SIGN,
} | { }
type: typeof SIGN_VERIFY.INPUT_CHANGE, | {
inputName: string, type: typeof SIGN_VERIFY.CLEAR_VERIFY,
value: string }
} | { | {
type: typeof SIGN_VERIFY.TOUCH, type: typeof SIGN_VERIFY.INPUT_CHANGE,
inputName: string, inputName: string,
} | { value: string,
type: typeof SIGN_VERIFY.ERROR, }
inputName: string, | {
message: ?string type: typeof SIGN_VERIFY.TOUCH,
} | { inputName: string,
type: typeof SIGN_VERIFY.ERROR, }
inputName: string, | {
message: ?string type: typeof SIGN_VERIFY.ERROR,
} inputName: string,
message: ?string,
}
| {
type: typeof SIGN_VERIFY.ERROR,
inputName: string,
message: ?string,
};
const sign = ( const sign = (path: Array<number>, message: string, hex: boolean = false): AsyncAction => async (
path: Array<number>, dispatch: Dispatch,
message: string, getState: GetState
hex: boolean = false, ): Promise<void> => {
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (!selected) return; if (!selected) return;
@ -73,7 +77,7 @@ const verify = (
address: string, address: string,
message: string, message: string,
signature: string, signature: string,
hex: boolean = false, hex: boolean = false
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { ): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (!selected) return; if (!selected) return;
@ -125,7 +129,9 @@ const verify = (
} }
}; };
const inputChange = (inputName: string, value: string): ThunkAction => (dispatch: Dispatch): void => { const inputChange = (inputName: string, value: string): ThunkAction => (
dispatch: Dispatch
): void => {
dispatch({ dispatch({
type: SIGN_VERIFY.INPUT_CHANGE, type: SIGN_VERIFY.INPUT_CHANGE,
inputName, inputName,
@ -162,4 +168,4 @@ export default {
clearSign, clearSign,
clearVerify, clearVerify,
inputChange, inputChange,
}; };

@ -2,19 +2,20 @@
import * as SUMMARY from 'actions/constants/summary'; import * as SUMMARY from 'actions/constants/summary';
import { initialState } from 'reducers/SummaryReducer'; import { initialState } from 'reducers/SummaryReducer';
import type { import type { ThunkAction, Action, Dispatch } from 'flowtype';
ThunkAction, Action, Dispatch,
} from 'flowtype';
import type { State } from 'reducers/SummaryReducer'; import type { State } from 'reducers/SummaryReducer';
export type SummaryAction = { export type SummaryAction =
type: typeof SUMMARY.INIT, | {
state: State type: typeof SUMMARY.INIT,
} | { state: State,
type: typeof SUMMARY.DISPOSE, }
} | { | {
type: typeof SUMMARY.DETAILS_TOGGLE type: typeof SUMMARY.DISPOSE,
} }
| {
type: typeof SUMMARY.DETAILS_TOGGLE,
};
export const init = (): ThunkAction => (dispatch: Dispatch): void => { export const init = (): ThunkAction => (dispatch: Dispatch): void => {
const state: State = { const state: State = {

@ -1,41 +1,47 @@
/* @flow */ /* @flow */
import * as TOKEN from 'actions/constants/token'; import * as TOKEN from 'actions/constants/token';
import type { import type { GetState, AsyncAction, Action, Dispatch } from 'flowtype';
GetState, AsyncAction, Action, Dispatch,
} from 'flowtype';
import type { State, Token } from 'reducers/TokensReducer'; import type { State, Token } from 'reducers/TokensReducer';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
import type { NetworkToken } from 'reducers/LocalStorageReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer';
import * as BlockchainActions from 'actions/ethereum/BlockchainActions'; import * as BlockchainActions from 'actions/ethereum/BlockchainActions';
export type TokenAction = { export type TokenAction =
type: typeof TOKEN.FROM_STORAGE, | {
payload: State type: typeof TOKEN.FROM_STORAGE,
} | { payload: State,
type: typeof TOKEN.ADD, }
payload: Token | {
} | { type: typeof TOKEN.ADD,
type: typeof TOKEN.REMOVE, payload: Token,
token: Token }
} | { | {
type: typeof TOKEN.SET_BALANCE, type: typeof TOKEN.REMOVE,
payload: State token: Token,
} }
| {
type: typeof TOKEN.SET_BALANCE,
payload: State,
};
// action from component <reactSelect> // action from component <reactSelect>
export const load = ($input: string, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<any> => { export const load = ($input: string, network: string): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<any> => {
let input = $input; let input = $input;
if (input.length < 1) input = '0x'; if (input.length < 1) input = '0x';
const tokens = getState().localStorage.tokens[network]; const tokens = getState().localStorage.tokens[network];
const value = input.toLowerCase(); const value = input.toLowerCase();
const result = tokens.filter(t => t.symbol.toLowerCase().indexOf(value) >= 0 const result = tokens.filter(
|| t.address.toLowerCase().indexOf(value) >= 0 t =>
|| t.name.toLowerCase().indexOf(value) >= 0); t.symbol.toLowerCase().indexOf(value) >= 0 ||
t.address.toLowerCase().indexOf(value) >= 0 ||
t.name.toLowerCase().indexOf(value) >= 0
);
if (result.length > 0) { if (result.length > 0) {
// TODO: Temporary fix for async select // TODO: Temporary fix for async select
@ -52,23 +58,33 @@ export const load = ($input: string, network: string): AsyncAction => async (dis
return null; return null;
}; };
export const setBalance = (tokenAddress: string, ethAddress: string, balance: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const setBalance = (
tokenAddress: string,
ethAddress: string,
balance: string
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const newState: Array<Token> = [...getState().tokens]; const newState: Array<Token> = [...getState().tokens];
const token: ?Token = newState.find(t => t.address === tokenAddress && t.ethAddress === ethAddress); const token: ?Token = newState.find(
t => t.address === tokenAddress && t.ethAddress === ethAddress
);
if (token) { if (token) {
const others = newState.filter(t => t !== token); const others = newState.filter(t => t !== token);
dispatch({ dispatch({
type: TOKEN.SET_BALANCE, type: TOKEN.SET_BALANCE,
payload: others.concat([{ payload: others.concat([
...token, {
loaded: true, ...token,
balance, loaded: true,
}]), balance,
},
]),
}); });
} }
}; };
export const add = (token: NetworkToken, account: Account): AsyncAction => async (dispatch: Dispatch): Promise<void> => { export const add = (token: NetworkToken, account: Account): AsyncAction => async (
dispatch: Dispatch
): Promise<void> => {
const tkn: Token = { const tkn: Token = {
loaded: false, loaded: false,
deviceState: account.deviceState, deviceState: account.deviceState,
@ -93,4 +109,4 @@ export const add = (token: NetworkToken, account: Account): AsyncAction => async
export const remove = (token: Token): Action => ({ export const remove = (token: Token): Action => ({
type: TOKEN.REMOVE, type: TOKEN.REMOVE,
token, token,
}); });

@ -1,6 +1,10 @@
/* @flow */ /* @flow */
import TrezorConnect, { import TrezorConnect, {
DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT, DEVICE,
DEVICE_EVENT,
UI_EVENT,
TRANSPORT_EVENT,
BLOCKCHAIN_EVENT,
} from 'trezor-connect'; } from 'trezor-connect';
import { CONTEXT_NONE } from 'actions/constants/modal'; import { CONTEXT_NONE } from 'actions/constants/modal';
import urlConstants from 'constants/urls'; import urlConstants from 'constants/urls';
@ -31,96 +35,127 @@ import type {
TrezorDevice, TrezorDevice,
} from 'flowtype'; } from 'flowtype';
export type TrezorConnectAction =
export type TrezorConnectAction = { | {
type: typeof CONNECT.INITIALIZATION_ERROR, type: typeof CONNECT.INITIALIZATION_ERROR,
error: string error: string,
} | { }
type: typeof CONNECT.NETWORK_CHANGED, | {
payload: { type: typeof CONNECT.NETWORK_CHANGED,
network: string payload: {
} network: string,
} | { },
type: typeof CONNECT.AUTH_DEVICE, }
device: TrezorDevice, | {
state: string type: typeof CONNECT.AUTH_DEVICE,
} | { device: TrezorDevice,
type: typeof CONNECT.DUPLICATE, state: string,
device: TrezorDevice }
} | { | {
type: typeof CONNECT.REMEMBER_REQUEST, type: typeof CONNECT.DUPLICATE,
device: TrezorDevice, device: TrezorDevice,
instances: Array<TrezorDevice> }
} | { | {
type: typeof CONNECT.DISCONNECT_REQUEST, type: typeof CONNECT.REMEMBER_REQUEST,
device: TrezorDevice device: TrezorDevice,
} | { instances: Array<TrezorDevice>,
type: typeof CONNECT.FORGET_REQUEST, }
device: TrezorDevice | {
} | { type: typeof CONNECT.DISCONNECT_REQUEST,
type: typeof CONNECT.FORGET, device: TrezorDevice,
device: TrezorDevice }
} | { | {
type: typeof CONNECT.FORGET_SINGLE | typeof CONNECT.FORGET_SILENT, type: typeof CONNECT.FORGET_REQUEST,
device: TrezorDevice device: TrezorDevice,
} | { }
type: typeof CONNECT.REMEMBER, | {
device: TrezorDevice type: typeof CONNECT.FORGET,
} | { device: TrezorDevice,
type: typeof CONNECT.TRY_TO_DUPLICATE, }
device: TrezorDevice | {
} | { type: typeof CONNECT.FORGET_SINGLE | typeof CONNECT.FORGET_SILENT,
type: typeof CONNECT.DEVICE_FROM_STORAGE, device: TrezorDevice,
payload: Array<TrezorDevice> }
} | { | {
type: typeof CONNECT.START_ACQUIRING | typeof CONNECT.STOP_ACQUIRING, type: typeof CONNECT.REMEMBER,
} | { device: TrezorDevice,
type: typeof CONNECT.REQUEST_WALLET_TYPE, }
device: TrezorDevice | {
} | { type: typeof CONNECT.TRY_TO_DUPLICATE,
type: typeof CONNECT.RECEIVE_WALLET_TYPE | typeof CONNECT.UPDATE_WALLET_TYPE, device: TrezorDevice,
device: TrezorDevice, }
hidden: boolean, | {
}; type: typeof CONNECT.DEVICE_FROM_STORAGE,
payload: Array<TrezorDevice>,
}
| {
type: typeof CONNECT.START_ACQUIRING | typeof CONNECT.STOP_ACQUIRING,
}
| {
type: typeof CONNECT.REQUEST_WALLET_TYPE,
device: TrezorDevice,
}
| {
type: typeof CONNECT.RECEIVE_WALLET_TYPE | typeof CONNECT.UPDATE_WALLET_TYPE,
device: TrezorDevice,
hidden: boolean,
};
declare var LOCAL: ?string; declare var LOCAL: ?string;
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const init = (): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
// set listeners // set listeners
TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => { TrezorConnect.on(
// post event to reducers DEVICE_EVENT,
const type: DeviceMessageType = event.type; // eslint-disable-line prefer-destructuring (event: DeviceMessage): void => {
dispatch({ // post event to reducers
type, const type: DeviceMessageType = event.type; // eslint-disable-line prefer-destructuring
device: event.payload, dispatch({
}); type,
}); device: event.payload,
});
}
);
TrezorConnect.on(UI_EVENT, (event: UiMessage): void => { TrezorConnect.on(
// post event to reducers UI_EVENT,
const type: UiMessageType = event.type; // eslint-disable-line prefer-destructuring (event: UiMessage): void => {
dispatch({ // post event to reducers
type, const type: UiMessageType = event.type; // eslint-disable-line prefer-destructuring
payload: event.payload, dispatch({
}); type,
}); payload: event.payload,
});
}
);
TrezorConnect.on(TRANSPORT_EVENT, (event: TransportMessage): void => { TrezorConnect.on(
// post event to reducers TRANSPORT_EVENT,
const type: TransportMessageType = event.type; // eslint-disable-line prefer-destructuring (event: TransportMessage): void => {
dispatch({ // post event to reducers
type, const type: TransportMessageType = event.type; // eslint-disable-line prefer-destructuring
payload: event.payload, dispatch({
}); type,
}); payload: event.payload,
});
}
);
// post event to reducers // post event to reducers
TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainEvent): void => { TrezorConnect.on(
dispatch(event); BLOCKCHAIN_EVENT,
}); (event: BlockchainEvent): void => {
dispatch(event);
}
);
if (buildUtils.isDev()) { if (buildUtils.isDev()) {
window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; // eslint-disable-line no-underscore-dangle // eslint-disable-next-line
window.__TREZOR_CONNECT_SRC =
typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; // eslint-disable-line no-underscore-dangle
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://localhost:8088/'; // eslint-disable-line no-underscore-dangle // window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://localhost:8088/'; // eslint-disable-line no-underscore-dangle
window.TrezorConnect = TrezorConnect; window.TrezorConnect = TrezorConnect;
} }
@ -131,7 +166,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
debug: false, debug: false,
popup: false, popup: false,
webusb: true, webusb: true,
pendingTransportEvent: (getState().devices.length < 1), pendingTransportEvent: getState().devices.length < 1,
manifest: { manifest: {
email: 'info@trezor.io', email: 'info@trezor.io',
appUrl: urlConstants.NEXT_WALLET, appUrl: urlConstants.NEXT_WALLET,
@ -165,10 +200,18 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch): void => {
} }
}; };
export const requestWalletType = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const requestWalletType = (): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (!selected) return; if (!selected) return;
const isDeviceReady = selected.connected && selected.features && !selected.state && selected.mode === 'normal' && selected.firmware !== 'required'; const isDeviceReady =
selected.connected &&
selected.features &&
!selected.state &&
selected.mode === 'normal' &&
selected.firmware !== 'required';
if (!isDeviceReady) return; if (!isDeviceReady) return;
if (selected.features && selected.features.passphrase_protection) { if (selected.features && selected.features.passphrase_protection) {
@ -186,10 +229,18 @@ export const requestWalletType = (): AsyncAction => async (dispatch: Dispatch, g
} }
}; };
export const authorizeDevice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const authorizeDevice = (): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (!selected) return; if (!selected) return;
const isDeviceReady = selected.connected && selected.features && !selected.state && selected.mode === 'normal' && selected.firmware !== 'required'; const isDeviceReady =
selected.connected &&
selected.features &&
!selected.state &&
selected.mode === 'normal' &&
selected.firmware !== 'required';
if (!isDeviceReady) return; if (!isDeviceReady) return;
const response = await TrezorConnect.getDeviceState({ const response = await TrezorConnect.getDeviceState({
@ -233,11 +284,24 @@ export const authorizeDevice = (): AsyncAction => async (dispatch: Dispatch, get
} }
}; };
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> => {
if (device.features) { if (device.features) {
const instances = getState().devices.filter(d => d.features && device.features && d.state && !d.remember && d.features.device_id === device.features.device_id); const instances = getState().devices.filter(
d =>
d.features &&
device.features &&
d.state &&
!d.remember &&
d.features.device_id === device.features.device_id
);
if (instances.length > 0) { if (instances.length > 0) {
const isSelected = deviceUtils.isSelectedDevice(getState().wallet.selectedDevice, device); const isSelected = deviceUtils.isSelectedDevice(
getState().wallet.selectedDevice,
device
);
if (!isSelected && getState().modal.context !== CONTEXT_NONE) { if (!isSelected && getState().modal.context !== CONTEXT_NONE) {
dispatch({ dispatch({
type: CONNECT.FORGET_SILENT, type: CONNECT.FORGET_SILENT,
@ -259,8 +323,7 @@ export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch
}; };
export function reload(): AsyncAction { export function reload(): AsyncAction {
return async (): Promise<void> => { return async (): Promise<void> => {};
};
} }
export function acquire(): AsyncAction { export function acquire(): AsyncAction {
@ -314,7 +377,10 @@ export const forget = (device: TrezorDevice): Action => ({
device, device,
}); });
export const duplicateDeviceOld = (device: TrezorDevice): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const duplicateDeviceOld = (device: TrezorDevice): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const instance: number = getDuplicateInstanceNumber(getState().devices, device); const instance: number = getDuplicateInstanceNumber(getState().devices, device);
const extended: Object = { instance }; const extended: Object = { instance };
dispatch({ dispatch({
@ -323,7 +389,9 @@ export const duplicateDeviceOld = (device: TrezorDevice): AsyncAction => async (
}); });
}; };
export const duplicateDevice = (device: TrezorDevice): AsyncAction => async (dispatch: Dispatch): Promise<void> => { export const duplicateDevice = (device: TrezorDevice): AsyncAction => async (
dispatch: Dispatch
): Promise<void> => {
dispatch({ dispatch({
type: CONNECT.REQUEST_WALLET_TYPE, type: CONNECT.REQUEST_WALLET_TYPE,
device, device,

@ -7,27 +7,28 @@ import { toHex } from 'web3-utils'; // eslint-disable-line import/no-extraneous-
import { initWeb3 } from 'actions/Web3Actions'; import { initWeb3 } from 'actions/Web3Actions';
import * as ethUtils from 'utils/ethUtils'; import * as ethUtils from 'utils/ethUtils';
import type { import type { Dispatch, PromiseAction } from 'flowtype';
Dispatch,
PromiseAction,
} from 'flowtype';
import type { EthereumTransaction } from 'trezor-connect'; import type { EthereumTransaction } from 'trezor-connect';
import type { Token } from 'reducers/TokensReducer'; import type { Token } from 'reducers/TokensReducer';
type EthereumTxRequest = { type EthereumTxRequest = {
network: string; network: string,
token: ?Token; token: ?Token,
from: string; from: string,
to: string; to: string,
amount: string; amount: string,
data: string; data: string,
gasLimit: string; gasLimit: string,
gasPrice: string; gasPrice: string,
nonce: number; nonce: number,
} };
export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction<EthereumTransaction> => async (dispatch: Dispatch): Promise<EthereumTransaction> => { export const prepareEthereumTx = (
tx: EthereumTxRequest
): PromiseAction<EthereumTransaction> => async (
dispatch: Dispatch
): Promise<EthereumTransaction> => {
const instance = await dispatch(initWeb3(tx.network)); const instance = await dispatch(initWeb3(tx.network));
const { token } = tx; const { token } = tx;
let data: string = ethUtils.sanitizeHex(tx.data); let data: string = ethUtils.sanitizeHex(tx.data);
@ -37,7 +38,9 @@ export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction<Ethereum
// smart contract transaction // smart contract transaction
const contract = instance.erc20.clone(); const contract = instance.erc20.clone();
contract.options.address = token.address; contract.options.address = token.address;
const tokenAmount: string = new BigNumber(tx.amount).times(10 ** token.decimals).toString(10); const tokenAmount: string = new BigNumber(tx.amount)
.times(10 ** token.decimals)
.toString(10);
data = instance.erc20.methods.transfer(to, tokenAmount).encodeABI(); data = instance.erc20.methods.transfer(to, tokenAmount).encodeABI();
value = '0x00'; value = '0x00';
to = token.address; to = token.address;
@ -57,7 +60,9 @@ export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction<Ethereum
}; };
}; };
export const serializeEthereumTx = (tx: EthereumTransaction): PromiseAction<string> => async (): Promise<string> => { export const serializeEthereumTx = (
tx: EthereumTransaction
): PromiseAction<string> => async (): Promise<string> => {
const ethTx = new EthereumjsTx(tx); const ethTx = new EthereumjsTx(tx);
return `0x${ethTx.serialize().toString('hex')}`; return `0x${ethTx.serialize().toString('hex')}`;
}; };

@ -19,34 +19,46 @@ import type {
State, State,
} from 'flowtype'; } from 'flowtype';
export type WalletAction = { export type WalletAction =
type: typeof WALLET.SET_INITIAL_URL, | {
state?: RouterLocationState, type: typeof WALLET.SET_INITIAL_URL,
pathname?: string state?: RouterLocationState,
} | { pathname?: string,
type: typeof WALLET.TOGGLE_DEVICE_DROPDOWN, }
opened: boolean | {
} | { type: typeof WALLET.TOGGLE_DEVICE_DROPDOWN,
type: typeof WALLET.ONLINE_STATUS, opened: boolean,
online: boolean }
} | { | {
type: typeof WALLET.SET_SELECTED_DEVICE, type: typeof WALLET.ONLINE_STATUS,
device: ?TrezorDevice online: boolean,
} | { }
type: typeof WALLET.UPDATE_SELECTED_DEVICE, | {
device: TrezorDevice type: typeof WALLET.SET_SELECTED_DEVICE,
} | { device: ?TrezorDevice,
type: typeof WALLET.CLEAR_UNAVAILABLE_DEVICE_DATA, }
devices: Array<TrezorDevice> | {
} | { type: typeof WALLET.UPDATE_SELECTED_DEVICE,
type: typeof WALLET.SHOW_BETA_DISCLAIMER | typeof WALLET.HIDE_BETA_DISCLAIMER | typeof WALLET.SET_FIRST_LOCATION_CHANGE, device: TrezorDevice,
} | { }
type: typeof WALLET.TOGGLE_SIDEBAR, | {
} | { type: typeof WALLET.CLEAR_UNAVAILABLE_DEVICE_DATA,
type: typeof WALLET.SET_LANGUAGE, devices: Array<TrezorDevice>,
locale: string, }
messages: { [string]: string }, | {
} type:
| typeof WALLET.SHOW_BETA_DISCLAIMER
| typeof WALLET.HIDE_BETA_DISCLAIMER
| typeof WALLET.SET_FIRST_LOCATION_CHANGE,
}
| {
type: typeof WALLET.TOGGLE_SIDEBAR,
}
| {
type: typeof WALLET.SET_LANGUAGE,
locale: string,
messages: { [string]: string },
};
export const init = (): ThunkAction => (dispatch: Dispatch): void => { export const init = (): ThunkAction => (dispatch: Dispatch): void => {
const updateOnlineStatus = () => { const updateOnlineStatus = () => {
@ -75,7 +87,7 @@ export const toggleSidebar = (): WalletAction => ({
export const fetchLocale = (locale: string): ThunkAction => (dispatch: Dispatch): void => { export const fetchLocale = (locale: string): ThunkAction => (dispatch: Dispatch): void => {
fetch(`/l10n/${locale}.json`) fetch(`/l10n/${locale}.json`)
.then(response => response.json()) .then(response => response.json())
.then((messages) => { .then(messages => {
dispatch({ dispatch({
type: WALLET.SET_LANGUAGE, type: WALLET.SET_LANGUAGE,
locale, locale,
@ -89,12 +101,18 @@ export const fetchLocale = (locale: string): ThunkAction => (dispatch: Dispatch)
// all saved instances will be removed immediately inside DevicesReducer // all saved instances will be removed immediately inside DevicesReducer
// This method will clear leftovers associated with removed instances from reducers. // This method will clear leftovers associated with removed instances from reducers.
// (DiscoveryReducer, AccountReducer, TokensReducer) // (DiscoveryReducer, AccountReducer, TokensReducer)
export const clearUnavailableDevicesData = (prevState: State, device: Device): ThunkAction => (dispatch: Dispatch): void => { export const clearUnavailableDevicesData = (prevState: State, device: Device): ThunkAction => (
dispatch: Dispatch
): void => {
if (!device.features) return; if (!device.features) return;
const affectedDevices = prevState.devices.filter(d => d.features && device.features const affectedDevices = prevState.devices.filter(
&& d.features.device_id === device.features.device_id d =>
&& d.features.passphrase_protection !== device.features.passphrase_protection); d.features &&
device.features &&
d.features.device_id === device.features.device_id &&
d.features.passphrase_protection !== device.features.passphrase_protection
);
if (affectedDevices.length > 0) { if (affectedDevices.length > 0) {
dispatch({ dispatch({
@ -114,15 +132,21 @@ const actions = [
]; ];
/* /*
* Called from WalletService * Called from WalletService
*/ */
export const observe = (prevState: State, action: Action): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => { export const observe = (prevState: State, action: Action): PayloadAction<boolean> => (
dispatch: Dispatch,
getState: GetState
): boolean => {
// ignore not listed actions // ignore not listed actions
if (actions.indexOf(action.type) < 0) return false; if (actions.indexOf(action.type) < 0) return false;
const state: State = getState(); const state: State = getState();
const locationChanged = reducerUtils.observeChanges(prevState.router.location, state.router.location); const locationChanged = reducerUtils.observeChanges(
prevState.router.location,
state.router.location
);
const device = reducerUtils.getSelectedDevice(state); const device = reducerUtils.getSelectedDevice(state);
const selectedDeviceChanged = reducerUtils.observeChanges(state.wallet.selectedDevice, device); const selectedDeviceChanged = reducerUtils.observeChanges(state.wallet.selectedDevice, device);
@ -142,4 +166,4 @@ export const observe = (prevState: State, action: Action): PayloadAction<boolean
return true; return true;
} }
return false; return false;
}; };

@ -9,12 +9,7 @@ import * as WEB3 from 'actions/constants/web3';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as ethUtils from 'utils/ethUtils'; import * as ethUtils from 'utils/ethUtils';
import type { import type { Dispatch, GetState, ThunkAction, PromiseAction } from 'flowtype';
Dispatch,
GetState,
ThunkAction,
PromiseAction,
} from 'flowtype';
import type { EthereumAccount } from 'trezor-connect'; import type { EthereumAccount } from 'trezor-connect';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
@ -27,100 +22,114 @@ import * as AccountsActions from './AccountsActions';
export type Web3UpdateBlockAction = { export type Web3UpdateBlockAction = {
type: typeof WEB3.BLOCK_UPDATED, type: typeof WEB3.BLOCK_UPDATED,
network: string, network: string,
blockHash: string blockHash: string,
}; };
export type Web3UpdateGasPriceAction = { export type Web3UpdateGasPriceAction = {
type: typeof WEB3.GAS_PRICE_UPDATED, type: typeof WEB3.GAS_PRICE_UPDATED,
network: string, network: string,
gasPrice: string gasPrice: string,
}; };
export type Web3Action = { export type Web3Action =
type: typeof WEB3.READY, | {
} | { type: typeof WEB3.READY,
type: typeof WEB3.START, }
} | { | {
type: typeof WEB3.CREATE | typeof WEB3.DISCONNECT, type: typeof WEB3.START,
instance: Web3Instance }
} | Web3UpdateBlockAction | {
| Web3UpdateGasPriceAction; type: typeof WEB3.CREATE | typeof WEB3.DISCONNECT,
instance: Web3Instance,
export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<Web3Instance> => async (dispatch: Dispatch, getState: GetState): Promise<Web3Instance> => new Promise(async (resolve, reject) => { }
// check if requested web was initialized before | Web3UpdateBlockAction
const instance = getState().web3.find(w3 => w3.network === network); | Web3UpdateGasPriceAction;
if (instance && instance.web3.currentProvider.connected) {
resolve(instance); export const initWeb3 = (
return; network: string,
} urlIndex: number = 0
): PromiseAction<Web3Instance> => async (
// requested web3 wasn't initialized or is disconnected dispatch: Dispatch,
// initialize again getState: GetState
const { config, ERC20Abi } = getState().localStorage; ): Promise<Web3Instance> =>
const networkData = config.networks.find(c => c.shortcut === network); new Promise(async (resolve, reject) => {
if (!networkData) { // check if requested web was initialized before
// network not found const instance = getState().web3.find(w3 => w3.network === network);
reject(new Error(`Network ${network} not found in application config.`)); if (instance && instance.web3.currentProvider.connected) {
return; resolve(instance);
} return;
}
// get first url // requested web3 wasn't initialized or is disconnected
const url = networkData.web3[urlIndex]; // initialize again
if (!url) { const { config, ERC20Abi } = getState().localStorage;
reject(new Error('Web3 backend is not responding')); const networkData = config.networks.find(c => c.shortcut === network);
return; if (!networkData) {
} // network not found
reject(new Error(`Network ${network} not found in application config.`));
return;
}
const web3 = new Web3(new Web3.providers.WebsocketProvider(url)); // get first url
const url = networkData.web3[urlIndex];
if (!url) {
reject(new Error('Web3 backend is not responding'));
return;
}
const onConnect = async () => { const web3 = new Web3(new Web3.providers.WebsocketProvider(url));
const latestBlock = await web3.eth.getBlockNumber();
const gasPrice = await web3.eth.getGasPrice();
const newInstance = { const onConnect = async () => {
network, const latestBlock = await web3.eth.getBlockNumber();
web3, const gasPrice = await web3.eth.getGasPrice();
chainId: networkData.chainId,
erc20: new web3.eth.Contract(ERC20Abi),
latestBlock,
gasPrice,
};
dispatch({ const newInstance = {
type: WEB3.CREATE, network,
instance: newInstance, web3,
}); chainId: networkData.chainId,
erc20: new web3.eth.Contract(ERC20Abi),
latestBlock,
gasPrice,
};
resolve(newInstance); dispatch({
}; type: WEB3.CREATE,
instance: newInstance,
});
const onEnd = async () => { resolve(newInstance);
web3.currentProvider.reset(); };
const oldInstance = getState().web3.find(w3 => w3.network === network);
if (oldInstance && oldInstance.web3.currentProvider.connected) { const onEnd = async () => {
// backend disconnects web3.currentProvider.reset();
// dispatch({ const oldInstance = getState().web3.find(w3 => w3.network === network);
// type: 'WEB3.DISCONNECT',
// network if (oldInstance && oldInstance.web3.currentProvider.connected) {
// }); // backend disconnects
} else { // dispatch({
// backend initialization error for given url, try next one // type: 'WEB3.DISCONNECT',
try { // network
const otherWeb3 = await dispatch(initWeb3(network, urlIndex + 1)); // });
resolve(otherWeb3); } else {
} catch (error) { // backend initialization error for given url, try next one
reject(error); try {
const otherWeb3 = await dispatch(initWeb3(network, urlIndex + 1));
resolve(otherWeb3);
} catch (error) {
reject(error);
}
} }
} };
};
web3.currentProvider.on('connect', onConnect); web3.currentProvider.on('connect', onConnect);
web3.currentProvider.on('end', onEnd); web3.currentProvider.on('end', onEnd);
web3.currentProvider.on('error', onEnd); web3.currentProvider.on('error', onEnd);
}); });
export const discoverAccount = (descriptor: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => { export const discoverAccount = (
descriptor: string,
network: string
): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
const instance: Web3Instance = await dispatch(initWeb3(network)); const instance: Web3Instance = await dispatch(initWeb3(network));
const balance = await instance.web3.eth.getBalance(descriptor); const balance = await instance.web3.eth.getBalance(descriptor);
const nonce = await instance.web3.eth.getTransactionCount(descriptor); const nonce = await instance.web3.eth.getTransactionCount(descriptor);
@ -134,10 +143,13 @@ export const discoverAccount = (descriptor: string, network: string): PromiseAct
}; };
}; };
export const resolvePendingTransactions = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const resolvePendingTransactions = (network: string): PromiseAction<void> => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const instance: Web3Instance = await dispatch(initWeb3(network)); const instance: Web3Instance = await dispatch(initWeb3(network));
const pending = getState().pending.filter(p => p.network === network); const pending = getState().pending.filter(p => p.network === network);
pending.forEach(async (tx) => { pending.forEach(async tx => {
const status = await instance.web3.eth.getTransaction(tx.hash); const status = await instance.web3.eth.getTransaction(tx.hash);
if (!status) { if (!status) {
dispatch({ dispatch({
@ -192,39 +204,49 @@ export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch):
}; };
*/ */
export const updateAccount = (account: Account, newAccount: EthereumAccount, network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => { export const updateAccount = (
account: Account,
newAccount: EthereumAccount,
network: string
): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
const instance: Web3Instance = await dispatch(initWeb3(network)); const instance: Web3Instance = await dispatch(initWeb3(network));
const balance = await instance.web3.eth.getBalance(account.descriptor); const balance = await instance.web3.eth.getBalance(account.descriptor);
const nonce = await instance.web3.eth.getTransactionCount(account.descriptor); const nonce = await instance.web3.eth.getTransactionCount(account.descriptor);
dispatch(AccountsActions.update({ dispatch(
networkType: 'ethereum', AccountsActions.update({
...account, networkType: 'ethereum',
...newAccount, ...account,
nonce, ...newAccount,
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), nonce,
availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
})); availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
})
);
// update tokens for this account // update tokens for this account
dispatch(updateAccountTokens(account)); dispatch(updateAccountTokens(account));
}; };
export const updateAccountTokens = (account: Account): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const updateAccountTokens = (account: Account): PromiseAction<void> => async (
const tokens = getState().tokens.filter(t => t.network === account.network && t.ethAddress === account.descriptor); dispatch: Dispatch,
tokens.forEach(async (token) => { getState: GetState
): Promise<void> => {
const tokens = getState().tokens.filter(
t => t.network === account.network && t.ethAddress === account.descriptor
);
tokens.forEach(async token => {
const balance = await dispatch(getTokenBalance(token)); const balance = await dispatch(getTokenBalance(token));
// const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10); // const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10);
if (balance !== token.balance) { if (balance !== token.balance) {
dispatch(TokenActions.setBalance( dispatch(TokenActions.setBalance(token.address, token.ethAddress, balance));
token.address,
token.ethAddress,
balance,
));
} }
}); });
}; };
export const getTokenInfo = (address: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => { export const getTokenInfo = (
address: string,
network: string
): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => {
const instance: Web3Instance = await dispatch(initWeb3(network)); const instance: Web3Instance = await dispatch(initWeb3(network));
const contract = instance.erc20.clone(); const contract = instance.erc20.clone();
contract.options.address = address; contract.options.address = address;
@ -241,7 +263,9 @@ export const getTokenInfo = (address: string, network: string): PromiseAction<Ne
}; };
}; };
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => { export const getTokenBalance = (token: Token): PromiseAction<string> => async (
dispatch: Dispatch
): Promise<string> => {
const instance = await dispatch(initWeb3(token.network)); const instance = await dispatch(initWeb3(token.network));
const contract = instance.erc20.clone(); const contract = instance.erc20.clone();
contract.options.address = token.address; contract.options.address = token.address;
@ -250,7 +274,10 @@ export const getTokenBalance = (token: Token): PromiseAction<string> => async (d
return new BigNumber(balance).dividedBy(10 ** token.decimals).toString(10); return new BigNumber(balance).dividedBy(10 ** token.decimals).toString(10);
}; };
export const getCurrentGasPrice = (network: string): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => { export const getCurrentGasPrice = (network: string): PromiseAction<string> => async (
dispatch: Dispatch,
getState: GetState
): Promise<string> => {
const instance = getState().web3.find(w3 => w3.network === network); const instance = getState().web3.find(w3 => w3.network === network);
if (instance) { if (instance) {
return EthereumjsUnits.convert(instance.gasPrice, 'wei', 'gwei'); return EthereumjsUnits.convert(instance.gasPrice, 'wei', 'gwei');
@ -258,7 +285,9 @@ export const getCurrentGasPrice = (network: string): PromiseAction<string> => as
return '0'; return '0';
}; };
export const updateGasPrice = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => { export const updateGasPrice = (network: string): PromiseAction<void> => async (
dispatch: Dispatch
): Promise<void> => {
try { try {
const instance = await dispatch(initWeb3(network)); const instance = await dispatch(initWeb3(network));
const gasPrice = await instance.web3.eth.getGasPrice(); const gasPrice = await instance.web3.eth.getGasPrice();
@ -275,23 +304,32 @@ export const updateGasPrice = (network: string): PromiseAction<void> => async (d
} }
}; };
export const estimateGasLimit = (
export const estimateGasLimit = (network: string, $options: EstimateGasOptions): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => { network: string,
$options: EstimateGasOptions
): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
const instance = await dispatch(initWeb3(network)); const instance = await dispatch(initWeb3(network));
const data = ethUtils.sanitizeHex($options.data); const data = ethUtils.sanitizeHex($options.data);
const options = { const options = {
...$options, ...$options,
to: '0x0000000000000000000000000000000000000000', to: '0x0000000000000000000000000000000000000000',
data, data,
value: instance.web3.utils.toHex(EthereumjsUnits.convert($options.value || '0', 'ether', 'wei')), value: instance.web3.utils.toHex(
gasPrice: instance.web3.utils.toHex(EthereumjsUnits.convert($options.gasPrice, 'gwei', 'wei')), EthereumjsUnits.convert($options.value || '0', 'ether', 'wei')
),
gasPrice: instance.web3.utils.toHex(
EthereumjsUnits.convert($options.gasPrice, 'gwei', 'wei')
),
}; };
const limit = await instance.web3.eth.estimateGas(options); const limit = await instance.web3.eth.estimateGas(options);
return limit.toString(); return limit.toString();
}; };
export const disconnect = (network: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const disconnect = (network: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
// check if Web3 was already initialized // check if Web3 was already initialized
const instance = getState().web3.find(w3 => w3.network === network); const instance = getState().web3.find(w3 => w3.network === network);
if (instance) { if (instance) {

@ -1,13 +1,11 @@
/* @flow */ /* @flow */
//regExp1 : string = '(.*)' //regExp1 : string = '(.*)'
//regExp2 : '$1' = '$1' //regExp2 : '$1' = '$1'
export const READY: 'connect__ready' = 'connect__ready'; export const READY: 'connect__ready' = 'connect__ready';
export const INITIALIZATION_ERROR: 'connect__init_error' = 'connect__init_error'; export const INITIALIZATION_ERROR: 'connect__init_error' = 'connect__init_error';
export const DEVICE_FROM_STORAGE: 'connect__device_from_storage' = 'connect__device_from_storage'; export const DEVICE_FROM_STORAGE: 'connect__device_from_storage' = 'connect__device_from_storage';
export const AUTH_DEVICE: 'connect__auth_device' = 'connect__auth_device'; export const AUTH_DEVICE: 'connect__auth_device' = 'connect__auth_device';
export const NETWORK_CHANGED: 'connect__network_changed' = 'connect__network_changed'; export const NETWORK_CHANGED: 'connect__network_changed' = 'connect__network_changed';
@ -23,11 +21,12 @@ export const REMEMBER: 'connect__remember' = 'connect__remember';
export const TRY_TO_DUPLICATE: 'connect__try_to_duplicate' = 'connect__try_to_duplicate'; export const TRY_TO_DUPLICATE: 'connect__try_to_duplicate' = 'connect__try_to_duplicate';
export const DUPLICATE: 'connect__duplicate' = 'connect__duplicate'; export const DUPLICATE: 'connect__duplicate' = 'connect__duplicate';
export const DEVICE_STATE_EXCEPTION: 'connect__device_state_exception' = 'connect__device_state_exception'; export const DEVICE_STATE_EXCEPTION: 'connect__device_state_exception' =
'connect__device_state_exception';
export const START_ACQUIRING: 'connect__start_acquiring' = 'connect__start_acquiring'; export const START_ACQUIRING: 'connect__start_acquiring' = 'connect__start_acquiring';
export const STOP_ACQUIRING: 'connect__stop_acquiring' = 'connect__stop_acquiring'; export const STOP_ACQUIRING: 'connect__stop_acquiring' = 'connect__stop_acquiring';
export const REQUEST_WALLET_TYPE: 'connect__request_wallet_type' = 'connect__request_wallet_type'; export const REQUEST_WALLET_TYPE: 'connect__request_wallet_type' = 'connect__request_wallet_type';
export const RECEIVE_WALLET_TYPE: 'connect__receive_wallet_type' = 'connect__receive_wallet_type'; export const RECEIVE_WALLET_TYPE: 'connect__receive_wallet_type' = 'connect__receive_wallet_type';
export const UPDATE_WALLET_TYPE: 'connect__update_wallet_type' = 'connect__update_wallet_type'; export const UPDATE_WALLET_TYPE: 'connect__update_wallet_type' = 'connect__update_wallet_type';

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
export const INIT: 'account__init' = 'account__init'; export const INIT: 'account__init' = 'account__init';
export const DISPOSE: 'account__dispose' = 'account__dispose'; export const DISPOSE: 'account__dispose' = 'account__dispose';
@ -11,4 +10,5 @@ export const SET_BALANCE: 'account__set_balance' = 'account__set_balance';
export const SET_NONCE: 'account__set_nonce' = 'account__set_nonce'; export const SET_NONCE: 'account__set_nonce' = 'account__set_nonce';
export const FROM_STORAGE: 'account__from_storage' = 'account__from_storage'; export const FROM_STORAGE: 'account__from_storage' = 'account__from_storage';
export const UPDATE_SELECTED_ACCOUNT: 'account__update_selected_account' = 'account__update_selected_account'; export const UPDATE_SELECTED_ACCOUNT: 'account__update_selected_account' =
'account__update_selected_account';

@ -2,4 +2,4 @@
export const START_SUBSCRIBE: 'blockchain__start_subscribe' = 'blockchain__start_subscribe'; export const START_SUBSCRIBE: 'blockchain__start_subscribe' = 'blockchain__start_subscribe';
export const READY: 'blockchain__ready' = 'blockchain__ready'; export const READY: 'blockchain__ready' = 'blockchain__ready';
export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee'; export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee';

@ -1,11 +1,11 @@
/* @flow */ /* @flow */
export const START: 'discovery__start' = 'discovery__start'; export const START: 'discovery__start' = 'discovery__start';
export const STOP: 'discovery__stop' = 'discovery__stop'; export const STOP: 'discovery__stop' = 'discovery__stop';
export const FIRMWARE_NOT_SUPPORTED: 'discovery__fw_not_supported' = 'discovery__fw_not_supported'; export const FIRMWARE_NOT_SUPPORTED: 'discovery__fw_not_supported' = 'discovery__fw_not_supported';
export const FIRMWARE_OUTDATED: 'discovery__fw_outdated' = 'discovery__fw_outdated'; export const FIRMWARE_OUTDATED: 'discovery__fw_outdated' = 'discovery__fw_outdated';
export const COMPLETE: 'discovery__complete' = 'discovery__complete'; export const COMPLETE: 'discovery__complete' = 'discovery__complete';
export const WAITING_FOR_DEVICE: 'discovery__waiting_for_device' = 'discovery__waiting_for_device'; export const WAITING_FOR_DEVICE: 'discovery__waiting_for_device' = 'discovery__waiting_for_device';
export const WAITING_FOR_BLOCKCHAIN: 'discovery__waiting_for_blockchain' = 'discovery__waiting_for_blockchain'; export const WAITING_FOR_BLOCKCHAIN: 'discovery__waiting_for_blockchain' =
export const FROM_STORAGE: 'discovery__from_storage' = 'discovery__from_storage'; 'discovery__waiting_for_blockchain';
export const FROM_STORAGE: 'discovery__from_storage' = 'discovery__from_storage';

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
export const SAVE: 'storage__save' = 'storage__save'; export const SAVE: 'storage__save' = 'storage__save';
export const READY: 'storage__ready' = 'storage__ready'; export const READY: 'storage__ready' = 'storage__ready';
export const ERROR: 'storage__error' = 'storage__error'; export const ERROR: 'storage__error' = 'storage__error';

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
export const OPEN: 'log__open' = 'log__open'; export const OPEN: 'log__open' = 'log__open';
export const CLOSE: 'log__close' = 'log__close'; export const CLOSE: 'log__close' = 'log__close';
export const ADD: 'log__add' = 'log__add'; export const ADD: 'log__add' = 'log__add';

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
export const ADD: 'notification__add' = 'notification__add'; export const ADD: 'notification__add' = 'notification__add';
export const CLOSE: 'notification__close' = 'notification__close'; export const CLOSE: 'notification__close' = 'notification__close';
export const REMOVE: 'account__remove' = 'account__remove'; export const REMOVE: 'account__remove' = 'account__remove';

@ -1,8 +1,7 @@
/* @flow */ /* @flow */
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_REJECTED: 'pending__tx_rejected' = 'pending__tx_rejected'; 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';

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
export const INIT: 'receive__init' = 'receive__init'; export const INIT: 'receive__init' = 'receive__init';
export const DISPOSE: 'receive__dispose' = 'receive__dispose'; export const DISPOSE: 'receive__dispose' = 'receive__dispose';
export const REQUEST_UNVERIFIED: 'receive__request_unverified' = 'receive__request_unverified'; export const REQUEST_UNVERIFIED: 'receive__request_unverified' = 'receive__request_unverified';

@ -7,4 +7,4 @@ export const TX_SENDING: 'send__tx_sending' = 'send__tx_sending';
export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete'; export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete';
export const TX_ERROR: 'send__tx_error' = 'send__tx_error'; export const TX_ERROR: 'send__tx_error' = 'send__tx_error';
export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced'; export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced';
export const CLEAR: 'send__clear' = 'send__clear'; export const CLEAR: 'send__clear' = 'send__clear';

@ -4,4 +4,4 @@ export const INPUT_CHANGE: 'sign__verify__input__change' = 'sign__verify__input_
export const TOUCH: 'sign__verify__input__touch' = 'sign__verify__input__touch'; export const TOUCH: 'sign__verify__input__touch' = 'sign__verify__input__touch';
export const CLEAR_SIGN: 'sign__verify__sign__clear' = 'sign__verify__sign__clear'; export const CLEAR_SIGN: 'sign__verify__sign__clear' = 'sign__verify__sign__clear';
export const CLEAR_VERIFY: 'sign__verify__verify__clear' = 'sign__verify__verify__clear'; export const CLEAR_VERIFY: 'sign__verify__verify__clear' = 'sign__verify__verify__clear';
export const ERROR: 'sign__verify__error' = 'sign__verify__error'; export const ERROR: 'sign__verify__error' = 'sign__verify__error';

@ -1,6 +1,5 @@
/* @flow */ /* @flow */
export const INIT: 'summary__init' = 'summary__init'; export const INIT: 'summary__init' = 'summary__init';
export const DISPOSE: 'summary__dispose' = 'summary__dispose'; export const DISPOSE: 'summary__dispose' = 'summary__dispose';
export const ADD_TOKEN: 'summary__add_token' = 'summary__add_token'; export const ADD_TOKEN: 'summary__add_token' = 'summary__add_token';

@ -1,7 +1,6 @@
/* @flow */ /* @flow */
export const ADD: 'token__add' = 'token__add'; export const ADD: 'token__add' = 'token__add';
export const REMOVE: 'token__remove' = 'token__remove'; export const REMOVE: 'token__remove' = 'token__remove';
export const SET_BALANCE: 'token__set_balance' = 'token__set_balance'; export const SET_BALANCE: 'token__set_balance' = 'token__set_balance';
export const FROM_STORAGE: 'token__from_storage' = 'token__from_storage'; export const FROM_STORAGE: 'token__from_storage' = 'token__from_storage';

@ -2,15 +2,18 @@
export const TOGGLE_DEVICE_DROPDOWN: 'wallet__toggle_dropdown' = 'wallet__toggle_dropdown'; export const TOGGLE_DEVICE_DROPDOWN: 'wallet__toggle_dropdown' = 'wallet__toggle_dropdown';
export const SET_INITIAL_URL: 'wallet__set_initial_url' = 'wallet__set_initial_url'; export const SET_INITIAL_URL: 'wallet__set_initial_url' = 'wallet__set_initial_url';
export const SET_FIRST_LOCATION_CHANGE: 'wallet__set_first_location_change' = 'wallet__set_first_location_change'; export const SET_FIRST_LOCATION_CHANGE: 'wallet__set_first_location_change' =
'wallet__set_first_location_change';
export const ONLINE_STATUS: 'wallet__online_status' = 'wallet__online_status'; export const ONLINE_STATUS: 'wallet__online_status' = 'wallet__online_status';
export const SET_SELECTED_DEVICE: 'wallet__set_selected_device' = 'wallet__set_selected_device'; export const SET_SELECTED_DEVICE: 'wallet__set_selected_device' = 'wallet__set_selected_device';
export const UPDATE_SELECTED_DEVICE: 'wallet__update_selected_device' = 'wallet__update_selected_device'; export const UPDATE_SELECTED_DEVICE: 'wallet__update_selected_device' =
'wallet__update_selected_device';
export const SHOW_BETA_DISCLAIMER: 'wallet__show_beta_disclaimer' = 'wallet__show_beta_disclaimer'; export const SHOW_BETA_DISCLAIMER: 'wallet__show_beta_disclaimer' = 'wallet__show_beta_disclaimer';
export const HIDE_BETA_DISCLAIMER: 'wallet__hide_beta_disclaimer' = 'wallet__hide_beta_disclaimer'; export const HIDE_BETA_DISCLAIMER: 'wallet__hide_beta_disclaimer' = 'wallet__hide_beta_disclaimer';
export const CLEAR_UNAVAILABLE_DEVICE_DATA: 'wallet__clear_unavailable_device_data' = 'wallet__clear_unavailable_device_data'; export const CLEAR_UNAVAILABLE_DEVICE_DATA: 'wallet__clear_unavailable_device_data' =
'wallet__clear_unavailable_device_data';
export const TOGGLE_SIDEBAR: 'wallet__toggle_sidebar' = 'wallet__toggle_sidebar'; export const TOGGLE_SIDEBAR: 'wallet__toggle_sidebar' = 'wallet__toggle_sidebar';
export const SET_LANGUAGE: 'wallet__set_language' = 'wallet__set_language'; export const SET_LANGUAGE: 'wallet__set_language' = 'wallet__set_language';

@ -1,10 +1,9 @@
/* @flow */ /* @flow */
export const START: 'web3__start' = 'web3__start'; export const START: 'web3__start' = 'web3__start';
export const STOP: 'web3__stop' = 'web3__stop'; export const STOP: 'web3__stop' = 'web3__stop';
export const CREATE: 'web3__create' = 'web3__create'; export const CREATE: 'web3__create' = 'web3__create';
export const READY: 'web3__ready' = 'web3__ready'; export const READY: 'web3__ready' = 'web3__ready';
export const BLOCK_UPDATED: 'web3__block_updated' = 'web3__block_updated'; export const BLOCK_UPDATED: 'web3__block_updated' = 'web3__block_updated';
export const GAS_PRICE_UPDATED: 'web3__gas_price_updated' = 'web3__gas_price_updated'; export const GAS_PRICE_UPDATED: 'web3__gas_price_updated' = 'web3__gas_price_updated';
export const DISCONNECT: 'web3__disconnect' = 'web3__disconnect'; export const DISCONNECT: 'web3__disconnect' = 'web3__disconnect';

@ -4,19 +4,18 @@ import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import type { import type { TrezorDevice, Dispatch, GetState, PromiseAction } from 'flowtype';
TrezorDevice,
Dispatch,
GetState,
PromiseAction,
} from 'flowtype';
import type { EthereumAccount, BlockchainNotification } from 'trezor-connect'; import type { EthereumAccount, BlockchainNotification } from 'trezor-connect';
import type { Token } from 'reducers/TokensReducer'; import type { Token } from 'reducers/TokensReducer';
import type { NetworkToken } from 'reducers/LocalStorageReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer';
import * as Web3Actions from 'actions/Web3Actions'; import * as Web3Actions from 'actions/Web3Actions';
import * as AccountsActions from 'actions/AccountsActions'; import * as AccountsActions from 'actions/AccountsActions';
export const discoverAccount = (device: TrezorDevice, descriptor: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => { export const discoverAccount = (
device: TrezorDevice,
descriptor: string,
network: string
): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
// get data from connect // get data from connect
const txs = await TrezorConnect.ethereumGetAccountInfo({ const txs = await TrezorConnect.ethereumGetAccountInfo({
account: { account: {
@ -46,11 +45,18 @@ export const discoverAccount = (device: TrezorDevice, descriptor: string, networ
}; };
}; };
export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => dispatch(Web3Actions.getTokenInfo(input, network)); export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (
dispatch: Dispatch
): Promise<NetworkToken> => dispatch(Web3Actions.getTokenInfo(input, network));
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => dispatch(Web3Actions.getTokenBalance(token)); export const getTokenBalance = (token: Token): PromiseAction<string> => async (
dispatch: Dispatch
): Promise<string> => dispatch(Web3Actions.getTokenBalance(token));
export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAction<BigNumber> => async (dispatch: Dispatch): Promise<BigNumber> => { export const getGasPrice = (
network: string,
defaultGasPrice: number
): PromiseAction<BigNumber> => async (dispatch: Dispatch): Promise<BigNumber> => {
try { try {
const gasPrice = await dispatch(Web3Actions.getCurrentGasPrice(network)); const gasPrice = await dispatch(Web3Actions.getCurrentGasPrice(network));
return gasPrice === '0' ? new BigNumber(defaultGasPrice) : new BigNumber(gasPrice); return gasPrice === '0' ? new BigNumber(defaultGasPrice) : new BigNumber(gasPrice);
@ -60,7 +66,12 @@ export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAc
}; };
const estimateProxy: Array<Promise<string>> = []; const estimateProxy: Array<Promise<string>> = [];
export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => { export const estimateGasLimit = (
network: string,
data: string,
value: string,
gasPrice: string
): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
// Since this method could be called multiple times in short period of time // Since this method could be called multiple times in short period of time
// check for pending calls in proxy and if there more than two (first is current running and the second is waiting for result of first) // check for pending calls in proxy and if there more than two (first is current running and the second is waiting for result of first)
// TODO: should reject second call immediately? // TODO: should reject second call immediately?
@ -69,12 +80,14 @@ export const estimateGasLimit = (network: string, data: string, value: string, g
await estimateProxy[0]; await estimateProxy[0];
} }
const call = dispatch(Web3Actions.estimateGasLimit(network, { const call = dispatch(
to: '', Web3Actions.estimateGasLimit(network, {
data, to: '',
value, data,
gasPrice, value,
})); gasPrice,
})
);
// add current call to proxy // add current call to proxy
estimateProxy.push(call); estimateProxy.push(call);
// wait for result // wait for result
@ -85,8 +98,13 @@ export const estimateGasLimit = (network: string, data: string, value: string, g
return result; return result;
}; };
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const subscribe = (network: string): PromiseAction<void> => async (
const accounts: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.descriptor); // eslint-disable-line no-unused-vars dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const accounts: Array<string> = getState()
.accounts.filter(a => a.network === network)
.map(a => a.descriptor); // eslint-disable-line no-unused-vars
const response = await TrezorConnect.blockchainSubscribe({ const response = await TrezorConnect.blockchainSubscribe({
accounts, accounts,
coin: network, coin: network,
@ -96,7 +114,10 @@ export const subscribe = (network: string): PromiseAction<void> => async (dispat
await dispatch(Web3Actions.initWeb3(network)); await dispatch(Web3Actions.initWeb3(network));
}; };
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onBlockMined = (network: string): PromiseAction<void> => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
// TODO: handle rollback, // TODO: handle rollback,
// check latest saved transaction blockhash against blockhheight // check latest saved transaction blockhash against blockhheight
@ -131,7 +152,9 @@ export const onBlockMined = (network: string): PromiseAction<void> => async (dis
} }
}; };
export const onNotification = (payload: $ElementType<BlockchainNotification, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onNotification = (
payload: $ElementType<BlockchainNotification, 'payload'>
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { notification } = payload; const { notification } = payload;
const account = getState().accounts.find(a => a.descriptor === notification.descriptor); const account = getState().accounts.find(a => a.descriptor === notification.descriptor);
if (!account) return; if (!account) return;
@ -148,6 +171,8 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
} }
}; };
export const onError = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => { export const onError = (network: string): PromiseAction<void> => async (
dispatch: Dispatch
): Promise<void> => {
dispatch(Web3Actions.disconnect(network)); dispatch(Web3Actions.disconnect(network));
}; };

@ -5,14 +5,7 @@ import EthereumjsUtil from 'ethereumjs-util';
import * as DISCOVERY from 'actions/constants/discovery'; import * as DISCOVERY from 'actions/constants/discovery';
import * as BlockchainActions from 'actions/ethereum/BlockchainActions'; import * as BlockchainActions from 'actions/ethereum/BlockchainActions';
import type { import type { PromiseAction, Dispatch, GetState, TrezorDevice, Network, Account } from 'flowtype';
PromiseAction,
Dispatch,
GetState,
TrezorDevice,
Network,
Account,
} from 'flowtype';
import type { Discovery } from 'reducers/DiscoveryReducer'; import type { Discovery } from 'reducers/DiscoveryReducer';
export type DiscoveryStartAction = { export type DiscoveryStartAction = {
@ -28,7 +21,10 @@ export type DiscoveryStartAction = {
// first iteration // first iteration
// generate public key for this account // generate public key for this account
// start discovery process // start discovery process
export const begin = (device: TrezorDevice, network: Network): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => { export const begin = (
device: TrezorDevice,
network: Network
): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => {
// get xpub from TREZOR // get xpub from TREZOR
const response = await TrezorConnect.getPublicKey({ const response = await TrezorConnect.getPublicKey({
device: { device: {
@ -61,18 +57,26 @@ export const begin = (device: TrezorDevice, network: Network): PromiseAction<Dis
}; };
}; };
export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => { export const discoverAccount = (
device: TrezorDevice,
discoveryProcess: Discovery
): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => {
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network); const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
if (!network) throw new Error('Discovery network not found'); if (!network) throw new Error('Discovery network not found');
const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`); const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`);
const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex); const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex);
const publicAddress: string = EthereumjsUtil.publicToAddress(derivedKey.publicKey, true).toString('hex'); const publicAddress: string = EthereumjsUtil.publicToAddress(
derivedKey.publicKey,
true
).toString('hex');
const ethAddress: string = EthereumjsUtil.toChecksumAddress(publicAddress); const ethAddress: string = EthereumjsUtil.toChecksumAddress(publicAddress);
// TODO: check if address was created before // TODO: check if address was created before
const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network.shortcut)); const account = await dispatch(
BlockchainActions.discoverAccount(device, ethAddress, network.shortcut)
);
// const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0'; // const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0';
const empty = account.nonce <= 0 && account.balance === '0'; const empty = account.nonce <= 0 && account.balance === '0';
@ -95,4 +99,4 @@ export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discover
networkType: 'ethereum', networkType: 'ethereum',
nonce: account.nonce, nonce: account.nonce,
}; };
}; };

@ -36,9 +36,12 @@ const actions = [
]; ];
/* /*
* Called from WalletService * 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 // ignore not listed actions
if (actions.indexOf(action.type) < 0) return; if (actions.indexOf(action.type) < 0) return;
@ -68,14 +71,26 @@ 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
shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { shouldUpdate = reducerUtils.observeChanges(
account: ['balance', 'nonce', 'tokens'], prevState.selectedAccount,
}); currentState.selectedAccount,
if (shouldUpdate && currentState.sendFormEthereum.currency !== currentState.sendFormEthereum.networkSymbol) { {
account: ['balance', 'nonce', 'tokens'],
}
);
if (
shouldUpdate &&
currentState.sendFormEthereum.currency !== currentState.sendFormEthereum.networkSymbol
) {
// make sure that this token is added into account // make sure that this token is added into account
const { account, tokens } = getState().selectedAccount; const { account, tokens } = getState().selectedAccount;
if (!account) return; if (!account) return;
const token = reducerUtils.findToken(tokens, account.descriptor, currentState.sendFormEthereum.currency, account.deviceState); const token = reducerUtils.findToken(
tokens,
account.descriptor,
currentState.sendFormEthereum.currency,
account.deviceState
);
if (!token) { if (!token) {
// token not found, re-init form // token not found, re-init form
dispatch(init()); dispatch(init());
@ -85,7 +100,10 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
// check if "sendFormEthereum" reducer changed // check if "sendFormEthereum" reducer changed
if (!shouldUpdate) { if (!shouldUpdate) {
shouldUpdate = reducerUtils.observeChanges(prevState.sendFormEthereum, currentState.sendFormEthereum); shouldUpdate = reducerUtils.observeChanges(
prevState.sendFormEthereum,
currentState.sendFormEthereum
);
} }
if (shouldUpdate) { if (shouldUpdate) {
@ -99,15 +117,15 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
}; };
/* /*
* Called from "observe" action * Called from "observe" action
* Initialize "sendFormEthereum" reducer data * Initialize "sendFormEthereum" reducer data
* Get data either from session storage or "selectedAccount" reducer * Get data either from session storage or "selectedAccount" reducer
*/ */
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const init = (): AsyncAction => async (
const { dispatch: Dispatch,
account, getState: GetState
network, ): Promise<void> => {
} = getState().selectedAccount; const { account, network } = getState().selectedAccount;
if (!account || !network) return; if (!account || !network) return;
@ -122,10 +140,15 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
return; return;
} }
const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice)); const gasPrice: BigNumber = await dispatch(
BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice)
);
const gasLimit = network.defaultGasLimit.toString(); const gasLimit = network.defaultGasLimit.toString();
const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit); const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
initialState.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.INIT, type: SEND.INIT,
@ -145,17 +168,20 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
}; };
/* /*
* Called from UI from "advanced" button * Called from UI from "advanced" button
*/ */
export const toggleAdvanced = (): Action => ({ export const toggleAdvanced = (): Action => ({
type: SEND.TOGGLE_ADVANCED, type: SEND.TOGGLE_ADVANCED,
networkType: 'ethereum', networkType: 'ethereum',
}); });
/* /*
* Called from UI from "clear" button * Called from UI from "clear" button
*/ */
export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onClear = (): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
const { advanced } = getState().sendFormEthereum; const { advanced } = getState().sendFormEthereum;
@ -164,10 +190,15 @@ export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: G
// clear transaction draft from session storage // clear transaction draft from session storage
dispatch(SessionStorageActions.clear()); dispatch(SessionStorageActions.clear());
const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice)); const gasPrice: BigNumber = await dispatch(
BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice)
);
const gasLimit = network.defaultGasLimit.toString(); const gasLimit = network.defaultGasLimit.toString();
const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit); const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
initialState.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.CLEAR, type: SEND.CLEAR,
@ -188,9 +219,12 @@ export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: G
}; };
/* /*
* Called from UI on "address" field change * Called from UI on "address" field change
*/ */
export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onAddressChange = (address: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -205,9 +239,12 @@ export const onAddressChange = (address: string): ThunkAction => (dispatch: Disp
}; };
/* /*
* Called from UI on "amount" field change * Called from UI on "amount" field change
*/ */
export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onAmountChange = (amount: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state = getState().sendFormEthereum; const state = getState().sendFormEthereum;
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -223,22 +260,32 @@ export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispat
}; };
/* /*
* Called from UI on "currency" selection change * Called from UI on "currency" selection change
*/ */
export const onCurrencyChange = (currency: { value: string, label: string }): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onCurrencyChange = (currency: { value: string, label: string }): ThunkAction => (
const { dispatch: Dispatch,
account, getState: GetState
network, ): void => {
} = getState().selectedAccount; const { account, network } = getState().selectedAccount;
if (!account || !network) return; if (!account || !network) return;
const state = getState().sendFormEthereum; const state = getState().sendFormEthereum;
const isToken = currency.value !== state.networkSymbol; const isToken = currency.value !== state.networkSymbol;
const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); const gasLimit = isToken
? network.defaultGasLimitTokens.toString()
const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); : network.defaultGasLimit.toString();
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
const feeLevels = ValidationActions.getFeeLevels(
network.symbol,
state.recommendedGasPrice,
gasLimit,
state.selectedFeeLevel
);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
state.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -254,8 +301,8 @@ export const onCurrencyChange = (currency: { value: string, label: string }): Th
}; };
/* /*
* Called from UI from "set max" button * Called from UI from "set max" button
*/ */
export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const state = getState().sendFormEthereum; const state = getState().sendFormEthereum;
dispatch({ dispatch({
@ -271,9 +318,12 @@ export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetSta
}; };
/* /*
* Called from UI on "fee" selection change * Called from UI on "fee" selection change
*/ */
export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state = getState().sendFormEthereum; const state = getState().sendFormEthereum;
const isCustom = feeLevel.value === 'Custom'; const isCustom = feeLevel.value === 'Custom';
@ -292,7 +342,9 @@ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch:
} else { } else {
// corner case: gas limit was changed by user OR by "estimateGasPrice" action // corner case: gas limit was changed by user OR by "estimateGasPrice" action
// leave gasLimit as it is // leave gasLimit as it is
newGasLimit = state.touched.gasLimit ? state.gasLimit : network.defaultGasLimit.toString(); newGasLimit = state.touched.gasLimit
? state.gasLimit
: network.defaultGasLimit.toString();
} }
newGasPrice = feeLevel.gasPrice; newGasPrice = feeLevel.gasPrice;
} }
@ -311,18 +363,26 @@ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch:
}; };
/* /*
* Called from UI from "update recommended fees" button * Called from UI from "update recommended fees" button
*/ */
export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const updateFeeLevels = (): ThunkAction => (
const { dispatch: Dispatch,
account, getState: GetState
network, ): void => {
} = getState().selectedAccount; const { account, network } = getState().selectedAccount;
if (!account || !network) return; if (!account || !network) return;
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, state.gasLimit, state.selectedFeeLevel); const feeLevels = ValidationActions.getFeeLevels(
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); network.symbol,
state.recommendedGasPrice,
state.gasLimit,
state.selectedFeeLevel
);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
state.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -338,13 +398,17 @@ export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState:
}; };
/* /*
* Called from UI on "gas price" field change * Called from UI on "gas price" field change
*/ */
export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onGasPriceChange = (gasPrice: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
// switch to custom fee level // switch to custom fee level
let newSelectedFeeLevel = state.selectedFeeLevel; let newSelectedFeeLevel = state.selectedFeeLevel;
if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); if (state.selectedFeeLevel.value !== 'Custom')
newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom');
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -360,16 +424,27 @@ export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Di
}; };
/* /*
* Called from UI on "data" field change * Called from UI on "data" field change
* OR from "estimateGasPrice" action * OR from "estimateGasPrice" action
*/ */
export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onGasLimitChange = (gasLimit: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
if (!network) return; if (!network) return;
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
// recalculate feeLevels with recommended gasPrice // recalculate feeLevels with recommended gasPrice
const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); const feeLevels = ValidationActions.getFeeLevels(
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); network.symbol,
state.recommendedGasPrice,
gasLimit,
state.selectedFeeLevel
);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
state.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -387,9 +462,12 @@ export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Di
}; };
/* /*
* Called from UI on "nonce" field change * Called from UI on "nonce" field change
*/ */
export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onNonceChange = (nonce: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -404,9 +482,12 @@ export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch
}; };
/* /*
* Called from UI on "data" field change * Called from UI on "data" field change
*/ */
export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onDataChange = (data: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -423,13 +504,18 @@ export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch,
dispatch(estimateGasPrice()); dispatch(estimateGasPrice());
}; };
export const setDefaultGasLimit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const setDefaultGasLimit = (): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
if (!network) return; if (!network) return;
const isToken = state.currency !== state.networkSymbol; const isToken = state.currency !== state.networkSymbol;
const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); const gasLimit = isToken
? network.defaultGasLimitTokens.toString()
: network.defaultGasLimit.toString();
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -445,11 +531,14 @@ export const setDefaultGasLimit = (): ThunkAction => (dispatch: Dispatch, getSta
}; };
/* /*
* Internal method * Internal method
* Called from "onDataChange" action * Called from "onDataChange" action
* try to asynchronously download data from backend * try to asynchronously download data from backend
*/ */
const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { const estimateGasPrice = (): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
if (!network) { if (!network) {
@ -461,7 +550,11 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState:
const requestedData = state.data; const requestedData = state.data;
if (!ethUtils.isHex(requestedData)) { if (!ethUtils.isHex(requestedData)) {
// stop "calculatingGasLimit" process // stop "calculatingGasLimit" process
dispatch(onGasLimitChange(requestedData.length > 0 ? state.gasLimit : network.defaultGasLimit.toString())); dispatch(
onGasLimitChange(
requestedData.length > 0 ? state.gasLimit : network.defaultGasLimit.toString()
)
);
return; return;
} }
@ -471,7 +564,14 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState:
return; return;
} }
const gasLimit = await dispatch(BlockchainActions.estimateGasLimit(network.shortcut, state.data, state.amount, state.gasPrice)); const gasLimit = await dispatch(
BlockchainActions.estimateGasLimit(
network.shortcut,
state.data,
state.amount,
state.gasPrice
)
);
// double check "data" field // double check "data" field
// possible race condition when data changed before backend respond // possible race condition when data changed before backend respond
@ -481,14 +581,13 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState:
}; };
/* /*
* Called from UI from "send" button * Called from UI from "send" button
*/ */
export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onSend = (): AsyncAction => async (
const { dispatch: Dispatch,
account, getState: GetState
network, ): Promise<void> => {
pending, const { account, network, pending } = getState().selectedAccount;
} = getState().selectedAccount;
if (!account || account.networkType !== 'ethereum' || !network) return; if (!account || account.networkType !== 'ethereum' || !network) return;
@ -498,17 +597,26 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
const pendingNonce: number = reducerUtils.getPendingSequence(pending); const pendingNonce: number = reducerUtils.getPendingSequence(pending);
const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce; const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce;
const txData = await dispatch(prepareEthereumTx({ const txData = await dispatch(
network: network.shortcut, prepareEthereumTx({
token: isToken ? reducerUtils.findToken(getState().tokens, account.descriptor, currentState.currency, account.deviceState) : null, network: network.shortcut,
from: account.descriptor, token: isToken
to: currentState.address, ? reducerUtils.findToken(
amount: currentState.amount, getState().tokens,
data: currentState.data, account.descriptor,
gasLimit: currentState.gasLimit, currentState.currency,
gasPrice: currentState.gasPrice, account.deviceState
nonce, )
})); : null,
from: account.descriptor,
to: currentState.address,
amount: currentState.amount,
data: currentState.data,
gasLimit: currentState.gasLimit,
gasPrice: currentState.gasPrice,
nonce,
})
);
const selected: ?TrezorDevice = getState().wallet.selectedDevice; const selected: ?TrezorDevice = getState().wallet.selectedDevice;
if (!selected) return; if (!selected) return;
@ -586,22 +694,28 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
total: currentState.total, total: currentState.total,
sequence: nonce, sequence: nonce,
tokens: isToken ? [{ tokens: isToken
name: currentState.currency, ? [
shortcut: currentState.currency, {
value: currentState.amount, name: currentState.currency,
}] : undefined, shortcut: currentState.currency,
value: currentState.amount,
},
]
: undefined,
blockHeight: 0, blockHeight: 0,
blockHash: undefined, blockHash: undefined,
timestamp: undefined, timestamp: undefined,
}; };
dispatch(BlockchainActions.onNotification({ dispatch(
// $FlowIssue: missing coinInfo declaration BlockchainActions.onNotification({
coin: {}, // $FlowIssue: missing coinInfo declaration
notification: blockchainNotification, coin: {},
})); notification: blockchainNotification,
})
);
// workaround end // workaround end
// clear session storage // clear session storage
@ -615,7 +729,11 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
payload: { payload: {
type: 'success', type: 'success',
title: 'Transaction success', title: 'Transaction success',
message: <Link href={`${network.explorer.tx}${txid}`} isGreen>See transaction detail</Link>, message: (
<Link href={`${network.explorer.tx}${txid}`} isGreen>
See transaction detail
</Link>
),
cancelable: true, cancelable: true,
actions: [], actions: [],
}, },
@ -649,4 +767,4 @@ export default {
onDataChange, onDataChange,
onSend, onSend,
onClear, onClear,
}; };

@ -7,30 +7,33 @@ import { findDevice, getPendingAmount, findToken } from 'reducers/utils';
import * as SEND from 'actions/constants/send'; import * as SEND from 'actions/constants/send';
import * as ethUtils from 'utils/ethUtils'; import * as ethUtils from 'utils/ethUtils';
import type { import type { Dispatch, GetState, PayloadAction } from 'flowtype';
Dispatch,
GetState,
PayloadAction,
} from 'flowtype';
import type { State, FeeLevel } from 'reducers/SendFormEthereumReducer'; import type { State, FeeLevel } from 'reducers/SendFormEthereumReducer';
// general regular expressions // general regular expressions
const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$'); const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$');
const UPPERCASE_RE = new RegExp('^(.*[A-Z].*)$'); const UPPERCASE_RE = new RegExp('^(.*[A-Z].*)$');
const ABS_RE = new RegExp('^[0-9]+$'); const ABS_RE = new RegExp('^[0-9]+$');
const ETH_18_RE = new RegExp('^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$'); const ETH_18_RE = new RegExp(
'^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$'
);
const dynamicRegexp = (decimals: number): RegExp => { const dynamicRegexp = (decimals: number): RegExp => {
if (decimals > 0) { if (decimals > 0) {
return new RegExp(`^(0|0\\.([0-9]{0,${decimals}})?|[1-9][0-9]*\\.?([0-9]{0,${decimals}})?|\\.[0-9]{1,${decimals}})$`); return new RegExp(
`^(0|0\\.([0-9]{0,${decimals}})?|[1-9][0-9]*\\.?([0-9]{0,${decimals}})?|\\.[0-9]{1,${decimals}})$`
);
} }
return ABS_RE; return ABS_RE;
}; };
/* /*
* Called from SendFormActions.observe * Called from SendFormActions.observe
* Reaction for WEB3.GAS_PRICE_UPDATED action * Reaction for WEB3.GAS_PRICE_UPDATED action
*/ */
export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => { export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAction<void> => (
dispatch: Dispatch,
getState: GetState
): void => {
// testing random data // testing random data
// function getRandomInt(min, max) { // function getRandomInt(min, max) {
// return Math.floor(Math.random() * (max - min + 1)) + min; // return Math.floor(Math.random() * (max - min + 1)) + min;
@ -77,9 +80,12 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct
}; };
/* /*
* Recalculate amount, total and fees * Recalculate amount, total and fees
*/ */
export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const validation = (): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
// clone deep nested object // clone deep nested object
// to avoid overrides across state history // to avoid overrides across state history
let state: State = JSON.parse(JSON.stringify(getState().sendFormEthereum)); let state: State = JSON.parse(JSON.stringify(getState().sendFormEthereum));
@ -99,12 +105,11 @@ export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getSt
return state; return state;
}; };
export const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const recalculateTotalAmount = ($state: State): PayloadAction<State> => (
const { dispatch: Dispatch,
account, getState: GetState
tokens, ): State => {
pending, const { account, tokens, pending } = getState().selectedAccount;
} = getState().selectedAccount;
if (!account) return $state; if (!account) return $state;
const state = { ...$state }; const state = { ...$state };
@ -113,7 +118,12 @@ export const recalculateTotalAmount = ($state: State): PayloadAction<State> => (
if (state.setMax) { if (state.setMax) {
const pendingAmount = getPendingAmount(pending, state.currency, isToken); const pendingAmount = getPendingAmount(pending, state.currency, isToken);
if (isToken) { if (isToken) {
const token = findToken(tokens, account.descriptor, state.currency, account.deviceState); const token = findToken(
tokens,
account.descriptor,
state.currency,
account.deviceState
);
if (token) { if (token) {
state.amount = new BigNumber(token.balance).minus(pendingAmount).toFixed(); state.amount = new BigNumber(token.balance).minus(pendingAmount).toFixed();
} }
@ -140,8 +150,8 @@ export const updateCustomFeeLabel = ($state: State): PayloadAction<State> => ():
}; };
/* /*
* Address value validation * Address value validation
*/ */
export const addressValidation = ($state: State): PayloadAction<State> => (): State => { export const addressValidation = ($state: State): PayloadAction<State> => (): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.address) return state; if (!state.touched.address) return state;
@ -159,36 +169,52 @@ export const addressValidation = ($state: State): PayloadAction<State> => (): St
}; };
/* /*
* Address label assignation * Address label assignation
*/ */
export const addressLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const addressLabel = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.address || state.errors.address) return state; if (!state.touched.address || state.errors.address) return state;
const { const { account, network } = getState().selectedAccount;
account,
network,
} = getState().selectedAccount;
if (!account || !network) return state; if (!account || !network) return state;
const { address } = state; const { address } = state;
const savedAccounts = getState().accounts.filter(a => a.descriptor.toLowerCase() === address.toLowerCase()); const savedAccounts = getState().accounts.filter(
a => a.descriptor.toLowerCase() === address.toLowerCase()
);
if (savedAccounts.length > 0) { if (savedAccounts.length > 0) {
// check if found account belongs to this network // check if found account belongs to this network
const currentNetworkAccount = savedAccounts.find(a => a.network === network.shortcut); const currentNetworkAccount = savedAccounts.find(a => a.network === network.shortcut);
if (currentNetworkAccount) { if (currentNetworkAccount) {
const device = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState); const device = findDevice(
getState().devices,
currentNetworkAccount.deviceID,
currentNetworkAccount.deviceState
);
if (device) { if (device) {
state.infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`; state.infos.address = `${
device.instanceLabel
} Account #${currentNetworkAccount.index + 1}`;
} }
} else { } else {
// corner-case: the same derivation path is used on different networks // corner-case: the same derivation path is used on different networks
const otherNetworkAccount = savedAccounts[0]; const otherNetworkAccount = savedAccounts[0];
const device = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState); const device = findDevice(
getState().devices,
otherNetworkAccount.deviceID,
otherNetworkAccount.deviceState
);
const { networks } = getState().localStorage.config; const { networks } = getState().localStorage.config;
const otherNetwork = networks.find(c => c.shortcut === otherNetworkAccount.network); const otherNetwork = networks.find(c => c.shortcut === otherNetworkAccount.network);
if (device && otherNetwork) { if (device && otherNetwork) {
state.warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`; state.warnings.address = `Looks like it's ${
device.instanceLabel
} Account #${otherNetworkAccount.index + 1} address of ${
otherNetwork.name
} network`;
} }
} }
} }
@ -197,17 +223,16 @@ export const addressLabel = ($state: State): PayloadAction<State> => (dispatch:
}; };
/* /*
* Amount value validation * Amount value validation
*/ */
export const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const amountValidation = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.amount) return state; if (!state.touched.amount) return state;
const { const { account, tokens, pending } = getState().selectedAccount;
account,
tokens,
pending,
} = getState().selectedAccount;
if (!account) return state; if (!account) return state;
const { amount } = state; const { amount } = state;
@ -220,7 +245,12 @@ export const amountValidation = ($state: State): PayloadAction<State> => (dispat
const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, isToken); const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, isToken);
if (isToken) { if (isToken) {
const token = findToken(tokens, account.descriptor, state.currency, account.deviceState); const token = findToken(
tokens,
account.descriptor,
state.currency,
account.deviceState
);
if (!token) return state; if (!token) return state;
const decimalRegExp = dynamicRegexp(parseInt(token.decimals, 0)); const decimalRegExp = dynamicRegexp(parseInt(token.decimals, 0));
@ -228,14 +258,22 @@ export const amountValidation = ($state: State): PayloadAction<State> => (dispat
state.errors.amount = `Maximum ${token.decimals} decimals allowed`; state.errors.amount = `Maximum ${token.decimals} decimals allowed`;
} else if (new BigNumber(state.total).isGreaterThan(account.balance)) { } else if (new BigNumber(state.total).isGreaterThan(account.balance)) {
state.errors.amount = `Not enough ${state.networkSymbol} to cover transaction fee`; state.errors.amount = `Not enough ${state.networkSymbol} to cover transaction fee`;
} else if (new BigNumber(state.amount).isGreaterThan(new BigNumber(token.balance).minus(pendingAmount))) { } else if (
new BigNumber(state.amount).isGreaterThan(
new BigNumber(token.balance).minus(pendingAmount)
)
) {
state.errors.amount = 'Not enough funds'; state.errors.amount = 'Not enough funds';
} else if (new BigNumber(state.amount).isLessThanOrEqualTo('0')) { } else if (new BigNumber(state.amount).isLessThanOrEqualTo('0')) {
state.errors.amount = 'Amount is too low'; state.errors.amount = 'Amount is too low';
} }
} else if (!state.amount.match(ETH_18_RE)) { } else if (!state.amount.match(ETH_18_RE)) {
state.errors.amount = 'Maximum 18 decimals allowed'; state.errors.amount = 'Maximum 18 decimals allowed';
} else if (new BigNumber(state.total).isGreaterThan(new BigNumber(account.balance).minus(pendingAmount))) { } else if (
new BigNumber(state.total).isGreaterThan(
new BigNumber(account.balance).minus(pendingAmount)
)
) {
state.errors.amount = 'Not enough funds'; state.errors.amount = 'Not enough funds';
} }
} }
@ -243,15 +281,16 @@ export const amountValidation = ($state: State): PayloadAction<State> => (dispat
}; };
/* /*
* Gas limit value validation * Gas limit value validation
*/ */
export const gasLimitValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const gasLimitValidation = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.gasLimit) return state; if (!state.touched.gasLimit) return state;
const { const { network } = getState().selectedAccount;
network,
} = getState().selectedAccount;
if (!network) return state; if (!network) return state;
const { gasLimit } = state; const { gasLimit } = state;
@ -263,7 +302,13 @@ export const gasLimitValidation = ($state: State): PayloadAction<State> => (disp
const gl: BigNumber = new BigNumber(gasLimit); const gl: BigNumber = new BigNumber(gasLimit);
if (gl.isLessThan(1)) { if (gl.isLessThan(1)) {
state.errors.gasLimit = 'Gas limit is too low'; state.errors.gasLimit = 'Gas limit is too low';
} else if (gl.isLessThan(state.currency !== state.networkSymbol ? network.defaultGasLimitTokens : network.defaultGasLimit)) { } else if (
gl.isLessThan(
state.currency !== state.networkSymbol
? network.defaultGasLimitTokens
: network.defaultGasLimit
)
) {
state.warnings.gasLimit = 'Gas limit is below recommended'; state.warnings.gasLimit = 'Gas limit is below recommended';
} }
} }
@ -271,8 +316,8 @@ export const gasLimitValidation = ($state: State): PayloadAction<State> => (disp
}; };
/* /*
* Gas price value validation * Gas price value validation
*/ */
export const gasPriceValidation = ($state: State): PayloadAction<State> => (): State => { export const gasPriceValidation = ($state: State): PayloadAction<State> => (): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.gasPrice) return state; if (!state.touched.gasPrice) return state;
@ -294,15 +339,16 @@ export const gasPriceValidation = ($state: State): PayloadAction<State> => (): S
}; };
/* /*
* Nonce value validation * Nonce value validation
*/ */
export const nonceValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const nonceValidation = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.nonce) return state; if (!state.touched.nonce) return state;
const { const { account } = getState().selectedAccount;
account,
} = getState().selectedAccount;
if (!account || account.networkType !== 'ethereum') return state; if (!account || account.networkType !== 'ethereum') return state;
const { nonce } = state; const { nonce } = state;
@ -322,8 +368,8 @@ export const nonceValidation = ($state: State): PayloadAction<State> => (dispatc
}; };
/* /*
* Gas price value validation * Gas price value validation
*/ */
export const dataValidation = ($state: State): PayloadAction<State> => (): State => { export const dataValidation = ($state: State): PayloadAction<State> => (): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.data || state.data.length === 0) return state; if (!state.touched.data || state.data.length === 0) return state;
@ -334,12 +380,16 @@ export const dataValidation = ($state: State): PayloadAction<State> => (): State
}; };
/* /*
* UTILITIES * UTILITIES
*/ */
export const calculateFee = (gasPrice: string, gasLimit: string): string => { export const calculateFee = (gasPrice: string, gasLimit: string): string => {
try { try {
return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit).toFixed(), 'gwei', 'ether'); return EthereumjsUnits.convert(
new BigNumber(gasPrice).times(gasLimit).toFixed(),
'gwei',
'ether'
);
} catch (error) { } catch (error) {
return '0'; return '0';
} }
@ -358,7 +408,11 @@ export const calculateTotal = (amount: string, gasPrice: string, gasLimit: strin
} }
}; };
export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimit: string): string => { export const calculateMaxAmount = (
balance: BigNumber,
gasPrice: string,
gasLimit: string
): string => {
try { try {
// TODO - minus pendings // TODO - minus pendings
const fee = calculateFee(gasPrice, gasLimit); const fee = calculateFee(gasPrice, gasLimit);
@ -370,22 +424,30 @@ export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimi
} }
}; };
export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLimit: string, selected?: FeeLevel): Array<FeeLevel> => { export const getFeeLevels = (
symbol: string,
gasPrice: BigNumber | string,
gasLimit: string,
selected?: FeeLevel
): Array<FeeLevel> => {
const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice; const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice;
const quarter: BigNumber = price.dividedBy(4); const quarter: BigNumber = price.dividedBy(4);
const high: string = price.plus(quarter.times(2)).toFixed(); const high: string = price.plus(quarter.times(2)).toFixed();
const low: string = price.minus(quarter.times(2)).toFixed(); const low: string = price.minus(quarter.times(2)).toFixed();
const customLevel: FeeLevel = selected && selected.value === 'Custom' ? { const customLevel: FeeLevel =
value: 'Custom', selected && selected.value === 'Custom'
gasPrice: selected.gasPrice, ? {
// label: `${ calculateFee(gasPrice, gasLimit) } ${ symbol }` value: 'Custom',
label: `${calculateFee(selected.gasPrice, gasLimit)} ${symbol}`, gasPrice: selected.gasPrice,
} : { // label: `${ calculateFee(gasPrice, gasLimit) } ${ symbol }`
value: 'Custom', label: `${calculateFee(selected.gasPrice, gasLimit)} ${symbol}`,
gasPrice: low, }
label: '', : {
}; value: 'Custom',
gasPrice: low,
label: '',
};
return [ return [
{ {
@ -416,4 +478,4 @@ export const getSelectedFeeLevel = (feeLevels: Array<FeeLevel>, selected: FeeLev
selectedFeeLevel = feeLevels.find(f => f.value === 'Normal'); selectedFeeLevel = feeLevels.find(f => f.value === 'Normal');
} }
return selectedFeeLevel || selected; return selectedFeeLevel || selected;
}; };

@ -17,8 +17,13 @@ import type {
BlockchainFeeLevel, BlockchainFeeLevel,
} from 'flowtype'; } from 'flowtype';
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const subscribe = (network: string): PromiseAction<void> => async (
const accounts: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.descriptor); dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const accounts: Array<string> = getState()
.accounts.filter(a => a.network === network)
.map(a => a.descriptor);
await TrezorConnect.blockchainSubscribe({ await TrezorConnect.blockchainSubscribe({
accounts, accounts,
coin: network, coin: network,
@ -28,7 +33,10 @@ export const subscribe = (network: string): PromiseAction<void> => async (dispat
// Get current known fee // Get current known fee
// Use default values from appConfig.json if it wasn't downloaded from blockchain yet // Use default values from appConfig.json if it wasn't downloaded from blockchain yet
// update them later, after onBlockMined event // update them later, after onBlockMined event
export const getFeeLevels = (network: Network): PayloadAction<Array<BlockchainFeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<BlockchainFeeLevel> => { export const getFeeLevels = (network: Network): PayloadAction<Array<BlockchainFeeLevel>> => (
dispatch: Dispatch,
getState: GetState
): Array<BlockchainFeeLevel> => {
const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut); const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut);
if (!blockchain || blockchain.feeLevels.length < 1) { if (!blockchain || blockchain.feeLevels.length < 1) {
return network.fee.levels.map(level => ({ return network.fee.levels.map(level => ({
@ -39,7 +47,10 @@ export const getFeeLevels = (network: Network): PayloadAction<Array<BlockchainFe
return blockchain.feeLevels; return blockchain.feeLevels;
}; };
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onBlockMined = (network: string): PromiseAction<void> => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const blockchain = getState().blockchain.find(b => b.shortcut === network); const blockchain = getState().blockchain.find(b => b.shortcut === network);
if (!blockchain) return; // flowtype fallback if (!blockchain) return; // flowtype fallback
@ -69,9 +80,7 @@ export const onBlockMined = (network: string): PromiseAction<void> => async (dis
// level: 'transactions', // level: 'transactions',
// coin: network, // coin: network,
// }); // });
// if (!response.success) return; // if (!response.success) return;
// response.payload.forEach((a, i) => { // response.payload.forEach((a, i) => {
// if (a.transactions.length > 0) { // if (a.transactions.length > 0) {
// console.warn('APDEJTED!', a, i); // console.warn('APDEJTED!', a, i);
@ -87,7 +96,9 @@ export const onBlockMined = (network: string): PromiseAction<void> => async (dis
} }
}; };
export const onNotification = (payload: $ElementType<BlockchainNotification, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onNotification = (
payload: $ElementType<BlockchainNotification, 'payload'>
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { notification } = payload; const { notification } = payload;
const account = getState().accounts.find(a => a.descriptor === notification.descriptor); const account = getState().accounts.find(a => a.descriptor === notification.descriptor);
if (!account) return; if (!account) return;
@ -103,7 +114,10 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
network: account.network, network: account.network,
amount: toDecimalAmount(notification.amount, network.decimals), amount: toDecimalAmount(notification.amount, network.decimals),
total: notification.type === 'send' ? toDecimalAmount(notification.total, network.decimals) : toDecimalAmount(notification.amount, network.decimals), total:
notification.type === 'send'
? toDecimalAmount(notification.total, network.decimals)
: toDecimalAmount(notification.amount, network.decimals),
fee: toDecimalAmount(notification.fee, network.decimals), fee: toDecimalAmount(notification.fee, network.decimals),
}, },
}); });
@ -126,13 +140,18 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
}); });
if (!updatedAccount.success) return; if (!updatedAccount.success) return;
dispatch(AccountsActions.update({ dispatch(
networkType: 'ripple', AccountsActions.update({
...account, networkType: 'ripple',
balance: toDecimalAmount(updatedAccount.payload.balance, network.decimals), ...account,
availableBalance: toDecimalAmount(updatedAccount.payload.availableBalance, network.decimals), balance: toDecimalAmount(updatedAccount.payload.balance, network.decimals),
block: updatedAccount.payload.block, availableBalance: toDecimalAmount(
sequence: updatedAccount.payload.sequence, updatedAccount.payload.availableBalance,
reserve: '0', network.decimals
})); ),
block: updatedAccount.payload.block,
sequence: updatedAccount.payload.sequence,
reserve: '0',
})
);
}; };

@ -4,14 +4,7 @@ import TrezorConnect from 'trezor-connect';
import * as DISCOVERY from 'actions/constants/discovery'; import * as DISCOVERY from 'actions/constants/discovery';
import { toDecimalAmount } from 'utils/formatUtils'; import { toDecimalAmount } from 'utils/formatUtils';
import type { import type { PromiseAction, GetState, Dispatch, TrezorDevice, Network, Account } from 'flowtype';
PromiseAction,
GetState,
Dispatch,
TrezorDevice,
Network,
Account,
} from 'flowtype';
import type { Discovery } from 'reducers/DiscoveryReducer'; import type { Discovery } from 'reducers/DiscoveryReducer';
export type DiscoveryStartAction = { export type DiscoveryStartAction = {
@ -21,14 +14,20 @@ export type DiscoveryStartAction = {
device: TrezorDevice, device: TrezorDevice,
}; };
export const begin = (device: TrezorDevice, network: Network): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => ({ export const begin = (
device: TrezorDevice,
network: Network
): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => ({
type: DISCOVERY.START, type: DISCOVERY.START,
networkType: 'ripple', networkType: 'ripple',
network, network,
device, device,
}); });
export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => { export const discoverAccount = (
device: TrezorDevice,
discoveryProcess: Discovery
): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => {
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network); const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
if (!network) throw new Error('Discovery network not found'); if (!network) throw new Error('Discovery network not found');
@ -78,4 +77,4 @@ export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discover
sequence: account.sequence, sequence: account.sequence,
reserve: toDecimalAmount(account.reserve, network.decimals), reserve: toDecimalAmount(account.reserve, network.decimals),
}; };
}; };

@ -23,9 +23,12 @@ import * as BlockchainActions from './BlockchainActions';
import * as ValidationActions from './SendFormValidationActions'; import * as ValidationActions from './SendFormValidationActions';
/* /*
* Called from WalletService * 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 => {
const currentState = getState(); const currentState = getState();
// if action type is SEND.VALIDATION which is called as result of this process // if action type is SEND.VALIDATION which is called as result of this process
@ -50,13 +53,20 @@ 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
shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { shouldUpdate = reducerUtils.observeChanges(
account: ['balance', 'sequence'], prevState.selectedAccount,
}); currentState.selectedAccount,
{
account: ['balance', 'sequence'],
}
);
// check if "sendForm" reducer changed // check if "sendForm" reducer changed
if (!shouldUpdate) { if (!shouldUpdate) {
shouldUpdate = reducerUtils.observeChanges(prevState.sendFormRipple, currentState.sendFormRipple); shouldUpdate = reducerUtils.observeChanges(
prevState.sendFormRipple,
currentState.sendFormRipple
);
} }
if (shouldUpdate) { if (shouldUpdate) {
@ -70,15 +80,15 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
}; };
/* /*
* Called from "observe" action * Called from "observe" action
* Initialize "sendFormRipple" reducer data * Initialize "sendFormRipple" reducer data
* Get data either from session storage or "selectedAccount" reducer * Get data either from session storage or "selectedAccount" reducer
*/ */
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const init = (): AsyncAction => async (
const { dispatch: Dispatch,
account, getState: GetState
network, ): Promise<void> => {
} = getState().selectedAccount; const { account, network } = getState().selectedAccount;
if (!account || account.networkType !== 'ripple' || !network) return; if (!account || account.networkType !== 'ripple' || !network) return;
@ -94,7 +104,10 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network)); const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network));
const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels)); const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels));
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
initialState.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.INIT, type: SEND.INIT,
@ -112,17 +125,20 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
}; };
/* /*
* Called from UI from "advanced" button * Called from UI from "advanced" button
*/ */
export const toggleAdvanced = (): Action => ({ export const toggleAdvanced = (): Action => ({
type: SEND.TOGGLE_ADVANCED, type: SEND.TOGGLE_ADVANCED,
networkType: 'ripple', networkType: 'ripple',
}); });
/* /*
* Called from UI from "clear" button * Called from UI from "clear" button
*/ */
export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onClear = (): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
const { advanced } = getState().sendFormRipple; const { advanced } = getState().sendFormRipple;
@ -133,7 +149,10 @@ export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: G
const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network)); const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network));
const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels)); const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels));
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
initialState.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.CLEAR, type: SEND.CLEAR,
@ -152,9 +171,12 @@ export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: G
}; };
/* /*
* Called from UI on "address" field change * Called from UI on "address" field change
*/ */
export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onAddressChange = (address: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().sendFormRipple; const state: State = getState().sendFormRipple;
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -169,9 +191,12 @@ export const onAddressChange = (address: string): ThunkAction => (dispatch: Disp
}; };
/* /*
* Called from UI on "amount" field change * Called from UI on "amount" field change
*/ */
export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onAmountChange = (amount: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state = getState().sendFormRipple; const state = getState().sendFormRipple;
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -187,8 +212,8 @@ export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispat
}; };
/* /*
* Called from UI from "set max" button * Called from UI from "set max" button
*/ */
export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const state = getState().sendFormRipple; const state = getState().sendFormRipple;
dispatch({ dispatch({
@ -204,9 +229,12 @@ export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetSta
}; };
/* /*
* Called from UI on "fee" selection change * Called from UI on "fee" selection change
*/ */
export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state = getState().sendFormRipple; const state = getState().sendFormRipple;
const isCustom = feeLevel.value === 'Custom'; const isCustom = feeLevel.value === 'Custom';
@ -225,19 +253,24 @@ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch:
}; };
/* /*
* Called from UI from "update recommended fees" button * Called from UI from "update recommended fees" button
*/ */
export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const updateFeeLevels = (): ThunkAction => (
const { dispatch: Dispatch,
account, getState: GetState
network, ): void => {
} = getState().selectedAccount; const { account, network } = getState().selectedAccount;
if (!account || !network) return; if (!account || !network) return;
const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network)); const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network));
const state: State = getState().sendFormRipple; const state: State = getState().sendFormRipple;
const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels, state.selectedFeeLevel)); const feeLevels = dispatch(
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); ValidationActions.getFeeLevels(blockchainFeeLevels, state.selectedFeeLevel)
);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(
feeLevels,
state.selectedFeeLevel
);
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -252,16 +285,20 @@ export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState:
}; };
/* /*
* Called from UI on "advanced / fee" field change * Called from UI on "advanced / fee" field change
*/ */
export const onFeeChange = (fee: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onFeeChange = (fee: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
if (!network) return; if (!network) return;
const state: State = getState().sendFormRipple; const state: State = getState().sendFormRipple;
// switch to custom fee level // switch to custom fee level
let newSelectedFeeLevel = state.selectedFeeLevel; let newSelectedFeeLevel = state.selectedFeeLevel;
if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); if (state.selectedFeeLevel.value !== 'Custom')
newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom');
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -277,9 +314,12 @@ export const onFeeChange = (fee: string): ThunkAction => (dispatch: Dispatch, ge
}; };
/* /*
* Called from UI on "advanced / destination tag" field change * Called from UI on "advanced / destination tag" field change
*/ */
export const onDestinationTagChange = (destinationTag: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const onDestinationTagChange = (destinationTag: string): ThunkAction => (
dispatch: Dispatch,
getState: GetState
): void => {
const state: State = getState().sendFormRipple; const state: State = getState().sendFormRipple;
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
@ -294,13 +334,13 @@ export const onDestinationTagChange = (destinationTag: string): ThunkAction => (
}; };
/* /*
* Called from UI from "send" button * Called from UI from "send" button
*/ */
export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onSend = (): AsyncAction => async (
const { dispatch: Dispatch,
account, getState: GetState
network, ): Promise<void> => {
} = getState().selectedAccount; const { account, network } = getState().selectedAccount;
const selected: ?TrezorDevice = getState().wallet.selectedDevice; const selected: ?TrezorDevice = getState().wallet.selectedDevice;
if (!selected) return; if (!selected) return;
@ -401,4 +441,4 @@ export default {
onDestinationTagChange, onDestinationTagChange,
onSend, onSend,
onClear, onClear,
}; };

@ -21,10 +21,13 @@ const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?
const XRP_6_RE = new RegExp('^(0|0\\.([0-9]{0,6})?|[1-9][0-9]*\\.?([0-9]{0,6})?|\\.[0-9]{0,6})$'); const XRP_6_RE = new RegExp('^(0|0\\.([0-9]{0,6})?|[1-9][0-9]*\\.?([0-9]{0,6})?|\\.[0-9]{0,6})$');
/* /*
* Called from SendFormActions.observe * Called from SendFormActions.observe
* Reaction for BLOCKCHAIN.FEE_UPDATED action * Reaction for BLOCKCHAIN.FEE_UPDATED action
*/ */
export const onFeeUpdated = (network: string, feeLevels: Array<BlockchainFeeLevel>): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => { export const onFeeUpdated = (
network: string,
feeLevels: Array<BlockchainFeeLevel>
): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => {
const state = getState().sendFormRipple; const state = getState().sendFormRipple;
if (network === state.networkSymbol) return; if (network === state.networkSymbol) return;
@ -58,9 +61,12 @@ export const onFeeUpdated = (network: string, feeLevels: Array<BlockchainFeeLeve
}; };
/* /*
* Recalculate amount, total and fees * Recalculate amount, total and fees
*/ */
export const validation = (prevState: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const validation = (prevState: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
// clone deep nested object // clone deep nested object
// to avoid overrides across state history // to avoid overrides across state history
let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple)); let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple));
@ -81,12 +87,11 @@ export const validation = (prevState: State): PayloadAction<State> => (dispatch:
return state; return state;
}; };
const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { const recalculateTotalAmount = ($state: State): PayloadAction<State> => (
const { dispatch: Dispatch,
account, getState: GetState
network, ): State => {
pending, const { account, network, pending } = getState().selectedAccount;
} = getState().selectedAccount;
if (!account || account.networkType !== 'ripple' || !network) return $state; if (!account || account.networkType !== 'ripple' || !network) return $state;
const state = { ...$state }; const state = { ...$state };
@ -94,7 +99,9 @@ const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatc
if (state.setMax) { if (state.setMax) {
const pendingAmount = getPendingAmount(pending, state.networkSymbol, false); const pendingAmount = getPendingAmount(pending, state.networkSymbol, false);
const availableBalance = new BigNumber(account.balance).minus(account.reserve).minus(pendingAmount); const availableBalance = new BigNumber(account.balance)
.minus(account.reserve)
.minus(pendingAmount);
state.amount = calculateMaxAmount(availableBalance, fee); state.amount = calculateMaxAmount(availableBalance, fee);
} }
@ -102,7 +109,10 @@ const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatc
return state; return state;
}; };
const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
if (!network) return $state; // flowtype fallback if (!network) return $state; // flowtype fallback
@ -118,9 +128,12 @@ const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (dispatch:
}; };
/* /*
* Address value validation * Address value validation
*/ */
const addressValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { const addressValidation = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.address) return state; if (!state.touched.address) return state;
@ -140,10 +153,13 @@ const addressValidation = ($state: State): PayloadAction<State> => (dispatch: Di
}; };
/* /*
* Address balance validation * Address balance validation
* Fetch data from trezor-connect and set minimum required amount in reducer * Fetch data from trezor-connect and set minimum required amount in reducer
*/ */
const addressBalanceValidation = ($state: State): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { const addressBalanceValidation = ($state: State): PromiseAction<void> => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
if (!network) return; if (!network) return;
@ -177,36 +193,52 @@ const addressBalanceValidation = ($state: State): PromiseAction<void> => async (
}; };
/* /*
* Address label assignation * Address label assignation
*/ */
const addressLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { const addressLabel = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.address || state.errors.address) return state; if (!state.touched.address || state.errors.address) return state;
const { const { account, network } = getState().selectedAccount;
account,
network,
} = getState().selectedAccount;
if (!account || !network) return state; if (!account || !network) return state;
const { address } = state; const { address } = state;
const savedAccounts = getState().accounts.filter(a => a.descriptor.toLowerCase() === address.toLowerCase()); const savedAccounts = getState().accounts.filter(
a => a.descriptor.toLowerCase() === address.toLowerCase()
);
if (savedAccounts.length > 0) { if (savedAccounts.length > 0) {
// check if found account belongs to this network // check if found account belongs to this network
const currentNetworkAccount = savedAccounts.find(a => a.network === network.shortcut); const currentNetworkAccount = savedAccounts.find(a => a.network === network.shortcut);
if (currentNetworkAccount) { if (currentNetworkAccount) {
const device = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState); const device = findDevice(
getState().devices,
currentNetworkAccount.deviceID,
currentNetworkAccount.deviceState
);
if (device) { if (device) {
state.infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`; state.infos.address = `${
device.instanceLabel
} Account #${currentNetworkAccount.index + 1}`;
} }
} else { } else {
// corner-case: the same derivation path is used on different networks // corner-case: the same derivation path is used on different networks
const otherNetworkAccount = savedAccounts[0]; const otherNetworkAccount = savedAccounts[0];
const device = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState); const device = findDevice(
getState().devices,
otherNetworkAccount.deviceID,
otherNetworkAccount.deviceState
);
const { networks } = getState().localStorage.config; const { networks } = getState().localStorage.config;
const otherNetwork = networks.find(c => c.shortcut === otherNetworkAccount.network); const otherNetwork = networks.find(c => c.shortcut === otherNetworkAccount.network);
if (device && otherNetwork) { if (device && otherNetwork) {
state.warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`; state.warnings.address = `Looks like it's ${
device.instanceLabel
} Account #${otherNetworkAccount.index + 1} address of ${
otherNetwork.name
} network`;
} }
} }
} }
@ -215,16 +247,16 @@ const addressLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatc
}; };
/* /*
* Amount value validation * Amount value validation
*/ */
const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { const amountValidation = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.amount) return state; if (!state.touched.amount) return state;
const { const { account, pending } = getState().selectedAccount;
account,
pending,
} = getState().selectedAccount;
if (!account || account.networkType !== 'ripple') return state; if (!account || account.networkType !== 'ripple') return state;
const { amount } = state; const { amount } = state;
@ -236,32 +268,44 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol); const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol);
if (!state.amount.match(XRP_6_RE)) { if (!state.amount.match(XRP_6_RE)) {
state.errors.amount = 'Maximum 6 decimals allowed'; state.errors.amount = 'Maximum 6 decimals allowed';
} else if (new BigNumber(state.total).isGreaterThan(new BigNumber(account.balance).minus(pendingAmount))) { } else if (
new BigNumber(state.total).isGreaterThan(
new BigNumber(account.balance).minus(pendingAmount)
)
) {
state.errors.amount = 'Not enough funds'; state.errors.amount = 'Not enough funds';
} }
} }
if (!state.errors.amount && new BigNumber(account.balance).minus(state.total).lt(account.reserve)) { if (
state.errors.amount = `Not enough funds. Reserved amount for this account is ${account.reserve} ${state.networkSymbol}`; !state.errors.amount &&
new BigNumber(account.balance).minus(state.total).lt(account.reserve)
) {
state.errors.amount = `Not enough funds. Reserved amount for this account is ${
account.reserve
} ${state.networkSymbol}`;
} }
if (!state.errors.amount && new BigNumber(state.amount).lt(state.minAmount)) { if (!state.errors.amount && new BigNumber(state.amount).lt(state.minAmount)) {
state.errors.amount = `Amount is too low. Minimum amount for creating a new account is ${state.minAmount} ${state.networkSymbol}`; state.errors.amount = `Amount is too low. Minimum amount for creating a new account is ${
state.minAmount
} ${state.networkSymbol}`;
} }
return state; return state;
}; };
/* /*
* Fee value validation * Fee value validation
*/ */
export const feeValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const feeValidation = ($state: State): PayloadAction<State> => (
dispatch: Dispatch,
getState: GetState
): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.fee) return state; if (!state.touched.fee) return state;
const { const { network } = getState().selectedAccount;
network,
} = getState().selectedAccount;
if (!network) return state; if (!network) return state;
const { fee } = state; const { fee } = state;
@ -280,8 +324,8 @@ export const feeValidation = ($state: State): PayloadAction<State> => (dispatch:
return state; return state;
}; };
/* /*
* Destination Tag value validation * Destination Tag value validation
*/ */
export const destinationTagValidation = ($state: State): PayloadAction<State> => (): State => { export const destinationTagValidation = ($state: State): PayloadAction<State> => (): State => {
const state = { ...$state }; const state = { ...$state };
if (!state.touched.destinationTag) return state; if (!state.touched.destinationTag) return state;
@ -293,10 +337,9 @@ export const destinationTagValidation = ($state: State): PayloadAction<State> =>
return state; return state;
}; };
/* /*
* UTILITIES * UTILITIES
*/ */
const calculateTotal = (amount: string, fee: string): string => { const calculateTotal = (amount: string, fee: string): string => {
try { try {
@ -323,7 +366,10 @@ const calculateMaxAmount = (balance: BigNumber, fee: string): string => {
}; };
// Generate FeeLevel dataset for "fee" select // Generate FeeLevel dataset for "fee" select
export const getFeeLevels = (feeLevels: Array<BlockchainFeeLevel>, selected?: FeeLevel): PayloadAction<Array<FeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<FeeLevel> => { export const getFeeLevels = (
feeLevels: Array<BlockchainFeeLevel>,
selected?: FeeLevel
): PayloadAction<Array<FeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<FeeLevel> => {
const { network } = getState().selectedAccount; const { network } = getState().selectedAccount;
if (!network) return []; // flowtype fallback if (!network) return []; // flowtype fallback
@ -335,15 +381,18 @@ export const getFeeLevels = (feeLevels: Array<BlockchainFeeLevel>, selected?: Fe
})); }));
// add "Custom" level // add "Custom" level
const customLevel = selected && selected.value === 'Custom' ? { const customLevel =
value: 'Custom', selected && selected.value === 'Custom'
fee: selected.fee, ? {
label: `${toDecimalAmount(selected.fee, network.decimals)} ${network.symbol}`, value: 'Custom',
} : { fee: selected.fee,
value: 'Custom', label: `${toDecimalAmount(selected.fee, network.decimals)} ${network.symbol}`,
fee: '0', }
label: '', : {
}; value: 'Custom',
fee: '0',
label: '',
};
return levels.concat([customLevel]); return levels.concat([customLevel]);
}; };

@ -3,7 +3,6 @@ import styled, { css } from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FADE_IN } from 'config/animations'; import { FADE_IN } from 'config/animations';
const StyledBackdrop = styled.div` const StyledBackdrop = styled.div`
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -11,21 +10,17 @@ const StyledBackdrop = styled.div`
z-index: 100; z-index: 100;
left: 0; left: 0;
top: 0; top: 0;
background-color: rgba(0,0,0,0.5); background-color: rgba(0, 0, 0, 0.5);
${props => props.animated && css` ${props =>
animation: ${FADE_IN} 0.3s; props.animated &&
`}; css`
animation: ${FADE_IN} 0.3s;
`};
`; `;
const Backdrop = ({ const Backdrop = ({ className, show, animated, onClick }) =>
className, show ? <StyledBackdrop className={className} animated={animated} onClick={onClick} /> : null;
show,
animated,
onClick,
}) => (
show ? <StyledBackdrop className={className} animated={animated} onClick={onClick} /> : null
);
Backdrop.propTypes = { Backdrop.propTypes = {
show: PropTypes.bool, show: PropTypes.bool,

@ -17,8 +17,8 @@ type Props = {
isWhite?: boolean, isWhite?: boolean,
isWebUsb?: boolean, isWebUsb?: boolean,
isTransparent?: boolean, isTransparent?: boolean,
dataTest?: string dataTest?: string,
} };
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')};
@ -43,97 +43,105 @@ const Wrapper = styled.button`
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW}; box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
} }
${props => props.isDisabled && css` ${props =>
pointer-events: none; props.isDisabled &&
color: ${colors.TEXT_SECONDARY}; css`
background: ${colors.GRAY_LIGHT}; pointer-events: none;
`} color: ${colors.TEXT_SECONDARY};
background: ${colors.GRAY_LIGHT};
${props => props.isWhite && css` `}
background: ${colors.WHITE};
color: ${colors.TEXT_SECONDARY}; ${props =>
border: 1px solid ${colors.DIVIDER}; props.isWhite &&
css`
&:focus { background: ${colors.WHITE};
border-color: ${colors.INPUT_FOCUSED_BORDER}; color: ${colors.TEXT_SECONDARY};
} border: 1px solid ${colors.DIVIDER};
&:hover { &:focus {
color: ${colors.TEXT_PRIMARY}; border-color: ${colors.INPUT_FOCUSED_BORDER};
background: ${colors.DIVIDER}; }
}
&:hover {
&:active { color: ${colors.TEXT_PRIMARY};
color: ${colors.TEXT_PRIMARY}; background: ${colors.DIVIDER};
background: ${colors.DIVIDER}; }
}
`} &:active {
color: ${colors.TEXT_PRIMARY};
${props => props.isTransparent && css` background: ${colors.DIVIDER};
background: transparent; }
border: 0px; `}
color: ${colors.TEXT_SECONDARY};
${props =>
&:focus { props.isTransparent &&
color: ${colors.TEXT_PRIMARY}; css`
box-shadow: none;
}
&:hover,
&:active {
color: ${colors.TEXT_PRIMARY};
background: transparent; background: transparent;
} border: 0px;
`} color: ${colors.TEXT_SECONDARY};
${props => props.isWebUsb && css`
position: relative;
padding: 12px 24px 12px 40px;
background: transparent;
color: ${colors.GREEN_PRIMARY};
border: 1px solid ${colors.GREEN_PRIMARY};
transition: ${TRANSITION.HOVER};
&:before,
&:after {
content: '';
position: absolute;
background: ${colors.GREEN_PRIMARY};
top: 0;
bottom: 0;
margin: auto;
transition: ${TRANSITION.HOVER};
}
&:before { &:focus {
width: 12px; color: ${colors.TEXT_PRIMARY};
height: 2px; box-shadow: none;
left: 18px; }
}
&:after { &:hover,
width: 2px; &:active {
height: 12px; color: ${colors.TEXT_PRIMARY};
left: 23px; background: transparent;
} }
`}
&:hover { ${props =>
background: ${colors.GREEN_PRIMARY}; props.isWebUsb &&
color: ${colors.WHITE}; css`
position: relative;
padding: 12px 24px 12px 40px;
background: transparent;
color: ${colors.GREEN_PRIMARY};
border: 1px solid ${colors.GREEN_PRIMARY};
transition: ${TRANSITION.HOVER};
&:before, &:before,
&:after { &:after {
background: ${colors.WHITE}; content: '';
position: absolute;
background: ${colors.GREEN_PRIMARY};
top: 0;
bottom: 0;
margin: auto;
transition: ${TRANSITION.HOVER};
}
&:before {
width: 12px;
height: 2px;
left: 18px;
}
&:after {
width: 2px;
height: 12px;
left: 23px;
}
&:hover {
background: ${colors.GREEN_PRIMARY};
color: ${colors.WHITE};
&:before,
&:after {
background: ${colors.WHITE};
}
}
iframe {
position: absolute;
top: 0;
left: 0;
z-index: 1;
} }
} `}
iframe {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
`}
`; `;
const Button = ({ const Button = ({
@ -182,4 +190,4 @@ Button.propTypes = {
dataTest: PropTypes.string, dataTest: PropTypes.string,
}; };
export default Button; export default Button;

@ -12,7 +12,7 @@ type Props = {
onClick: (event: KeyboardEvent) => void, onClick: (event: KeyboardEvent) => void,
isChecked: boolean, isChecked: boolean,
children: React.Node, children: React.Node,
} };
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
@ -26,8 +26,7 @@ const Wrapper = styled.div`
} }
`; `;
const Tick = styled.div` const Tick = styled.div``;
`;
const IconWrapper = styled.div` const IconWrapper = styled.div`
display: flex; display: flex;
@ -42,9 +41,11 @@ const IconWrapper = styled.div`
&:hover, &:hover,
&:focus { &:focus {
${props => !props.isChecked && css` ${props =>
border: 1px solid ${colors.GREEN_PRIMARY}; !props.isChecked &&
`} css`
border: 1px solid ${colors.GREEN_PRIMARY};
`}
background: ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.WHITE)}; background: ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.WHITE)};
} }
`; `;
@ -70,17 +71,9 @@ class Checkbox extends React.PureComponent<Props> {
} }
render() { render() {
const { const { isChecked, children, onClick } = this.props;
isChecked,
children,
onClick,
} = this.props;
return ( return (
<Wrapper <Wrapper onClick={onClick} onKeyUp={event => this.handleKeyboard(event)} tabIndex={0}>
onClick={onClick}
onKeyUp={event => this.handleKeyboard(event)}
tabIndex={0}
>
<IconWrapper isChecked={isChecked}> <IconWrapper isChecked={isChecked}>
{isChecked && ( {isChecked && (
<Tick> <Tick>
@ -91,8 +84,7 @@ class Checkbox extends React.PureComponent<Props> {
icon={icons.SUCCESS} icon={icons.SUCCESS}
/> />
</Tick> </Tick>
) )}
}
</IconWrapper> </IconWrapper>
<Label isChecked={isChecked}>{children}</Label> <Label isChecked={isChecked}>{children}</Label>
</Wrapper> </Wrapper>

@ -2,12 +2,7 @@
import React from 'react'; 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 { import { getStatusColor, getStatusName, getStatus, getVersion } from 'utils/device';
getStatusColor,
getStatusName,
getStatus,
getVersion,
} from 'utils/device';
import TrezorImage from 'components/images/TrezorImage'; import TrezorImage from 'components/images/TrezorImage';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables'; import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
@ -27,19 +22,26 @@ const Wrapper = styled.div`
border-radius: 4px 0 0 0; border-radius: 4px 0 0 0;
box-shadow: ${props => (props.disabled ? 'none' : '0 3px 8px rgba(0, 0, 0, 0.04)')}; box-shadow: ${props => (props.disabled ? 'none' : '0 3px 8px rgba(0, 0, 0, 0.04)')};
${props => (props.isOpen || !props.isSelected) && css` ${props =>
box-shadow: none; (props.isOpen || !props.isSelected) &&
`} css`
box-shadow: none;
`}
${props => props.disabled && css` ${props =>
cursor: default; props.disabled &&
`} css`
cursor: default;
`}
${props => props.isHoverable && !props.disabled && css` ${props =>
&:hover { props.isHoverable &&
background: ${colors.GRAY_LIGHT}; !props.disabled &&
} css`
`} &:hover {
background: ${colors.GRAY_LIGHT};
}
`}
`; `;
const LabelWrapper = styled.div` const LabelWrapper = styled.div`
@ -88,7 +90,6 @@ const Dot = styled.div`
height: 10px; height: 10px;
`; `;
const DeviceHeader = ({ const DeviceHeader = ({
isOpen, isOpen,
icon, icon,
@ -120,9 +121,7 @@ const DeviceHeader = ({
<Name>{device.instanceLabel}</Name> <Name>{device.instanceLabel}</Name>
<Status title={getStatusName(status)}>{getStatusName(status)}</Status> <Status title={getStatusName(status)}>{getStatusName(status)}</Status>
</LabelWrapper> </LabelWrapper>
<IconWrapper> <IconWrapper>{icon && !disabled && isAccessible && icon}</IconWrapper>
{icon && !disabled && isAccessible && icon}
</IconWrapper>
</Wrapper> </Wrapper>
); );
}; };

@ -9,7 +9,6 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE } from 'config/variables'; import { FONT_SIZE } from 'config/variables';
import * as LogActions from 'actions/LogActions'; import * as LogActions from 'actions/LogActions';
@ -19,7 +18,7 @@ type Props = {
opened: boolean, opened: boolean,
isLanding: boolean, isLanding: boolean,
toggle: () => any, toggle: () => any,
} };
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@ -62,17 +61,27 @@ const Footer = ({ opened, toggle, isLanding }: Props) => (
<Wrapper> <Wrapper>
<Left> <Left>
<Copy>&copy; {getYear(new Date())}</Copy> <Copy>&copy; {getYear(new Date())}</Copy>
<StyledLink href="http://satoshilabs.com" isGreen>SatoshiLabs</StyledLink> <StyledLink href="http://satoshilabs.com" isGreen>
SatoshiLabs
</StyledLink>
<StyledLink href="https://trezor.io/static/pdf/tos.pdf" isGreen> <StyledLink href="https://trezor.io/static/pdf/tos.pdf" isGreen>
<FormattedMessage {...l10nMessages.TR_TERMS} /> <FormattedMessage {...l10nMessages.TR_TERMS} />
</StyledLink> </StyledLink>
<StyledLink onClick={toggle} isGreen>{ opened ? 'Hide Log' : 'Show Log' }</StyledLink> <StyledLink onClick={toggle} isGreen>
{opened ? 'Hide Log' : 'Show Log'}
</StyledLink>
</Left> </Left>
{!isLanding && ( {!isLanding && (
<Right> <Right>
<FormattedMessage <FormattedMessage
{...l10nMessages.TR_EXCHANGE_RATES_BY} {...l10nMessages.TR_EXCHANGE_RATES_BY}
values={{ service: <Link href="https://www.coingecko.com" isGreen>Coingecko</Link> }} values={{
service: (
<Link href="https://www.coingecko.com" isGreen>
Coingecko
</Link>
),
}}
/> />
</Right> </Right>
)} )}
@ -93,4 +102,7 @@ const mapDispatchToProps = dispatch => ({
toggle: bindActionCreators(LogActions.toggle, dispatch), toggle: bindActionCreators(LogActions.toggle, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(Footer); export default connect(
mapStateToProps,
mapDispatchToProps
)(Footer);

@ -14,4 +14,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -11,26 +11,31 @@ import LanguagePicker from './index';
type StateProps = { type StateProps = {
language: string, language: string,
} };
type DispatchProps = { type DispatchProps = {
fetchLocale: typeof WalletActions.fetchLocale, fetchLocale: typeof WalletActions.fetchLocale,
}; };
type OwnProps = { type OwnProps = {};
}
export type Props = StateProps & DispatchProps; export type Props = StateProps & DispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (
state: State
): StateProps => ({
language: state.wallet.language, language: state.wallet.language,
}); });
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (
dispatch: Dispatch
): DispatchProps => ({
fetchLocale: bindActionCreators(WalletActions.fetchLocale, dispatch), fetchLocale: bindActionCreators(WalletActions.fetchLocale, dispatch),
}); });
export default withRouter( export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(LanguagePicker), connect(
mapStateToProps,
mapDispatchToProps
)(LanguagePicker)
); );

@ -80,8 +80,7 @@ const styles = {
}), }),
}; };
const buildOption = langCode => {
const buildOption = (langCode) => {
const lang = LANGUAGE.find(l => l.code === langCode); const lang = LANGUAGE.find(l => l.code === langCode);
return { value: lang.code, label: lang.name }; return { value: lang.code, label: lang.name };
}; };
@ -99,9 +98,7 @@ const LanguagePicker = ({ language, fetchLocale }: Props) => (
isClearable={false} isClearable={false}
onChange={option => fetchLocale(option.value)} onChange={option => fetchLocale(option.value)}
value={buildOption(language)} value={buildOption(language)}
options={ options={LANGUAGE.map(lang => buildOption(lang.code))}
LANGUAGE.map(lang => buildOption(lang.code))
}
/> />
</SelectWrapper> </SelectWrapper>
); );

@ -53,16 +53,14 @@ const MenuToggler = styled.div`
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
padding: 10px 0px; padding: 10px 0px;
transition: all .1s ease-in; transition: all 0.1s ease-in;
@media screen and (max-width: ${SCREEN_SIZE.SM}) { @media screen and (max-width: ${SCREEN_SIZE.SM}) {
display: flex; display: flex;
} }
`; `;
const TogglerText = styled.div` const TogglerText = styled.div``;
`;
const TREZOR = styled.div``; const TREZOR = styled.div``;
const T = styled.div``; const T = styled.div``;
@ -75,11 +73,11 @@ const Logo = styled.div`
${T} { ${T} {
display: none; display: none;
width: 20px; width: 20px;
} }
${TREZOR} { ${TREZOR} {
width: 100px; width: 100px;
} }
svg { svg {
fill: ${colors.WHITE}; fill: ${colors.WHITE};
@ -95,11 +93,11 @@ const Logo = styled.div`
/* hides full width trezor logo, shows only trezor icon */ /* hides full width trezor logo, shows only trezor icon */
${TREZOR} { ${TREZOR} {
display: none; display: none;
} }
${T} { ${T} {
display: inherit; display: inherit;
} }
} }
`; `;
@ -131,7 +129,7 @@ const Projects = styled.div`
const A = styled.a` const A = styled.a`
color: ${colors.WHITE}; color: ${colors.WHITE};
margin-left: 24px; margin-left: 24px;
transition: all .1s ease-in; transition: all 0.1s ease-in;
white-space: nowrap; white-space: nowrap;
&:visited { &:visited {
@ -153,33 +151,24 @@ type Props = {
sidebarEnabled?: boolean, sidebarEnabled?: boolean,
sidebarOpened?: ?boolean, sidebarOpened?: ?boolean,
toggleSidebar?: toggleSidebarType, toggleSidebar?: toggleSidebarType,
}; };
const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => ( const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => (
<Wrapper data-test="Main__page__navigation"> <Wrapper data-test="Main__page__navigation">
<LayoutWrapper> <LayoutWrapper>
<Left> <Left>
{ sidebarEnabled && ( {sidebarEnabled && (
<MenuToggler onClick={toggleSidebar}> <MenuToggler onClick={toggleSidebar}>
{sidebarOpened ? ( {sidebarOpened ? (
<> <>
<Icon <Icon size={24} color={colors.WHITE} icon={icons.CLOSE} />
size={24}
color={colors.WHITE}
icon={icons.CLOSE}
/>
<TogglerText> <TogglerText>
<FormattedMessage {...l10nMessages.TR_MENU_CLOSE} /> <FormattedMessage {...l10nMessages.TR_MENU_CLOSE} />
</TogglerText> </TogglerText>
</> </>
) : ( ) : (
<> <>
<Icon <Icon color={colors.WHITE} size={24} icon={icons.MENU} />
color={colors.WHITE}
size={24}
icon={icons.MENU}
/>
<TogglerText> <TogglerText>
<FormattedMessage {...l10nMessages.TR_MENU} /> <FormattedMessage {...l10nMessages.TR_MENU} />
</TogglerText> </TogglerText>
@ -191,7 +180,15 @@ const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => (
<Logo> <Logo>
<NavLink to="/"> <NavLink to="/">
<TREZOR> <TREZOR>
<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" />
<path d="M130.8,12.5c-6.8,0-11.6,4.9-11.6,11.5s4.9,11.5,11.6,11.5s11.7-4.9,11.7-11.5S137.6,12.5,130.8,12.5z M130.8,30.3 c-3.4,0-5.7-2.6-5.7-6.3c0-3.8,2.3-6.3,5.7-6.3c3.4,0,5.8,2.6,5.8,6.3C136.6,27.7,134.2,30.3,130.8,30.3z" /> <path d="M130.8,12.5c-6.8,0-11.6,4.9-11.6,11.5s4.9,11.5,11.6,11.5s11.7-4.9,11.7-11.5S137.6,12.5,130.8,12.5z M130.8,30.3 c-3.4,0-5.7-2.6-5.7-6.3c0-3.8,2.3-6.3,5.7-6.3c3.4,0,5.8,2.6,5.8,6.3C136.6,27.7,134.2,30.3,130.8,30.3z" />
@ -202,7 +199,15 @@ const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => (
</svg> </svg>
</TREZOR> </TREZOR>
<T> <T>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 20 41.9" width="100%" height="100%" preserveAspectRatio="xMinYMin meet"> <svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 20 41.9"
width="100%"
height="100%"
preserveAspectRatio="xMinYMin meet"
>
<path d="M24.6,9.7C24.6,4.4,20,0,14.4,0S4.2,4.4,4.2,9.7v3.1H0v22.3h0l14.4,6.7l14.4-6.7h0V12.9h-4.2V9.7z M9.4,9.7 c0-2.5,2.2-4.5,5-4.5s5,2,5,4.5v3.1H9.4V9.7z M23,31.5l-8.6,4l-8.6-4V18.1H23V31.5z" /> <path d="M24.6,9.7C24.6,4.4,20,0,14.4,0S4.2,4.4,4.2,9.7v3.1H0v22.3h0l14.4,6.7l14.4-6.7h0V12.9h-4.2V9.7z M9.4,9.7 c0-2.5,2.2-4.5,5-4.5s5,2,5,4.5v3.1H9.4V9.7z M23,31.5l-8.6,4l-8.6-4V18.1H23V31.5z" />
</svg> </svg>
</T> </T>
@ -210,10 +215,18 @@ const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => (
</Logo> </Logo>
<MenuLinks> <MenuLinks>
<Projects> <Projects>
<A href="https://trezor.io/" target="_blank" rel="noreferrer noopener"><FormattedMessage {...l10nMessages.TR_TREZOR} /></A> <A href="https://trezor.io/" target="_blank" rel="noreferrer noopener">
<A href="https://wiki.trezor.io/" target="_blank" rel="noreferrer noopener"><FormattedMessage {...l10nMessages.TR_WIKI} /></A> <FormattedMessage {...l10nMessages.TR_TREZOR} />
<A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener"><FormattedMessage {...l10nMessages.TR_BLOG} /></A> </A>
<A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener"><FormattedMessage {...l10nMessages.TR_SUPPORT} /></A> <A href="https://wiki.trezor.io/" target="_blank" rel="noreferrer noopener">
<FormattedMessage {...l10nMessages.TR_WIKI} />
</A>
<A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener">
<FormattedMessage {...l10nMessages.TR_BLOG} />
</A>
<A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener">
<FormattedMessage {...l10nMessages.TR_SUPPORT} />
</A>
</Projects> </Projects>
<LanguagePicker /> <LanguagePicker />
</MenuLinks> </MenuLinks>
@ -221,4 +234,4 @@ const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => (
</Wrapper> </Wrapper>
); );
export default Header; export default Header;

@ -35,4 +35,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -22,8 +22,9 @@ const H2 = styled.h2`
font-size: ${FONT_SIZE.H2}; font-size: ${FONT_SIZE.H2};
padding-bottom: 10px; padding-bottom: 10px;
${props => props.claim ${props =>
&& css` props.claim &&
css`
font-size: ${FONT_SIZE.HUGE}; font-size: ${FONT_SIZE.HUGE};
padding-bottom: 24px; padding-bottom: 24px;
`}; `};
@ -41,6 +42,4 @@ const H4 = styled.h4`
padding-bottom: 10px; padding-bottom: 10px;
`; `;
export { export { H1, H2, H3, H4 };
H1, H2, H3, H4,
};

@ -17,7 +17,7 @@ type Props = {
onMouseLeave?: () => void, onMouseLeave?: () => void,
onFocus?: () => void, onFocus?: () => void,
onClick?: () => void, onClick?: () => void,
} };
const chooseIconAnimationType = (canAnimate, isActive) => { const chooseIconAnimationType = (canAnimate, isActive) => {
if (canAnimate) { if (canAnimate) {
@ -49,11 +49,12 @@ const rotate180down = keyframes`
`; `;
const SvgWrapper = styled.svg` const SvgWrapper = styled.svg`
animation: ${props => chooseIconAnimationType(props.canAnimate, props.isActive)} 0.2s linear 1 forwards; animation: ${props => chooseIconAnimationType(props.canAnimate, props.isActive)} 0.2s linear 1
forwards;
:hover { :hover {
path { path {
fill: ${props => props.hoverColor} fill: ${props => props.hoverColor};
} }
} }
`; `;
@ -93,12 +94,7 @@ const Icon = ({
onClick={onClick} onClick={onClick}
> >
{icon.map(path => ( {icon.map(path => (
<Path <Path key={path} isActive={isActive} color={color} d={path} />
key={path}
isActive={isActive}
color={color}
d={path}
/>
))} ))}
</SvgWrapper> </SvgWrapper>
); );
@ -117,4 +113,4 @@ Icon.propTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
}; };
export default Icon; export default Icon;

@ -11,25 +11,33 @@ const A = styled.a`
transition: ${TRANSITION.HOVER}; transition: ${TRANSITION.HOVER};
font-size: ${FONT_SIZE.SMALL}; font-size: ${FONT_SIZE.SMALL};
${props => props.isGreen && css` ${props =>
text-decoration: underline; props.isGreen &&
text-decoration-color: ${colors.GREEN_PRIMARY}; css`
`} text-decoration: underline;
${props => props.isGray && css` text-decoration-color: ${colors.GREEN_PRIMARY};
text-decoration: underline; `}
text-decoration-color: ${colors.TEXT_SECONDARY}; ${props =>
`} props.isGray &&
css`
text-decoration: underline;
text-decoration-color: ${colors.TEXT_SECONDARY};
`}
&, &,
&:visited, &:visited,
&:active, &:active,
&:hover { &:hover {
${props => props.isGreen && css` ${props =>
color: ${colors.GREEN_PRIMARY}; props.isGreen &&
`} css`
${props => props.isGray && css` color: ${colors.GREEN_PRIMARY};
color: ${colors.TEXT_SECONDARY}; `}
`} ${props =>
props.isGray &&
css`
color: ${colors.TEXT_SECONDARY};
`}
} }
&:hover { &:hover {
@ -38,13 +46,17 @@ const A = styled.a`
`; `;
const StyledNavLink = styled(NavLink)` const StyledNavLink = styled(NavLink)`
${props => props.isGreen && css` ${props =>
color: ${colors.GREEN_PRIMARY}; props.isGreen &&
`} css`
color: ${colors.GREEN_PRIMARY};
`}
${props => props.isGray && css` ${props =>
color: ${colors.TEXT_SECONDARY}; props.isGray &&
`} css`
color: ${colors.TEXT_SECONDARY};
`}
`; `;
class Link extends PureComponent { class Link extends PureComponent {
@ -52,8 +64,7 @@ class Link extends PureComponent {
const shouldRenderRouterLink = this.props.to; const shouldRenderRouterLink = this.props.to;
let LinkComponent; let LinkComponent;
if (shouldRenderRouterLink) { if (shouldRenderRouterLink) {
LinkComponent = ( LinkComponent = <StyledNavLink {...this.props}>{this.props.children}</StyledNavLink>;
<StyledNavLink {...this.props}>{this.props.children}</StyledNavLink>);
} else { } else {
LinkComponent = ( LinkComponent = (
<A <A
@ -61,7 +72,8 @@ class Link extends PureComponent {
target={this.props.target || '_blank'} target={this.props.target || '_blank'}
rel="noreferrer noopener" rel="noreferrer noopener"
{...this.props} {...this.props}
>{this.props.children} >
{this.props.children}
</A> </A>
); );
} }
@ -86,4 +98,4 @@ Link.propTypes = {
isGray: PropTypes.bool, isGray: PropTypes.bool,
}; };
export default Link; export default Link;

@ -24,17 +24,22 @@ const SvgWrapper = styled.svg`
`; `;
const CircleWrapper = styled.circle` const CircleWrapper = styled.circle`
${props => props.isRoute && css` ${props =>
stroke: ${props.transparentRoute ? 'transparent' : colors.GRAY_LIGHT}; props.isRoute &&
`} css`
stroke: ${props.transparentRoute ? 'transparent' : colors.GRAY_LIGHT};
`}
${props => props.isPath && css` ${props =>
stroke-width: ${props.transparentRoute ? '2px' : '1px'}; props.isPath &&
stroke-dasharray: 1, 200; css`
stroke-dashoffset: 0; stroke-width: ${props.transparentRoute ? '2px' : '1px'};
animation: ${DASH} 1.5s ease-in-out infinite, ${props.animationColor || GREEN_COLOR} 6s ease-in-out infinite; stroke-dasharray: 1, 200;
stroke-linecap: round; stroke-dashoffset: 0;
`}; animation: ${DASH} 1.5s ease-in-out infinite,
${props.animationColor || GREEN_COLOR} 6s ease-in-out infinite;
stroke-linecap: round;
`};
`; `;
const StyledParagraph = styled(Paragraph)` const StyledParagraph = styled(Paragraph)`
@ -43,10 +48,18 @@ const StyledParagraph = styled(Paragraph)`
`; `;
const Loader = ({ const Loader = ({
className, text, isWhiteText = false, isSmallText, size = 100, animationColor, transparentRoute, className,
text,
isWhiteText = false,
isSmallText,
size = 100,
animationColor,
transparentRoute,
}) => ( }) => (
<Wrapper className={className} size={size}> <Wrapper className={className} size={size}>
<StyledParagraph isSmallText={isSmallText} isWhiteText={isWhiteText}>{text}</StyledParagraph> <StyledParagraph isSmallText={isSmallText} isWhiteText={isWhiteText}>
{text}
</StyledParagraph>
<SvgWrapper viewBox="25 25 50 50"> <SvgWrapper viewBox="25 25 50 50">
<CircleWrapper <CircleWrapper
animationColor={animationColor} animationColor={animationColor}

@ -10,7 +10,6 @@ import Icon from 'components/Icon';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import * as LogActions from 'actions/LogActions'; import * as LogActions from 'actions/LogActions';
import icons from 'config/icons'; import icons from 'config/icons';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
@ -18,8 +17,8 @@ import l10nMessages from './index.messages';
type Props = { type Props = {
log: $ElementType<State, 'log'>, log: $ElementType<State, 'log'>,
toggle: typeof LogActions.toggle toggle: typeof LogActions.toggle,
} };
const Wrapper = styled.div` const Wrapper = styled.div`
position: relative; position: relative;
@ -86,5 +85,5 @@ export default connect(
}), }),
(dispatch: Dispatch) => ({ (dispatch: Dispatch) => ({
toggle: bindActionCreators(LogActions.toggle, dispatch), toggle: bindActionCreators(LogActions.toggle, dispatch),
}), })
)(Log); )(Log);

@ -5,7 +5,8 @@ import type { Messages } from 'flowtype/npm/react-intl';
const definedMessages: Messages = defineMessages({ const definedMessages: Messages = defineMessages({
TR_ATTENTION_COLON_THE_LOG_CONTAINS: { TR_ATTENTION_COLON_THE_LOG_CONTAINS: {
id: 'TR_ATTENTION_COLON_THE_LOG_CONTAINS', id: 'TR_ATTENTION_COLON_THE_LOG_CONTAINS',
defaultMessage: 'Attention: The log contains your XPUBs. Anyone with your XPUBs can see your account history.', defaultMessage:
'Attention: The log contains your XPUBs. Anyone with your XPUBs can see your account history.',
}, },
TR_LOG: { TR_LOG: {
id: 'TR_LOG', id: 'TR_LOG',
@ -14,4 +15,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -8,20 +8,18 @@ import colors from 'config/colors';
import { WHITE_COLOR } from 'config/animations'; import { WHITE_COLOR } from 'config/animations';
import { getPrimaryColor } from 'utils/notification'; import { getPrimaryColor } from 'utils/notification';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import { import { TRANSITION, FONT_SIZE, FONT_WEIGHT, SCREEN_SIZE } from 'config/variables';
TRANSITION, FONT_SIZE, FONT_WEIGHT, SCREEN_SIZE,
} from 'config/variables';
type Props = { type Props = {
type: string; type: string,
icon?: { icon?: {
type: Array<string>; type: Array<string>,
color: string; color: string,
size: number; size: number,
}; },
onClick: () => void; onClick: () => void,
isLoading?: boolean; isLoading?: boolean,
children: React.Node; children: React.Node,
}; };
const LoaderContent = styled.div` const LoaderContent = styled.div`
@ -50,8 +48,8 @@ const Wrapper = styled.button`
border: 1px solid ${props => getPrimaryColor(props.type)}; border: 1px solid ${props => getPrimaryColor(props.type)};
transition: ${TRANSITION.HOVER}; transition: ${TRANSITION.HOVER};
@media screen and (max-width: ${SCREEN_SIZE.SM}){ @media screen and (max-width: ${SCREEN_SIZE.SM}) {
padding: 12px 24px; padding: 12px 24px;
} }
&:hover { &:hover {
@ -64,14 +62,8 @@ const IconWrapper = styled.span`
margin-right: 8px; margin-right: 8px;
`; `;
const NotificationButton = ({ const NotificationButton = ({ type, icon, onClick, children, isLoading }: Props) => (
type, icon, onClick, children, isLoading, <Wrapper icon={icon} onClick={onClick} type={type}>
}: Props) => (
<Wrapper
icon={icon}
onClick={onClick}
type={type}
>
{isLoading && ( {isLoading && (
<LoaderContent type={type}> <LoaderContent type={type}>
<Loader transparentRoute animationColor={WHITE_COLOR} size={30} /> <Loader transparentRoute animationColor={WHITE_COLOR} size={30} />
@ -79,11 +71,7 @@ const NotificationButton = ({
)} )}
{icon && ( {icon && (
<IconWrapper> <IconWrapper>
<Icon <Icon icon={icon.type} color={icon.color} size={icon.size} />
icon={icon.type}
color={icon.color}
size={icon.size}
/>
</IconWrapper> </IconWrapper>
)} )}
{children} {children}
@ -102,4 +90,4 @@ NotificationButton.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
}; };
export default NotificationButton; export default NotificationButton;

@ -14,14 +14,14 @@ import NotificationButton from './components/NotificationButton';
type Props = { type Props = {
type: string, type: string,
cancelable?: boolean; cancelable?: boolean,
title: ?React.Node; title: ?React.Node,
className?: string; className?: string,
message?: ?React.Node; message?: ?React.Node,
actions?: Array<CallbackAction>; actions?: Array<CallbackAction>,
isActionInProgress?: boolean; isActionInProgress?: boolean,
close?: typeof NotificationActions.close, close?: typeof NotificationActions.close,
loading?: boolean loading?: boolean,
}; };
const Wrapper = styled.div` const Wrapper = styled.div`
@ -98,7 +98,7 @@ const Notification = (props: Props): React$Element<string> => {
return ( return (
<Wrapper className={props.className} type={props.type}> <Wrapper className={props.className} type={props.type}>
<Content> <Content>
{props.loading && <Loader size={50} /> } {props.loading && <Loader size={50} />}
<Body> <Body>
<IconWrapper> <IconWrapper>
<StyledIcon <StyledIcon
@ -107,8 +107,8 @@ const Notification = (props: Props): React$Element<string> => {
/> />
</IconWrapper> </IconWrapper>
<Texts> <Texts>
<Title>{ props.title }</Title> <Title>{props.title}</Title>
{ props.message ? <Message>{props.message}</Message> : '' } {props.message ? <Message>{props.message}</Message> : ''}
</Texts> </Texts>
</Body> </Body>
<AdditionalContent> <AdditionalContent>
@ -119,8 +119,12 @@ const Notification = (props: Props): React$Element<string> => {
key={action.label} key={action.label}
type={props.type} type={props.type}
isLoading={props.isActionInProgress} isLoading={props.isActionInProgress}
onClick={() => { close(); action.callback(); }} onClick={() => {
>{action.label} close();
action.callback();
}}
>
{action.label}
</NotificationButton> </NotificationButton>
))} ))}
</ActionContent> </ActionContent>
@ -128,11 +132,7 @@ const Notification = (props: Props): React$Element<string> => {
</AdditionalContent> </AdditionalContent>
{props.cancelable && ( {props.cancelable && (
<CloseClick onClick={() => close()}> <CloseClick onClick={() => close()}>
<Icon <Icon color={getPrimaryColor(props.type)} icon={icons.CLOSE} size={20} />
color={getPrimaryColor(props.type)}
icon={icons.CLOSE}
size={20}
/>
</CloseClick> </CloseClick>
)} )}
</Content> </Content>
@ -140,4 +140,4 @@ const Notification = (props: Props): React$Element<string> => {
); );
}; };
export default Notification; export default Notification;

@ -11,16 +11,16 @@ const Wrapper = styled.p`
padding: 0; padding: 0;
margin: 0; margin: 0;
${props => props.isSmaller && css` ${props =>
font-size: ${FONT_SIZE.SMALL}; props.isSmaller &&
`} css`
font-size: ${FONT_SIZE.SMALL};
`}
`; `;
const P = ({ children, className, isSmaller = false }) => ( const P = ({ children, className, isSmaller = false }) => (
<Wrapper <Wrapper className={className} isSmaller={isSmaller}>
className={className} {children}
isSmaller={isSmaller}
>{children}
</Wrapper> </Wrapper>
); );

@ -28,7 +28,7 @@ const styles = isSearchable => ({
}), }),
dropdownIndicator: (base, { isDisabled }) => ({ dropdownIndicator: (base, { isDisabled }) => ({
...base, ...base,
display: (isSearchable || isDisabled) ? 'none' : 'block', display: isSearchable || isDisabled ? 'none' : 'block',
color: colors.TEXT_SECONDARY, color: colors.TEXT_SECONDARY,
path: '', path: '',
'&:hover': { '&:hover': {
@ -61,7 +61,6 @@ const styles = isSearchable => ({
}), }),
}); });
const propTypes = { const propTypes = {
isAsync: PropTypes.bool, isAsync: PropTypes.bool,
isSearchable: PropTypes.bool, isSearchable: PropTypes.bool,
@ -71,7 +70,4 @@ const AsyncSelect = props => <ReactAsyncSelect styles={styles(props.isSearchable
Select.propTypes = propTypes; Select.propTypes = propTypes;
AsyncSelect.propTypes = propTypes; AsyncSelect.propTypes = propTypes;
export { export { Select, AsyncSelect };
Select,
AsyncSelect,
};

@ -3,12 +3,7 @@ import Textarea from 'react-textarea-autosize';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { import { FONT_SIZE, FONT_WEIGHT, LINE_HEIGHT, FONT_FAMILY } from 'config/variables';
FONT_SIZE,
FONT_WEIGHT,
LINE_HEIGHT,
FONT_FAMILY,
} from 'config/variables';
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@ -34,11 +29,11 @@ const StyledTextarea = styled(Textarea)`
background: ${colors.WHITE}; background: ${colors.WHITE};
font-weight: ${FONT_WEIGHT.MEDIUM}; font-weight: ${FONT_WEIGHT.MEDIUM};
font-size: ${FONT_SIZE.BASE}; font-size: ${FONT_SIZE.BASE};
white-space: pre-wrap; /* css-3 */ white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */ white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */ white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */ word-wrap: break-word; /* Internet Explorer 5.5+ */
/* placeholder styles do not work correctly when groupped into one block */ /* placeholder styles do not work correctly when groupped into one block */
@ -98,14 +93,16 @@ const StyledTextarea = styled(Textarea)`
} }
} }
${props => props.trezorAction && css` ${props =>
z-index: 10001; /* bigger than modal container */ props.trezorAction &&
border-color: ${colors.WHITE}; css`
border-width: 2px; z-index: 10001; /* bigger than modal container */
transform: translate(-1px, -1px); border-color: ${colors.WHITE};
background: ${colors.DIVIDER}; border-width: 2px;
pointer-events: none; transform: translate(-1px, -1px);
`} background: ${colors.DIVIDER};
pointer-events: none;
`}
`; `;
const TopLabel = styled.span` const TopLabel = styled.span`
@ -119,7 +116,7 @@ const BottomText = styled.span`
color: ${props => (props.color ? props.color : colors.TEXT_SECONDARY)}; color: ${props => (props.color ? props.color : colors.TEXT_SECONDARY)};
`; `;
const getColor = (inputState) => { const getColor = inputState => {
let color = ''; let color = '';
if (inputState === 'success') { if (inputState === 'success') {
color = colors.SUCCESS_PRIMARY; color = colors.SUCCESS_PRIMARY;
@ -179,9 +176,7 @@ const TextArea = ({
trezorAction = null, trezorAction = null,
}) => ( }) => (
<Wrapper className={className}> <Wrapper className={className}>
{topLabel && ( {topLabel && <TopLabel>{topLabel}</TopLabel>}
<TopLabel>{topLabel}</TopLabel>
)}
<StyledTextarea <StyledTextarea
spellCheck="false" spellCheck="false"
autoCorrect="off" autoCorrect="off"
@ -202,15 +197,10 @@ const TextArea = ({
onChange={onChange} onChange={onChange}
/> />
<TrezorAction action={trezorAction}> <TrezorAction action={trezorAction}>
<ArrowUp />{trezorAction} <ArrowUp />
{trezorAction}
</TrezorAction> </TrezorAction>
{bottomText && ( {bottomText && <BottomText color={getColor(state)}>{bottomText}</BottomText>}
<BottomText
color={getColor(state)}
>
{bottomText}
</BottomText>
)}
</Wrapper> </Wrapper>
); );

@ -45,10 +45,11 @@ const Tooltip = ({
<Content maxWidth={maxWidth}>{content}</Content> <Content maxWidth={maxWidth}>{content}</Content>
{readMoreLink && ( {readMoreLink && (
<Link href={readMoreLink}> <Link href={readMoreLink}>
<ReadMore><FormattedMessage {...l10nCommonMessages.TR_LEARN_MORE} /></ReadMore> <ReadMore>
<FormattedMessage {...l10nCommonMessages.TR_LEARN_MORE} />
</ReadMore>
</Link> </Link>
) )}
}
</ContentWrapper> </ContentWrapper>
)} )}
> >
@ -60,15 +61,9 @@ const Tooltip = ({
Tooltip.propTypes = { Tooltip.propTypes = {
className: PropTypes.string, className: PropTypes.string,
placement: PropTypes.string, placement: PropTypes.string,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
PropTypes.element,
PropTypes.string,
]),
maxWidth: PropTypes.number, maxWidth: PropTypes.number,
content: PropTypes.oneOfType([ content: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
PropTypes.element,
PropTypes.string,
]),
readMoreLink: PropTypes.string, readMoreLink: PropTypes.string,
enterDelayMs: PropTypes.number, enterDelayMs: PropTypes.number,
}; };

@ -2,7 +2,6 @@
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import type { Messages } from 'flowtype/npm/react-intl'; import type { Messages } from 'flowtype/npm/react-intl';
const definedMessages: Messages = defineMessages({ const definedMessages: Messages = defineMessages({});
});
export default definedMessages; export default definedMessages;

@ -65,30 +65,55 @@ const Fee = styled.div`
border: 1px; border: 1px;
`; `;
const TransactionItem = ({ const TransactionItem = ({ tx, network }: Props) => {
tx,
network,
}: Props) => {
const url = `${network.explorer.tx}${tx.hash}`; const url = `${network.explorer.tx}${tx.hash}`;
const date = typeof tx.timestamp === 'string' ? tx.timestamp : undefined; // TODO: format date const date = typeof tx.timestamp === 'string' ? tx.timestamp : undefined; // TODO: format date
const addresses = (tx.type === 'send' ? tx.outputs : tx.inputs).reduce((arr, item) => arr.concat(item.addresses), []); const addresses = (tx.type === 'send' ? tx.outputs : tx.inputs).reduce(
(arr, item) => arr.concat(item.addresses),
[]
);
const operation = tx.type === 'send' ? '-' : '+'; const operation = tx.type === 'send' ? '-' : '+';
const amount = tx.tokens ? tx.tokens.map(t => (<Amount key={t.value}>{operation}{t.value} {t.shortcut}</Amount>)) : <Amount>{operation}{tx.total} {network.symbol}</Amount>; const amount = tx.tokens ? (
tx.tokens.map(t => (
<Amount key={t.value}>
{operation}
{t.value} {t.shortcut}
</Amount>
))
) : (
<Amount>
{operation}
{tx.total} {network.symbol}
</Amount>
);
const fee = tx.tokens && tx.type === 'send' ? `${tx.fee} ${network.symbol}` : undefined; const fee = tx.tokens && tx.type === 'send' ? `${tx.fee} ${network.symbol}` : undefined;
return ( return (
<Wrapper> <Wrapper>
{ date && (<Date href={url} isGray>{ date }</Date>)} {date && (
<Date href={url} isGray>
{date}
</Date>
)}
<Addresses> <Addresses>
{ addresses.map(addr => (<Address key={addr}>{addr}</Address>)) } {addresses.map(addr => (
{ !tx.blockHeight && ( <Address key={addr}>{addr}</Address>
<Date href={url} isGray>Transaction hash: {tx.hash}</Date> ))}
{!tx.blockHeight && (
<Date href={url} isGray>
Transaction hash: {tx.hash}
</Date>
)} )}
</Addresses> </Addresses>
<Value className={tx.type}> <Value className={tx.type}>
{amount} {amount}
{ fee && (<Fee>{operation}{fee}</Fee>) } {fee && (
<Fee>
{operation}
{fee}
</Fee>
)}
</Value> </Value>
</Wrapper> </Wrapper>
); );
@ -99,4 +124,4 @@ TransactionItem.propTypes = {
network: PropTypes.object.isRequired, network: PropTypes.object.isRequired,
}; };
export default TransactionItem; export default TransactionItem;

@ -42,11 +42,7 @@ class CoinLogo extends PureComponent {
return logo; return logo;
} }
return ( return <Wrapper className={className}>{logo}</Wrapper>;
<Wrapper className={className}>
{logo}
</Wrapper>
);
} }
} }

@ -13,12 +13,12 @@ type Props = {
color?: string, color?: string,
hoverColor?: string, hoverColor?: string,
onClick?: any, onClick?: any,
} };
const SvgWrapper = styled.svg` const SvgWrapper = styled.svg`
:hover { :hover {
path { path {
fill: ${props => props.hoverColor} fill: ${props => props.hoverColor};
} }
} }
`; `;
@ -54,11 +54,7 @@ const DeviceIcon = ({
viewBox="0 0 1024 1024" viewBox="0 0 1024 1024"
onClick={onClick} onClick={onClick}
> >
<Path <Path key={majorVersion} color={color} d={getDeviceIcon(majorVersion)} />
key={majorVersion}
color={color}
d={getDeviceIcon(majorVersion)}
/>
</SvgWrapper> </SvgWrapper>
); );
}; };
@ -71,4 +67,4 @@ DeviceIcon.propTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
}; };
export default DeviceIcon; export default DeviceIcon;

@ -5,8 +5,8 @@ import styled from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
type Props = { type Props = {
model: string; model: string,
} };
const Wrapper = styled.div``; const Wrapper = styled.div``;

@ -7,7 +7,7 @@ import ICONS from 'config/icons';
const SvgWrapper = styled.svg` const SvgWrapper = styled.svg`
:hover { :hover {
path { path {
fill: ${props => props.hoverColor} fill: ${props => props.hoverColor};
} }
} }
`; `;
@ -46,4 +46,4 @@ Icon.propTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
}; };
export default Icon; export default Icon;

@ -4,13 +4,7 @@ import styled, { css } from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import ICONS from 'config/icons'; import ICONS from 'config/icons';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { import { FONT_SIZE, FONT_FAMILY, FONT_WEIGHT, LINE_HEIGHT, TRANSITION } from 'config/variables';
FONT_SIZE,
FONT_FAMILY,
FONT_WEIGHT,
LINE_HEIGHT,
TRANSITION,
} from 'config/variables';
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@ -48,10 +42,12 @@ const StyledInput = styled.input`
border-radius: 2px; border-radius: 2px;
${props => props.hasAddon && css` ${props =>
border-top-right-radius: 0; props.hasAddon &&
border-bottom-right-radius: 0; css`
`} border-top-right-radius: 0;
border-bottom-right-radius: 0;
`}
border: 1px solid ${colors.DIVIDER}; border: 1px solid ${colors.DIVIDER};
border-color: ${props => props.borderColor}; border-color: ${props => props.borderColor};
@ -75,14 +71,16 @@ const StyledInput = styled.input`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
} }
${props => props.trezorAction && css` ${props =>
z-index: 10001; props.trezorAction &&
position: relative; /* bigger than modal container */ css`
border-color: ${colors.WHITE}; z-index: 10001;
border-width: 2px; position: relative; /* bigger than modal container */
transform: translate(-1px, -1px); border-color: ${colors.WHITE};
background: ${colors.DIVIDER}; border-width: 2px;
`}; transform: translate(-1px, -1px);
background: ${colors.DIVIDER};
`};
`; `;
const StyledIcon = styled(Icon)` const StyledIcon = styled(Icon)`
@ -99,18 +97,21 @@ const BottomText = styled.span`
`; `;
const Overlay = styled.div` const Overlay = styled.div`
${props => props.isPartiallyHidden && css` ${props =>
bottom: 0; props.isPartiallyHidden &&
border: 1px solid ${colors.DIVIDER}; css`
border-radius: 2px; bottom: 0;
position: absolute; border: 1px solid ${colors.DIVIDER};
width: 100%; border-radius: 2px;
height: 100%; position: absolute;
background-image: linear-gradient(to right, width: 100%;
rgba(0,0,0, 0) 0%, height: 100%;
rgba(249,249,249, 1) 220px background-image: linear-gradient(
); to right,
`} rgba(0, 0, 0, 0) 0%,
rgba(249, 249, 249, 1) 220px
);
`}
`; `;
const TrezorAction = styled.div` const TrezorAction = styled.div`
@ -168,12 +169,8 @@ class Input extends PureComponent {
render() { render() {
return ( return (
<Wrapper <Wrapper className={this.props.className}>
className={this.props.className} {this.props.topLabel && <TopLabel>{this.props.topLabel}</TopLabel>}
>
{this.props.topLabel && (
<TopLabel>{this.props.topLabel}</TopLabel>
)}
<InputWrapper> <InputWrapper>
<InputIconWrapper> <InputIconWrapper>
{this.props.state && ( {this.props.state && (
@ -208,15 +205,14 @@ class Input extends PureComponent {
data-lpignore="true" data-lpignore="true"
/> />
<TrezorAction action={this.props.trezorAction}> <TrezorAction action={this.props.trezorAction}>
<ArrowUp />{this.props.trezorAction} <ArrowUp />
{this.props.trezorAction}
</TrezorAction> </TrezorAction>
</InputIconWrapper> </InputIconWrapper>
{this.props.sideAddons && this.props.sideAddons.map(sideAddon => sideAddon)} {this.props.sideAddons && this.props.sideAddons.map(sideAddon => sideAddon)}
</InputWrapper> </InputWrapper>
{this.props.bottomText && ( {this.props.bottomText && (
<BottomText <BottomText color={this.getColor(this.props.state)}>
color={this.getColor(this.props.state)}
>
{this.props.bottomText} {this.props.bottomText}
</BottomText> </BottomText>
)} )}

@ -33,7 +33,9 @@ type DispatchProps = {
export type Props = StateProps & DispatchProps; export type Props = StateProps & DispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (
state: State
): StateProps => ({
modal: state.modal, modal: state.modal,
accounts: state.accounts, accounts: state.accounts,
devices: state.devices, devices: state.devices,
@ -46,12 +48,17 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
wallet: state.wallet, wallet: state.wallet,
}); });
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (
dispatch: Dispatch
): DispatchProps => ({
modalActions: bindActionCreators(ModalActions, dispatch), modalActions: bindActionCreators(ModalActions, dispatch),
receiveActions: bindActionCreators(ReceiveActions, dispatch), receiveActions: bindActionCreators(ReceiveActions, dispatch),
}); });
// export default connect(mapStateToProps, mapDispatchToProps)(Modal); // export default connect(mapStateToProps, mapDispatchToProps)(Modal);
export default withRouter( export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Modal), connect(
); mapStateToProps,
mapDispatchToProps
)(Modal)
);

@ -63,7 +63,7 @@ type Props = {
onError?: (error: any) => any, onError?: (error: any) => any,
onCancel?: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>, onCancel?: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
intl: any, intl: any,
} };
type State = { type State = {
readerLoaded: boolean, readerLoaded: boolean,
@ -83,7 +83,7 @@ class QrModal extends Component<Props, State> {
this.setState({ this.setState({
readerLoaded: true, readerLoaded: true,
}); });
} };
handleScan = (data: string) => { handleScan = (data: string) => {
if (data) { if (data) {
@ -102,7 +102,7 @@ class QrModal extends Component<Props, State> {
this.handleError(error); this.handleError(error);
} }
} }
} };
handleError = (err: any) => { handleError = (err: any) => {
// log thrown error // log thrown error
@ -111,8 +111,12 @@ class QrModal extends Component<Props, State> {
this.props.onError(err); this.props.onError(err);
} }
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError' if (
|| err.name === 'NotReadableError' || err.name === 'TrackStartError') { err.name === 'NotAllowedError' ||
err.name === 'PermissionDeniedError' ||
err.name === 'NotReadableError' ||
err.name === 'TrackStartError'
) {
this.setState({ this.setState({
error: this.props.intl.formatMessage(l10nMessages.TR_CAMERA_PERMISSION_DENIED), error: this.props.intl.formatMessage(l10nMessages.TR_CAMERA_PERMISSION_DENIED),
}); });
@ -125,31 +129,34 @@ class QrModal extends Component<Props, State> {
error: this.props.intl.formatMessage(l10nMessages.TR_UNKOWN_ERROR_SEE_CONSOLE), error: this.props.intl.formatMessage(l10nMessages.TR_UNKOWN_ERROR_SEE_CONSOLE),
}); });
} }
} };
handleCancel = () => { handleCancel = () => {
if (this.props.onCancel) { if (this.props.onCancel) {
this.props.onCancel(); this.props.onCancel();
} }
} };
render() { render() {
return ( return (
<Wrapper> <Wrapper>
<CloseLink onClick={this.handleCancel}> <CloseLink onClick={this.handleCancel}>
<Icon <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
size={24}
color={colors.TEXT_SECONDARY}
icon={icons.CLOSE}
/>
</CloseLink> </CloseLink>
<Padding> <Padding>
<H2><FormattedMessage {...l10nMessages.TR_SCAN_QR_CODE} /></H2> <H2>
{!this.state.readerLoaded && !this.state.error && <CameraPlaceholder><FormattedMessage {...l10nMessages.TR_WAITING_FOR_CAMERA} /></CameraPlaceholder>} <FormattedMessage {...l10nMessages.TR_SCAN_QR_CODE} />
</H2>
{!this.state.readerLoaded && !this.state.error && (
<CameraPlaceholder>
<FormattedMessage {...l10nMessages.TR_WAITING_FOR_CAMERA} />
</CameraPlaceholder>
)}
{this.state.error && ( {this.state.error && (
<Error> <Error>
<ErrorTitle><FormattedMessage {...l10nMessages.TR_OOPS_SOMETHING_WENT_WRONG} /></ErrorTitle> <ErrorTitle>
<FormattedMessage {...l10nMessages.TR_OOPS_SOMETHING_WENT_WRONG} />
</ErrorTitle>
<ErrorMessage>{this.state.error.toString()}</ErrorMessage> <ErrorMessage>{this.state.error.toString()}</ErrorMessage>
</Error> </Error>
)} )}

@ -30,4 +30,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -11,8 +11,8 @@ import { FormattedMessage } from 'react-intl';
import l10nMessages from './index.messages'; import l10nMessages from './index.messages';
type Props = { type Props = {
device: TrezorDevice; device: TrezorDevice,
} };
const Wrapper = styled.div``; const Wrapper = styled.div``;
@ -35,5 +35,4 @@ ConfirmAction.propTypes = {
device: PropTypes.object.isRequired, device: PropTypes.object.isRequired,
}; };
export default ConfirmAction;
export default ConfirmAction;

@ -9,4 +9,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -37,10 +37,7 @@ const Label = styled.div`
`; `;
const ConfirmAddress = (props: Props) => { const ConfirmAddress = (props: Props) => {
const { const { account, network } = props.selectedAccount;
account,
network,
} = props.selectedAccount;
if (!account || !network) return null; if (!account || !network) return null;
return ( return (
@ -54,9 +51,13 @@ const ConfirmAddress = (props: Props) => {
</P> </P>
</Header> </Header>
<Content> <Content>
<P>{ account.descriptor }</P> <P>{account.descriptor}</P>
<Label>{ network.symbol } <Label>
<FormattedMessage {...l10nCommonMessages.TR_ACCOUNT_HASH} values={{ number: account.index + 1 }} /> {network.symbol}
<FormattedMessage
{...l10nCommonMessages.TR_ACCOUNT_HASH}
values={{ number: account.index + 1 }}
/>
</Label> </Label>
</Content> </Content>
</Wrapper> </Wrapper>
@ -67,4 +68,4 @@ ConfirmAddress.propTypes = {
selectedAccount: PropTypes.object.isRequired, selectedAccount: PropTypes.object.isRequired,
}; };
export default ConfirmAddress; export default ConfirmAddress;

@ -13,4 +13,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -18,9 +18,12 @@ import type { TrezorDevice } from 'flowtype';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
onReceiveConfirmation: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onReceiveConfirmation'>; onReceiveConfirmation: $ElementType<
device: ?TrezorDevice; $ElementType<BaseProps, 'modalActions'>,
} 'onReceiveConfirmation'
>,
device: ?TrezorDevice,
};
const Wrapper = styled.div` const Wrapper = styled.div`
max-width: 370px; max-width: 370px;
@ -68,12 +71,19 @@ const Confirmation = (props: Props) => (
</StyledLink> </StyledLink>
<H2>Your Trezor is not backed up</H2> <H2>Your Trezor is not backed up</H2>
<Icon size={48} color={colors.WARNING_PRIMARY} icon={icons.WARNING} /> <Icon size={48} color={colors.WARNING_PRIMARY} icon={icons.WARNING} />
<StyledP isSmaller>If your device is ever lost or damaged, your funds will be lost. Backup your device first, to protect your coins against such events.</StyledP> <StyledP isSmaller>
If your device is ever lost or damaged, your funds will be lost. Backup your device
first, to protect your coins against such events.
</StyledP>
<Row> <Row>
<Link href={`${getOldWalletUrl(props.device)}/?backup`} target="_self"> <Link href={`${getOldWalletUrl(props.device)}/?backup`} target="_self">
<BackupButton onClick={() => props.onReceiveConfirmation(false)}>Create a backup in 3 minutes</BackupButton> <BackupButton onClick={() => props.onReceiveConfirmation(false)}>
Create a backup in 3 minutes
</BackupButton>
</Link> </Link>
<ProceedButton isWhite onClick={() => props.onReceiveConfirmation(true)}>Show address, I will take the risk</ProceedButton> <ProceedButton isWhite onClick={() => props.onReceiveConfirmation(true)}>
Show address, I will take the risk
</ProceedButton>
</Row> </Row>
</Wrapper> </Wrapper>
); );
@ -82,4 +92,4 @@ Confirmation.propTypes = {
onReceiveConfirmation: PropTypes.func.isRequired, onReceiveConfirmation: PropTypes.func.isRequired,
}; };
export default Confirmation; export default Confirmation;

@ -15,11 +15,10 @@ import { FormattedMessage } from 'react-intl';
import type { TrezorDevice, State } from 'flowtype'; import type { TrezorDevice, State } from 'flowtype';
import l10nMessages from './index.messages'; import l10nMessages from './index.messages';
type Props = { type Props = {
device: TrezorDevice; device: TrezorDevice,
sendForm: $ElementType<State, 'sendFormEthereum'> | $ElementType<State, 'sendFormRipple'>; sendForm: $ElementType<State, 'sendFormEthereum'> | $ElementType<State, 'sendFormRipple'>,
} };
const Wrapper = styled.div` const Wrapper = styled.div`
max-width: 390px; max-width: 390px;
@ -62,20 +61,22 @@ const FeeLevelName = styled(StyledP)`
`; `;
const ConfirmSignTx = (props: Props) => { const ConfirmSignTx = (props: Props) => {
const { const { amount, address, selectedFeeLevel } = props.sendForm;
amount,
address,
selectedFeeLevel,
} = props.sendForm;
const currency: string = typeof props.sendForm.currency === 'string' ? props.sendForm.currency : props.sendForm.networkSymbol; const currency: string =
typeof props.sendForm.currency === 'string'
? props.sendForm.currency
: props.sendForm.networkSymbol;
return ( return (
<Wrapper> <Wrapper>
<Header> <Header>
<DeviceIcon device={props.device} size={60} color={colors.TEXT_SECONDARY} /> <DeviceIcon device={props.device} size={60} color={colors.TEXT_SECONDARY} />
<H3> <H3>
<FormattedMessage {...l10nMessages.TR_CONFIRM_TRANSACTION_ON} values={{ deviceLabel: props.device.label }} /> <FormattedMessage
{...l10nMessages.TR_CONFIRM_TRANSACTION_ON}
values={{ deviceLabel: props.device.label }}
/>
</H3> </H3>
<P isSmaller> <P isSmaller>
<FormattedMessage {...l10nMessages.TR_DETAILS_ARE_SHOWN_ON} /> <FormattedMessage {...l10nMessages.TR_DETAILS_ARE_SHOWN_ON} />
@ -85,16 +86,16 @@ const ConfirmSignTx = (props: Props) => {
<Label> <Label>
<FormattedMessage {...l10nMessages.TR_SEND_LABEL} /> <FormattedMessage {...l10nMessages.TR_SEND_LABEL} />
</Label> </Label>
<StyledP>{`${amount} ${currency}` }</StyledP> <StyledP>{`${amount} ${currency}`}</StyledP>
<Label> <Label>
<FormattedMessage {...l10nMessages.TR_TO_LABEL} /> <FormattedMessage {...l10nMessages.TR_TO_LABEL} />
</Label> </Label>
<Address>{ address }</Address> <Address>{address}</Address>
<Label> <Label>
<FormattedMessage {...l10nMessages.TR_FEE_LABEL} /> <FormattedMessage {...l10nMessages.TR_FEE_LABEL} />
</Label> </Label>
<FeeLevelName>{selectedFeeLevel.value}</FeeLevelName> <FeeLevelName>{selectedFeeLevel.value}</FeeLevelName>
<StyledP>{ selectedFeeLevel.label }</StyledP> <StyledP>{selectedFeeLevel.label}</StyledP>
</Content> </Content>
</Wrapper> </Wrapper>
); );
@ -105,4 +106,4 @@ ConfirmSignTx.propTypes = {
sendForm: PropTypes.object.isRequired, sendForm: PropTypes.object.isRequired,
}; };
export default ConfirmSignTx; export default ConfirmSignTx;

@ -14,7 +14,7 @@ const definedMessages: Messages = defineMessages({
TR_TO_LABEL: { TR_TO_LABEL: {
id: 'TR_TO_LABEL', id: 'TR_TO_LABEL',
defaultMessage: 'To', defaultMessage: 'To',
description: 'Label for recepeint\'s address', description: "Label for recepeint's address",
}, },
TR_SEND_LABEL: { TR_SEND_LABEL: {
id: 'TR_SEND_LABEL', id: 'TR_SEND_LABEL',
@ -28,4 +28,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -19,12 +19,15 @@ import l10nMessages from './index.messages';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
device: TrezorDevice; device: TrezorDevice,
account: $ElementType<$ElementType<BaseProps, 'selectedAccount'>, 'account'>; account: $ElementType<$ElementType<BaseProps, 'selectedAccount'>, 'account'>,
showAddress: $ElementType<$ElementType<BaseProps, 'receiveActions'>, 'showAddress'>; showAddress: $ElementType<$ElementType<BaseProps, 'receiveActions'>, 'showAddress'>,
showUnverifiedAddress: $ElementType<$ElementType<BaseProps, 'receiveActions'>, 'showUnverifiedAddress'>; showUnverifiedAddress: $ElementType<
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>; $ElementType<BaseProps, 'receiveActions'>,
} 'showUnverifiedAddress'
>,
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
};
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
position: absolute; position: absolute;
@ -35,7 +38,6 @@ const StyledLink = styled(Link)`
const Wrapper = styled.div` const Wrapper = styled.div`
max-width: 370px; max-width: 370px;
padding: 30px 0px; padding: 30px 0px;
`; `;
const Content = styled.div` const Content = styled.div`
@ -57,7 +59,7 @@ const Row = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
Button + Button { button + button {
margin-top: 10px; margin-top: 10px;
} }
`; `;
@ -119,15 +121,27 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
let claim; let claim;
if (!device.connected) { if (!device.connected) {
deviceStatus = <FormattedMessage {...l10nMessages.TR_DEVICE_LABEL_IS_NOT_CONNECTED} values={{ deviceLabel: device.label }} />; deviceStatus = (
<FormattedMessage
{...l10nMessages.TR_DEVICE_LABEL_IS_NOT_CONNECTED}
values={{ deviceLabel: device.label }}
/>
);
claim = <FormattedMessage {...l10nMessages.TR_PLEASE_CONNECT_YOUR_DEVICE} />; claim = <FormattedMessage {...l10nMessages.TR_PLEASE_CONNECT_YOUR_DEVICE} />;
} else { } else {
// corner-case where device is connected but it is unavailable because it was created with different "passphrase_protection" settings // corner-case where device is connected but it is unavailable because it was created with different "passphrase_protection" settings
const enable: boolean = !!(device.features && device.features.passphrase_protection); const enable: boolean = !!(device.features && device.features.passphrase_protection);
deviceStatus = <FormattedMessage {...l10nMessages.TR_DEVICE_LABEL_IS_UNAVAILABLE} values={{ deviceLabel: device.label }} />; deviceStatus = (
claim = enable <FormattedMessage
? <FormattedMessage {...l10nMessages.TR_PLEASE_ENABLE_PASSPHRASE} /> {...l10nMessages.TR_DEVICE_LABEL_IS_UNAVAILABLE}
: <FormattedMessage {...l10nMessages.TR_PLEASE_DISABLE_PASSPHRASE} />; values={{ deviceLabel: device.label }}
/>
);
claim = enable ? (
<FormattedMessage {...l10nMessages.TR_PLEASE_ENABLE_PASSPHRASE} />
) : (
<FormattedMessage {...l10nMessages.TR_PLEASE_DISABLE_PASSPHRASE} />
);
} }
const needsBackup = device.features && device.features.needs_backup; const needsBackup = device.features && device.features.needs_backup;
@ -138,7 +152,7 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
<StyledLink onClick={onCancel}> <StyledLink onClick={onCancel}>
<Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} /> <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
</StyledLink> </StyledLink>
<H2>{ deviceStatus }</H2> <H2>{deviceStatus}</H2>
<StyledP isSmaller> <StyledP isSmaller>
<FormattedMessage <FormattedMessage
{...l10nMessages.TR_TO_PREVENT_PHISHING_ATTACKS_COMMA} {...l10nMessages.TR_TO_PREVENT_PHISHING_ATTACKS_COMMA}
@ -148,8 +162,12 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
</Content> </Content>
<Content> <Content>
<Row> <Row>
<Button onClick={() => (!account ? this.verifyAddress() : 'false')}><FormattedMessage {...l10nMessages.TR_TRY_AGAIN} /></Button> <Button onClick={() => (!account ? this.verifyAddress() : 'false')}>
<WarnButton isWhite onClick={() => this.showUnverifiedAddress()}><FormattedMessage {...l10nMessages.TR_SHOW_UNVERIFIED_ADDRESS} /></WarnButton> <FormattedMessage {...l10nMessages.TR_TRY_AGAIN} />
</Button>
<WarnButton isWhite onClick={() => this.showUnverifiedAddress()}>
<FormattedMessage {...l10nMessages.TR_SHOW_UNVERIFIED_ADDRESS} />
</WarnButton>
</Row> </Row>
</Content> </Content>
{needsBackup && <Divider />} {needsBackup && <Divider />}
@ -157,7 +175,10 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
<> <>
<Content> <Content>
<H2>Device {device.label} is not backed up</H2> <H2>Device {device.label} is not backed up</H2>
<StyledP isSmaller>If your device is ever lost or damaged, your funds will be lost. Backup your device first, to protect your coins against such events.</StyledP> <StyledP isSmaller>
If your device is ever lost or damaged, your funds will be lost.
Backup your device first, to protect your coins against such events.
</StyledP>
</Content> </Content>
<Content> <Content>
<Row> <Row>
@ -181,4 +202,4 @@ ConfirmUnverifiedAddress.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
export default ConfirmUnverifiedAddress; export default ConfirmUnverifiedAddress;

@ -17,11 +17,13 @@ const definedMessages: Messages = defineMessages({
}, },
TR_PLEASE_ENABLE_PASSPHRASE: { TR_PLEASE_ENABLE_PASSPHRASE: {
id: 'TR_PLEASE_ENABLE_PASSPHRASE', id: 'TR_PLEASE_ENABLE_PASSPHRASE',
defaultMessage: 'Please enable passphrase settings to continue with the verification process.', defaultMessage:
'Please enable passphrase settings to continue with the verification process.',
}, },
TR_PLEASE_DISABLE_PASSPHRASE: { TR_PLEASE_DISABLE_PASSPHRASE: {
id: 'TR_PLEASE_DISABLE_PASSPHRASE', id: 'TR_PLEASE_DISABLE_PASSPHRASE',
defaultMessage: 'Please disable passphrase settings to continue with the verification process.', defaultMessage:
'Please disable passphrase settings to continue with the verification process.',
}, },
TR_SHOW_UNVERIFIED_ADDRESS: { TR_SHOW_UNVERIFIED_ADDRESS: {
id: 'TR_SHOW_UNVERIFIED_ADDRESS', id: 'TR_SHOW_UNVERIFIED_ADDRESS',
@ -34,8 +36,9 @@ const definedMessages: Messages = defineMessages({
}, },
TR_TO_PREVENT_PHISHING_ATTACKS_COMMA: { TR_TO_PREVENT_PHISHING_ATTACKS_COMMA: {
id: 'TR_TO_PREVENT_PHISHING_ATTACKS_COMMA', id: 'TR_TO_PREVENT_PHISHING_ATTACKS_COMMA',
defaultMessage: 'To prevent phishing attacks, you should verify the address on your Trezor first. {claim}', defaultMessage:
'To prevent phishing attacks, you should verify the address on your Trezor first. {claim}',
}, },
}); });
export default definedMessages; export default definedMessages;

@ -20,18 +20,18 @@ import type { TrezorDevice } from 'flowtype';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
device: TrezorDevice; device: TrezorDevice,
devices: $ElementType<BaseProps, 'devices'>; devices: $ElementType<BaseProps, 'devices'>,
onDuplicateDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onDuplicateDevice'>; onDuplicateDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onDuplicateDevice'>,
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>; onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
} };
type State = { type State = {
defaultName: string; defaultName: string,
instance: number; instance: number,
instanceName: ?string; instanceName: ?string,
isUsed: boolean; isUsed: boolean,
} };
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
position: absolute; position: absolute;
@ -102,14 +102,14 @@ class DuplicateDevice extends PureComponent<Props, State> {
onNameChange = (value: string): void => { onNameChange = (value: string): void => {
let isUsed: boolean = false; let isUsed: boolean = false;
if (value.length > 0) { if (value.length > 0) {
isUsed = (this.props.devices.find(d => d.instanceName === value) !== undefined); isUsed = this.props.devices.find(d => d.instanceName === value) !== undefined;
} }
this.setState({ this.setState({
instanceName: value.length > 0 ? value : null, instanceName: value.length > 0 ? value : null,
isUsed, isUsed,
}); });
} };
input: ?HTMLInputElement; input: ?HTMLInputElement;
@ -123,25 +123,27 @@ class DuplicateDevice extends PureComponent<Props, State> {
keyboardHandler: (event: KeyboardEvent) => void; keyboardHandler: (event: KeyboardEvent) => void;
submit() { submit() {
const extended: Object = { instanceName: this.state.instanceName, instance: this.state.instance }; const extended: Object = {
instanceName: this.state.instanceName,
instance: this.state.instance,
};
this.props.onDuplicateDevice({ ...this.props.device, ...extended }); this.props.onDuplicateDevice({ ...this.props.device, ...extended });
} }
render() { render() {
const { device, onCancel } = this.props; const { device, onCancel } = this.props;
const { const { defaultName, instanceName, isUsed } = this.state;
defaultName,
instanceName,
isUsed,
} = this.state;
return ( return (
<Wrapper> <Wrapper>
<StyledLink onClick={onCancel}> <StyledLink onClick={onCancel}>
<Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} /> <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
</StyledLink> </StyledLink>
<H3>Clone { device.label }?</H3> <H3>Clone {device.label}?</H3>
<StyledP isSmaller>This will create new instance of device which can be used with different passphrase</StyledP> <StyledP isSmaller>
This will create new instance of device which can be used with different
passphrase
</StyledP>
<Column> <Column>
<Label>Instance name</Label> <Label>Instance name</Label>
<Input <Input
@ -151,22 +153,20 @@ class DuplicateDevice extends PureComponent<Props, State> {
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
placeholder={defaultName} placeholder={defaultName}
innerRef={(element) => { this.input = element; }} innerRef={element => {
this.input = element;
}}
onChange={event => this.onNameChange(event.currentTarget.value)} onChange={event => this.onNameChange(event.currentTarget.value)}
value={instanceName} value={instanceName}
/> />
{ isUsed && <ErrorMessage>Instance name is already in use</ErrorMessage> } {isUsed && <ErrorMessage>Instance name is already in use</ErrorMessage>}
</Column> </Column>
<Column> <Column>
<StyledButton <StyledButton disabled={isUsed} onClick={() => this.submit()}>
disabled={isUsed} Create new instance
onClick={() => this.submit()}
>Create new instance
</StyledButton> </StyledButton>
<StyledButton <StyledButton isWhite onClick={onCancel}>
isWhite Cancel
onClick={onCancel}
>Cancel
</StyledButton> </StyledButton>
</Column> </Column>
</Wrapper> </Wrapper>
@ -181,4 +181,4 @@ DuplicateDevice.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
export default DuplicateDevice; export default DuplicateDevice;

@ -17,10 +17,13 @@ import l10nMessages from './index.messages';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
device: TrezorDevice; device: TrezorDevice,
onForgetSingleDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onForgetSingleDevice'>; onForgetSingleDevice: $ElementType<
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>; $ElementType<BaseProps, 'modalActions'>,
} 'onForgetSingleDevice'
>,
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
};
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px; width: 360px;
@ -35,7 +38,7 @@ const Row = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
Button + Button { button + button {
margin-top: 10px; margin-top: 10px;
} }
`; `;
@ -75,11 +78,17 @@ class ForgetDevice extends PureComponent<Props> {
/> />
</H2> </H2>
<StyledP isSmaller> <StyledP isSmaller>
<FormattedMessage {...l10nMessages.TR_FORGETTING_ONLY_REMOVES_THE_DEVICE_FROM} /> <FormattedMessage
{...l10nMessages.TR_FORGETTING_ONLY_REMOVES_THE_DEVICE_FROM}
/>
</StyledP> </StyledP>
<Row> <Row>
<Button onClick={() => this.forget()}><FormattedMessage {...l10nCommonMessages.TR_FORGET_DEVICE} /></Button> <Button onClick={() => this.forget()}>
<Button isWhite onClick={this.props.onCancel}><FormattedMessage {...l10nMessages.TR_DONT_FORGET} /></Button> <FormattedMessage {...l10nCommonMessages.TR_FORGET_DEVICE} />
</Button>
<Button isWhite onClick={this.props.onCancel}>
<FormattedMessage {...l10nMessages.TR_DONT_FORGET} />
</Button>
</Row> </Row>
</Wrapper> </Wrapper>
); );
@ -92,4 +101,4 @@ ForgetDevice.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
export default ForgetDevice; export default ForgetDevice;

@ -5,13 +5,14 @@ import type { Messages } from 'flowtype/npm/react-intl';
const definedMessages: Messages = defineMessages({ const definedMessages: Messages = defineMessages({
TR_DONT_FORGET: { TR_DONT_FORGET: {
id: 'TR_DONT_FORGET', id: 'TR_DONT_FORGET',
defaultMessage: 'Don\'t forget', defaultMessage: "Don't forget",
description: 'Button in remember/forget dialog', description: 'Button in remember/forget dialog',
}, },
TR_FORGETTING_ONLY_REMOVES_THE_DEVICE_FROM: { TR_FORGETTING_ONLY_REMOVES_THE_DEVICE_FROM: {
id: 'TR_FORGETTING_ONLY_REMOVES_THE_DEVICE_FROM', id: 'TR_FORGETTING_ONLY_REMOVES_THE_DEVICE_FROM',
defaultMessage: 'Forgetting only removes the device from the list on the left, your coins are still safe and you can access them by reconnecting your Trezor again.', defaultMessage:
'Forgetting only removes the device from the list on the left, your coins are still safe and you can access them by reconnecting your Trezor again.',
}, },
}); });
export default definedMessages; export default definedMessages;

@ -17,16 +17,16 @@ import l10nMessages from './index.messages';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
device: TrezorDevice; device: TrezorDevice,
instances: ?Array<TrezorDevice>; instances: ?Array<TrezorDevice>,
onRememberDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onRememberDevice'>; onRememberDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onRememberDevice'>,
onForgetDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onForgetDevice'>; onForgetDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onForgetDevice'>,
} };
type State = { type State = {
countdown: number; countdown: number,
ticker?: number; ticker?: number,
} };
const ButtonContent = styled.div` const ButtonContent = styled.div`
display: flex; display: flex;
@ -52,7 +52,7 @@ const Column = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
Button + Button { button + button {
margin-top: 10px; margin-top: 10px;
} }
`; `;
@ -120,7 +120,7 @@ class RememberDevice extends PureComponent<Props, State> {
let { label } = device; let { label } = device;
const deviceCount = instances ? instances.length : 0; const deviceCount = instances ? instances.length : 0;
if (instances && instances.length > 0) { if (instances && instances.length > 0) {
label = instances.map(instance => (instance.instanceLabel)).join(','); label = instances.map(instance => instance.instanceLabel).join(',');
} }
return ( return (
<Wrapper> <Wrapper>
@ -154,10 +154,7 @@ class RememberDevice extends PureComponent<Props, State> {
/> />
</ButtonContent> </ButtonContent>
</Button> </Button>
<Button <Button isWhite onClick={() => onRememberDevice(device)}>
isWhite
onClick={() => onRememberDevice(device)}
>
<FormattedMessage {...l10nMessages.TR_REMEMBER_DEVICE} /> <FormattedMessage {...l10nMessages.TR_REMEMBER_DEVICE} />
</Button> </Button>
</Column> </Column>
@ -173,4 +170,4 @@ RememberDevice.propTypes = {
onForgetDevice: PropTypes.func.isRequired, onForgetDevice: PropTypes.func.isRequired,
}; };
export default RememberDevice; export default RememberDevice;

@ -5,7 +5,8 @@ import type { Messages } from 'flowtype/npm/react-intl';
const definedMessages: Messages = defineMessages({ const definedMessages: Messages = defineMessages({
TR_WOULD_YOU_LIKE_TREZOR_WALLET_TO: { TR_WOULD_YOU_LIKE_TREZOR_WALLET_TO: {
id: 'TR_WOULD_YOU_LIKE_TREZOR_WALLET_TO', id: 'TR_WOULD_YOU_LIKE_TREZOR_WALLET_TO',
defaultMessage: 'Would you like Trezor Wallet to forget your {deviceCount, plural, one {device} other {devices}} or to remember {deviceCount, plural, one {it} other {them}}, so that it is still visible even while disconnected?', defaultMessage:
'Would you like Trezor Wallet to forget your {deviceCount, plural, one {device} other {devices}} or to remember {deviceCount, plural, one {it} other {them}}, so that it is still visible even while disconnected?',
}, },
TR_REMEMBER_DEVICE: { TR_REMEMBER_DEVICE: {
id: 'TR_REMEMBER_DEVICE', id: 'TR_REMEMBER_DEVICE',
@ -13,4 +14,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -23,13 +23,15 @@ import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
intl: any, intl: any,
device: TrezorDevice; device: TrezorDevice,
onWalletTypeRequest: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onWalletTypeRequest'>; onWalletTypeRequest: $ElementType<
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>; $ElementType<BaseProps, 'modalActions'>,
} 'onWalletTypeRequest'
>,
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
};
const Wrapper = styled.div` const Wrapper = styled.div``;
`;
const Header = styled.div` const Header = styled.div`
display: flex; display: flex;
@ -71,10 +73,12 @@ const Content = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
${props => props.isTop && css` ${props =>
padding-top: 40px; props.isTop &&
border-bottom: 1px solid ${colors.DIVIDER}; css`
`} padding-top: 40px;
border-bottom: 1px solid ${colors.DIVIDER};
`}
`; `;
class WalletType extends PureComponent<Props> { class WalletType extends PureComponent<Props> {
@ -105,17 +109,13 @@ class WalletType extends PureComponent<Props> {
return ( return (
<Wrapper> <Wrapper>
{ device.state && ( {device.state && (
<StyledLink onClick={onCancel}> <StyledLink onClick={onCancel}>
<Icon <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
size={24}
color={colors.TEXT_SECONDARY}
icon={icons.CLOSE}
/>
</StyledLink> </StyledLink>
)} )}
<StyledHeading>{ device.state <StyledHeading>
? ( {device.state ? (
<FormattedMessage <FormattedMessage
{...l10nMessages.TR_CHANGE_WALLET_TYPE_FOR} {...l10nMessages.TR_CHANGE_WALLET_TYPE_FOR}
values={{ deviceLabel: device.instanceLabel }} values={{ deviceLabel: device.instanceLabel }}
@ -143,25 +143,21 @@ class WalletType extends PureComponent<Props> {
<Tooltip <Tooltip
maxWidth={285} maxWidth={285}
placement="top" placement="top"
content={this.props.intl.formatMessage(l10nMessages.TR_PASSPHRASE_IS_OPTIONAL_FEATURE)} content={this.props.intl.formatMessage(
l10nMessages.TR_PASSPHRASE_IS_OPTIONAL_FEATURE
)}
readMoreLink="https://wiki.trezor.io/Passphrase" readMoreLink="https://wiki.trezor.io/Passphrase"
> >
<StyledIcon <StyledIcon icon={icons.HELP} color={colors.TEXT_SECONDARY} size={26} />
icon={icons.HELP}
color={colors.TEXT_SECONDARY}
size={26}
/>
</Tooltip> </Tooltip>
<Header> <Header>
<WalletTypeIcon <WalletTypeIcon type="hidden" size={32} color={colors.TEXT_PRIMARY} />
type="hidden"
size={32}
color={colors.TEXT_PRIMARY}
/>
<FormattedMessage {...l10nMessages.TR_HIDDEN_WALLET} /> <FormattedMessage {...l10nMessages.TR_HIDDEN_WALLET} />
</Header> </Header>
<P isSmaller> <P isSmaller>
<FormattedMessage {...l10nMessages.TR_ASKED_ENTER_YOUR_PASSPHRASE_TO_UNLOCK} /> <FormattedMessage
{...l10nMessages.TR_ASKED_ENTER_YOUR_PASSPHRASE_TO_UNLOCK}
/>
</P> </P>
<StyledButton isWhite onClick={() => onWalletTypeRequest(true)}> <StyledButton isWhite onClick={() => onWalletTypeRequest(true)}>
<FormattedMessage {...l10nCommonMessages.TR_GO_TO_HIDDEN_WALLET} /> <FormattedMessage {...l10nCommonMessages.TR_GO_TO_HIDDEN_WALLET} />
@ -178,4 +174,4 @@ WalletType.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
export default injectIntl(WalletType); export default injectIntl(WalletType);

@ -25,7 +25,8 @@ const definedMessages: Messages = defineMessages({
}, },
TR_PASSPHRASE_IS_OPTIONAL_FEATURE: { TR_PASSPHRASE_IS_OPTIONAL_FEATURE: {
id: 'TR_PASSPHRASE_IS_OPTIONAL_FEATURE', id: 'TR_PASSPHRASE_IS_OPTIONAL_FEATURE',
defaultMessage: 'Passphrase is an optional feature of the Trezor device that is recommended for advanced users only. It is a word or a sentence of your choice. Its main purpose is to access a hidden wallet.', defaultMessage:
'Passphrase is an optional feature of the Trezor device that is recommended for advanced users only. It is a word or a sentence of your choice. Its main purpose is to access a hidden wallet.',
}, },
TR_ASKED_ENTER_YOUR_PASSPHRASE_TO_UNLOCK: { TR_ASKED_ENTER_YOUR_PASSPHRASE_TO_UNLOCK: {
id: 'TR_ASKED_ENTER_YOUR_PASSPHRASE_TO_UNLOCK', id: 'TR_ASKED_ENTER_YOUR_PASSPHRASE_TO_UNLOCK',
@ -33,4 +34,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -9,4 +9,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -20,8 +20,8 @@ import CardanoImage from './images/cardano.png';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>; onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
} };
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@ -51,11 +51,7 @@ const Img = styled.img`
const CardanoWallet = (props: Props) => ( const CardanoWallet = (props: Props) => (
<Wrapper> <Wrapper>
<StyledLink onClick={props.onCancel}> <StyledLink onClick={props.onCancel}>
<Icon <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
size={24}
color={colors.TEXT_SECONDARY}
icon={icons.CLOSE}
/>
</StyledLink> </StyledLink>
<Img src={CardanoImage} /> <Img src={CardanoImage} />
<H2> <H2>
@ -77,4 +73,4 @@ CardanoWallet.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
export default CardanoWallet; export default CardanoWallet;

@ -9,4 +9,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -18,8 +18,8 @@ import NemImage from './images/nem-download.png';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>; onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
} };
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@ -47,11 +47,7 @@ const Img = styled.img`
const NemWallet = (props: Props) => ( const NemWallet = (props: Props) => (
<Wrapper> <Wrapper>
<StyledLink onClick={props.onCancel}> <StyledLink onClick={props.onCancel}>
<Icon <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
size={24}
color={colors.TEXT_SECONDARY}
icon={icons.CLOSE}
/>
</StyledLink> </StyledLink>
<H2> <H2>
<FormattedMessage {...l10nMessages.TR_NEM_WALLET} /> <FormattedMessage {...l10nMessages.TR_NEM_WALLET} />
@ -75,4 +71,4 @@ NemWallet.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
export default NemWallet; export default NemWallet;

@ -9,7 +9,8 @@ const definedMessages: Messages = defineMessages({
}, },
TR_WE_HAVE_PARTNERED_UP_WITH_THE_NEM: { TR_WE_HAVE_PARTNERED_UP_WITH_THE_NEM: {
id: 'TR_WE_HAVE_PARTNERED_UP_WITH_THE_NEM', id: 'TR_WE_HAVE_PARTNERED_UP_WITH_THE_NEM',
defaultMessage: 'We have partnered up with the NEM Foundation to provide you with a full-fledged NEM Wallet.', defaultMessage:
'We have partnered up with the NEM Foundation to provide you with a full-fledged NEM Wallet.',
}, },
TR_MAKE_SURE_YOU_DOWNLOAD_THE_UNIVERSAL: { TR_MAKE_SURE_YOU_DOWNLOAD_THE_UNIVERSAL: {
id: 'TR_MAKE_SURE_YOU_DOWNLOAD_THE_UNIVERSAL', id: 'TR_MAKE_SURE_YOU_DOWNLOAD_THE_UNIVERSAL',
@ -21,4 +22,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

@ -20,8 +20,8 @@ import StellarImage from './images/xlm.png';
import type { Props as BaseProps } from '../../Container'; import type { Props as BaseProps } from '../../Container';
type Props = { type Props = {
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>; onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>,
} };
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@ -51,11 +51,7 @@ const Img = styled.img`
const StellarWallet = (props: Props) => ( const StellarWallet = (props: Props) => (
<Wrapper> <Wrapper>
<StyledLink onClick={props.onCancel}> <StyledLink onClick={props.onCancel}>
<Icon <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
size={24}
color={colors.TEXT_SECONDARY}
icon={icons.CLOSE}
/>
</StyledLink> </StyledLink>
<Img src={StellarImage} /> <Img src={StellarImage} />
<H2> <H2>
@ -77,4 +73,4 @@ StellarWallet.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
export default StellarWallet; export default StellarWallet;

@ -9,4 +9,4 @@ const definedMessages: Messages = defineMessages({
}, },
}); });
export default definedMessages; export default definedMessages;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save