Merge branch 'release/1.1.0-beta' into beta

pull/404/head
Vladimir Volek 5 years ago
commit 7f0d3b507c

@ -1,31 +0,0 @@
{
"presets": [
["env", {
"useBuiltIns": true,
"loose": true
}],
"react"
],
"plugins": [
"react-hot-loader/babel",
"transform-class-properties",
"transform-object-rest-spread",
"transform-flow-strip-types",
["transform-runtime", {
"polyfill": false,
"regenerator": true
}],
["module-resolver", {
"root": ["./src"],
"alias": {
"public": ["./public"]
}
}],
"babel-plugin-styled-components"
],
"env": {
"test": {
"presets": ["jest"]
}
}
}

@ -6,16 +6,19 @@
], ],
"globals": { "globals": {
"LOCAL": true, "LOCAL": true,
"COMMITHASH": true "COMMITHASH": true,
"VERSION": true
}, },
"env": { "env": {
"browser": true, "browser": true,
"jest": true "jest": true,
"cypress/globals": true
}, },
"rules": { "rules": {
"import/prefer-default-export": 0, "import/prefer-default-export": 0,
"no-use-before-define": 0, "no-use-before-define": 0,
"no-plusplus": 0, "no-plusplus": 0,
"jest/no-disabled-tests": 0,
"class-methods-use-this": 0, "class-methods-use-this": 0,
"react/require-default-props": 0, "react/require-default-props": 0,
"react/forbid-prop-types": 0, "react/forbid-prop-types": 0,
@ -32,13 +35,17 @@
"new-cap": 0, "new-cap": 0,
"max-len": 0, "max-len": 0,
"eol-last": 0, "eol-last": 0,
"spaced-comment": 0 "spaced-comment": 0,
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": 2
}, },
"plugins": [ "plugins": [
"import", "import",
"react", "react",
"jest", "jest",
"flowtype" "flowtype",
"cypress",
"chai-friendly"
], ],
"settings": { "settings": {
"import/resolver": { "import/resolver": {
@ -52,8 +59,7 @@
"ecmaVersion": 7, "ecmaVersion": 7,
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true, "jsx": true
"experimentalObjectRestSpread": true
} }
} }
} }

@ -10,6 +10,7 @@
.*/_old/.* .*/_old/.*
.*/scripts/solidity/.* .*/scripts/solidity/.*
.*/build/.* .*/build/.*
.*/cache/.*
[libs] [libs]
./src/flowtype/npm/redux_v3.x.x.js ./src/flowtype/npm/redux_v3.x.x.js

4
.gitignore vendored

@ -19,4 +19,6 @@ logs
_old _old
coverage coverage
test/**/__diff_output__
test/screenshots

@ -1,14 +1,19 @@
image: node:9.3 image: node:10.15.1
variables:
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"
cache: cache:
key: ${CI_COMMIT_REF_SLUG} key: ${CI_COMMIT_REF_SLUG}
paths: paths:
- node_modules/ - node_modules
- ${CYPRESS_CACHE_FOLDER}
stages: stages:
- test - test
- build - build
- deploy - deploy
- integration tests
lint: lint:
stage: test stage: test
@ -62,7 +67,23 @@ build stable:
expire_in: 1 week expire_in: 1 week
paths: paths:
- build/stable - build/stable
- scripts/s3sync.sh - scripts/s3sync.sh
build emulator and bridge image:
variables:
CONTAINER_NAME: "$CI_REGISTRY/emulator-bridge-tests"
image: docker:latest
services:
- docker:dind
before_script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
stage: build
when: manual
script:
- docker pull $CONTAINER_NAME:latest || true
- docker build --cache-from $CONTAINER_NAME:latest --tag $CONTAINER_NAME:$CI_COMMIT_SHA --tag $CONTAINER_NAME:latest .
- docker push $CONTAINER_NAME:$CI_COMMIT_SHA
- docker push $CONTAINER_NAME:latest
deploy review: deploy review:
stage: deploy stage: deploy
@ -138,3 +159,24 @@ delete review:
- branches - branches
tags: tags:
- deploy - deploy
integration tests:
image: docker:latest
services:
- docker:dind
stage: integration tests
script:
- 'export SHARED_PATH="$(dirname ${CI_PROJECT_DIR})/shared"'
- rm -r ${SHARED_PATH} || true
- docker build -t wallet-emulator-bridge-tests .
- mkdir -p ${SHARED_PATH}/trezor-wallet/screenshots
- mkdir -p ${SHARED_PATH}/trezor-wallet/videos
- docker run --volume ${SHARED_PATH}/trezor-wallet/screenshots:/trezor-wallet/test/screenshots --volume ${SHARED_PATH}/trezor-wallet/videos:/trezor-wallet/test/videos --rm wallet-emulator-bridge-tests
- find ${SHARED_PATH}
- mkdir trezor-wallet
- cp -r ${SHARED_PATH}/ trezor-wallet/
artifacts:
when: always
expire_in: 1 week
paths:
- trezor-wallet/

@ -1,3 +1,27 @@
## 1.1.0-beta
__added__
- Ripple support
- responsive sidebar
- QR code scanner in send form
- clear send form button
- backup notification modal
__updated__
- connect v7
- babel v7
- ethereum tokens list
- most of dependencies
__changed__
- icons for T1 and TT
- device header styles
- input styles
- sign and verify title
__fixed__
- beta disclaimer wrapper position
- sidebar scrollbar
## 1.0.3-beta ## 1.0.3-beta
__added__ __added__
- Ethereum: sign & verify tab - Ethereum: sign & verify tab

@ -1,17 +1,50 @@
FROM node:9.3 FROM python:latest
ARG BUILD_TYPE=stable #
# setup
#
RUN apt-get update
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y chromium libappindicator3-1 xdg-utils fonts-liberation nodejs wget dpkg git python python3 python3-pip xvfb libgtk2.0-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2
RUN npm install -g yarn
WORKDIR /trezor-wallet-app RUN ln -s /usr/bin/chromium /usr/local/bin/chromium-browser
COPY package.json /trezor-wallet-app #
COPY yarn.lock /trezor-wallet-app # build emulator
#
RUN mkdir /trezor-emulator
WORKDIR /trezor-emulator
RUN yarn install RUN git clone https://github.com/trezor/trezor-core
WORKDIR /trezor-emulator/trezor-core
RUN git submodule update --init --recursive
COPY . /trezor-wallet-app RUN apt-get install libusb-1.0-0
RUN pip3 install scons trezor
RUN make build_unix_noui
RUN yarn run build:${BUILD_TYPE} #
# install bridge
#
RUN mkdir /trezor-bridge
WORKDIR /trezor-bridge
RUN wget https://wallet.trezor.io/data/bridge/2.0.25/trezor-bridge_2.0.25_amd64.deb
RUN dpkg -x /trezor-bridge/trezor-bridge_2.0.25_amd64.deb /trezor-bridge/extracted
EXPOSE 8080 #
CMD [ "yarn", "run", "prod-server" ] # install trezor-wallet
#
RUN mkdir /trezor-wallet
WORKDIR /trezor-wallet
COPY package.json /trezor-wallet
COPY yarn.lock /trezor-wallet
RUN yarn
COPY . /trezor-wallet
RUN yarn run build:stable
#
# run
#
ENTRYPOINT ["/trezor-wallet/test/scripts/run-all.sh"]
EXPOSE 8080 21325

@ -1,13 +1,11 @@
# Trezor Wallet # Trezor Wallet
You can try this wallet live [HERE](https://beta-wallet.trezor.io/next/)
To install dependencies run `npm install` or `yarn` To install dependencies run `npm install` or `yarn`
To start locally run `npm run dev` or `yarn run dev` To start locally run `npm run dev` or `yarn run dev`
To build the project run `npm run build` or `yarn run build` To build the project run `npm run build` or `yarn run build`
## Docker
- Build `./scripts/docker-build.sh`
- Run `./scripts/docker-run.sh`
## Project structure ## Project structure
The project is divided into two parts - data that are used when compiling the project and data that aren't. The project is divided into two parts - data that are used when compiling the project and data that aren't.

@ -0,0 +1,51 @@
module.exports = (api) => {
// api.cache.forever();
const presets = [
[
'@babel/preset-env',
{
useBuiltIns: 'entry',
loose: true,
},
],
'@babel/preset-react',
'@babel/preset-flow',
];
const plugins = [
'react-hot-loader/babel',
'@babel/plugin-transform-flow-strip-types',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread',
[
'@babel/plugin-transform-runtime',
{
regenerator: true,
},
],
[
'module-resolver',
{
root: [
'./src',
],
alias: {
public: [
'./public',
],
},
},
],
'babel-plugin-styled-components',
];
if (api.env('test')) {
presets.push('jest');
}
return {
presets,
plugins,
};
};

@ -0,0 +1,11 @@
{
"integrationFolder": "test/integration",
"fixturesFolder": "test/fixtures",
"pluginsFile": "test/plugins/index.js",
"supportFile": "test/support/index.js",
"defaultCommandTimeout": 10000,
"screenshotsFolder": "test/screenshots",
"video": false,
"trashAssetsBeforeRuns": true,
"chromeWebSecurity": false
}

@ -17,4 +17,7 @@ module.exports = {
setupFiles: [ setupFiles: [
'./support/setupJest.js', './support/setupJest.js',
], ],
transform: {
'^.+\\.jsx?$': 'babel-jest',
},
}; };

@ -1,6 +1,6 @@
{ {
"name": "trezor-wallet", "name": "trezor-wallet",
"version": "1.0.0", "version": "1.0.3-beta",
"author": "TREZOR <info@trezor.io>", "author": "TREZOR <info@trezor.io>",
"description": "", "description": "",
"bin": { "bin": {
@ -21,100 +21,111 @@
"test": "run-s test:*", "test": "run-s test:*",
"test:unit": "npx jest", "test:unit": "npx jest",
"test-unit:watch": "npx jest -o --watch", "test-unit:watch": "npx jest -o --watch",
"test-integration:dev": "npx cypress open -c baseUrl=http://localhost:8081/#/",
"test-integration:test": "npx cypress run",
"test-integration:gitlab": "npx cypress run -c baseUrl=https://localhost:8080/#/ --browser chromium",
"server:beta": "node ./server/index.js --buildType=beta", "server:beta": "node ./server/index.js --buildType=beta",
"server:stable": "node ./server/index.js --buildType=stable" "server:stable": "node ./server/index.js --buildType=stable"
}, },
"dependencies": { "dependencies": {
"babel": "^6.23.0", "@babel/polyfill": "^7.2.5",
"babel-core": "^6.26.3", "bignumber.js": "8.0.2",
"bignumber.js": "2.4.0",
"color-hash": "^1.0.3", "color-hash": "^1.0.3",
"commander": "^2.19.0", "commander": "^2.19.0",
"connected-react-router": "^6.0.0", "connected-react-router": "6.0.0",
"copy-webpack-plugin": "^4.5.2", "copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"date-fns": "^1.29.0", "date-fns": "^1.30.1",
"ethereumjs-tx": "^1.3.3", "ethereumjs-tx": "^1.3.7",
"ethereumjs-units": "^0.2.0", "ethereumjs-units": "^0.2.0",
"ethereumjs-util": "^5.1.4", "ethereumjs-util": "^6.0.0",
"express": "^4.16.3", "express": "^4.16.4",
"flow-webpack-plugin": "^1.2.0",
"git-revision-webpack-plugin": "^3.0.3", "git-revision-webpack-plugin": "^3.0.3",
"hdkey": "^0.8.0", "hdkey": "^1.1.0",
"history": "^4.7.2", "history": "^4.7.2",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest-fetch-mock": "^1.6.5", "jest-fetch-mock": "^2.1.0",
"morgan": "^1.9.1", "morgan": "^1.9.1",
"npm-run-all": "^4.1.3", "npm-run-all": "^4.1.5",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"raf": "^3.4.0", "raf": "^3.4.1",
"raven-js": "^3.22.3", "raven-js": "^3.27.0",
"rc-tooltip": "^3.7.0", "rc-tooltip": "^3.7.3",
"react": "^16.6.3", "react": "^16.8.1",
"react-dom": "^16.6.3", "react-dom": "^16.8.1",
"react-hot-loader": "^4.6.2", "react-hot-loader": "^4.6.5",
"react-json-view": "^1.19.1", "react-json-view": "^1.19.1",
"react-qr-reader": "^2.1.2",
"react-qr-svg": "^2.1.0", "react-qr-svg": "^2.1.0",
"react-redux": "^6.0.0", "react-redux": "^6.0.0",
"react-router": "^4.3.1", "react-router": "^4.3.1",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.3.1",
"react-scale-text": "^1.2.2", "react-scale-text": "^1.2.2",
"react-select": "^2.2.0", "react-select": "^2.3.0",
"react-textarea-autosize": "^7.0.4", "react-textarea-autosize": "^7.1.0",
"react-transition-group": "^2.4.0", "react-transition-group": "^2.5.3",
"redbox-react": "^1.6.0", "redbox-react": "^1.6.0",
"redux": "4.0.0", "redux": "4.0.1",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-raven-middleware": "^1.2.0", "redux-raven-middleware": "^1.2.0",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.3.0",
"rimraf": "^2.6.2", "request": "^2.88.0",
"styled-components": "^4.1.2", "rimraf": "^2.6.3",
"styled-normalize": "^8.0.4", "styled-components": "^4.1.3",
"trezor-connect": "7.0.0-beta.1", "styled-normalize": "^8.0.6",
"trezor-bridge-communicator": "1.0.2",
"trezor-connect": "7.0.0-beta.3",
"wallet-address-validator": "^0.2.4", "wallet-address-validator": "^0.2.4",
"web3": "1.0.0-beta.35", "web3": "1.0.0-beta.38",
"webpack": "^4.16.3", "webpack": "^4.29.3",
"webpack-build-notifier": "^0.1.29", "webpack-build-notifier": "^0.1.30",
"webpack-bundle-analyzer": "^2.13.1", "webpack-bundle-analyzer": "^3.0.3",
"whatwg-fetch": "^2.0.4", "whatwg-fetch": "^3.0.0",
"yarn-run-all": "^3.1.1" "yarn-run-all": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.24.1", "@babel/cli": "^7.2.3",
"babel-eslint": "^8.2.6", "@babel/core": "^7.2.2",
"babel-loader": "^7.1.5", "@babel/plugin-proposal-class-properties": "^7.3.0",
"babel-plugin-module-resolver": "^3.1.1", "@babel/plugin-proposal-object-rest-spread": "^7.3.2",
"babel-plugin-styled-components": "^1.5.1", "@babel/plugin-transform-flow-strip-types": "^7.2.3",
"babel-plugin-transform-class-properties": "^6.24.1", "@babel/plugin-transform-runtime": "^7.2.0",
"babel-plugin-transform-flow-strip-types": "^6.22.0", "@babel/preset-env": "^7.3.1",
"babel-plugin-transform-object-rest-spread": "^6.23.0", "@babel/preset-flow": "^7.0.0",
"babel-plugin-transform-runtime": "^6.23.0", "@babel/preset-react": "^7.0.0",
"babel-preset-env": "^1.6.0", "@babel/register": "^7.0.0",
"babel-preset-jest": "^23.2.0", "babel-eslint": "^10.0.1",
"babel-preset-react": "^6.24.1", "babel-jest": "^24.1.0",
"eslint": "^4", "babel-loader": "^8.0.5",
"eslint-config-airbnb": "^17.0.0", "babel-plugin-module-resolver": "^3.1.3",
"eslint-import-resolver-babel-module": "^4.0.0", "babel-plugin-styled-components": "^1.10.0",
"eslint-loader": "^2.1.0", "cypress": "^3.1.5",
"eslint-plugin-flowtype": "^2.50.0", "cypress-image-snapshot": "^3.0.0",
"eslint-plugin-import": "^2.14.0", "eslint": "^5.13.0",
"eslint-plugin-jest": "^21.18.0", "eslint-config-airbnb": "^17.1.0",
"eslint-plugin-jsx-a11y": "^6.1.1", "eslint-import-resolver-babel-module": "^5.0.1",
"eslint-plugin-react": "^7.10.0", "eslint-loader": "^2.1.2",
"file-loader": "1.1.11", "eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-cypress": "^2.2.0",
"eslint-plugin-flowtype": "^3.2.1",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jest": "^22.2.2",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.12.4",
"file-loader": "3.0.1",
"flow-bin": "0.75.0", "flow-bin": "0.75.0",
"jest": "^23.4.2", "jest": "^24.1.0",
"stylelint": "^8.0.0", "stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"stylelint-config-styled-components": "^0.1.1", "stylelint-config-styled-components": "^0.1.1",
"stylelint-custom-processor-loader": "^0.5.0", "stylelint-custom-processor-loader": "^0.6.0",
"stylelint-processor-styled-components": "^1.3.2", "stylelint-processor-styled-components": "^1.5.2",
"stylelint-webpack-plugin": "^0.10.5", "stylelint-webpack-plugin": "^0.10.5",
"webpack-cli": "^2.1.3", "webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.1.4", "webpack-dev-server": "^3.1.14",
"yargs": "11.0.0" "yargs": "12.0.5"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "*" "fsevents": "1.2.7"
} }
} }

File diff suppressed because it is too large Load Diff

@ -19,6 +19,9 @@ export type BlockchainAction = {
type: typeof BLOCKCHAIN.UPDATE_FEE, type: typeof BLOCKCHAIN.UPDATE_FEE,
shortcut: string, shortcut: string,
feeLevels: Array<BlockchainFeeLevel>, feeLevels: Array<BlockchainFeeLevel>,
} | {
type: typeof BLOCKCHAIN.START_SUBSCRIBE,
shortcut: string,
} }
// Conditionally subscribe to blockchain backend // Conditionally subscribe to blockchain backend
@ -52,6 +55,11 @@ export const subscribe = (networkName: string): PromiseAction<void> => async (di
const network = config.networks.find(c => c.shortcut === networkName); const network = config.networks.find(c => c.shortcut === networkName);
if (!network) return; if (!network) return;
dispatch({
type: BLOCKCHAIN.START_SUBSCRIBE,
shortcut: network.shortcut,
});
switch (network.type) { switch (network.type) {
case 'ethereum': case 'ethereum':
await dispatch(EthereumBlockchainActions.subscribe(networkName)); await dispatch(EthereumBlockchainActions.subscribe(networkName));

@ -205,7 +205,7 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
} }
// handle outdated firmware error // handle outdated firmware error
if (error.message === UI.FIRMWARE) { if (error.message === UI.FIRMWARE_OLD) {
dispatch({ dispatch({
type: DISCOVERY.FIRMWARE_OUTDATED, type: DISCOVERY.FIRMWARE_OUTDATED,
device, device,

@ -1,3 +1,4 @@
/* eslint-disable import/no-named-as-default-member */
/* @flow */ /* @flow */
import TrezorConnect, { UI } from 'trezor-connect'; import TrezorConnect, { UI } from 'trezor-connect';
@ -9,6 +10,10 @@ import type {
ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice, ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice,
} from 'flowtype'; } from 'flowtype';
import type { State } from 'reducers/ModalReducer'; import type { State } from 'reducers/ModalReducer';
import type { parsedURI } from 'utils/cryptoUriParser';
import sendEthereumFormActions from './ethereum/SendFormActions';
import sendRippleFormActions from './ripple/SendFormActions';
export type ModalAction = { export type ModalAction = {
type: typeof MODAL.CLOSE type: typeof MODAL.CLOSE
@ -16,8 +21,11 @@ export type ModalAction = {
type: typeof MODAL.OPEN_EXTERNAL_WALLET, type: typeof MODAL.OPEN_EXTERNAL_WALLET,
id: string, id: string,
url: string, 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 });
return { return {
@ -51,6 +59,17 @@ export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (di
}); });
}; };
export const onReceiveConfirmation = (confirmation: any): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
await TrezorConnect.uiResponse({
type: UI.RECEIVE_CONFIRMATION,
payload: confirmation,
});
dispatch({
type: MODAL.CLOSE,
});
};
export const onRememberDevice = (device: TrezorDevice): Action => ({ export const onRememberDevice = (device: TrezorDevice): Action => ({
type: CONNECT.REMEMBER, type: CONNECT.REMEMBER,
device, device,
@ -139,9 +158,33 @@ export const gotoExternalWallet = (id: string, url: string): ThunkAction => (dis
}); });
}; };
export const openQrModal = (): ThunkAction => (dispatch: Dispatch): void => {
dispatch({
type: MODAL.OPEN_SCAN_QR,
});
};
export const onQrScan = (parsedUri: parsedURI, networkType: string): ThunkAction => (dispatch: Dispatch): void => {
const { address = '', amount } = parsedUri;
switch (networkType) {
case 'ethereum':
dispatch(sendEthereumFormActions.onAddressChange(address));
if (amount) dispatch(sendEthereumFormActions.onAmountChange(amount));
break;
case 'ripple':
dispatch(sendRippleFormActions.onAddressChange(address));
if (amount) dispatch(sendRippleFormActions.onAmountChange(amount));
break;
default:
break;
}
};
export default { export default {
onPinSubmit, onPinSubmit,
onPassphraseSubmit, onPassphraseSubmit,
onReceiveConfirmation,
onRememberDevice, onRememberDevice,
onForgetDevice, onForgetDevice,
onForgetSingleDevice, onForgetSingleDevice,
@ -149,4 +192,6 @@ export default {
onDuplicateDevice, onDuplicateDevice,
onWalletTypeRequest, onWalletTypeRequest,
gotoExternalWallet, gotoExternalWallet,
openQrModal,
onQrScan,
}; };

@ -94,6 +94,10 @@ export const showAddress = (path: Array<number>): AsyncAction => async (dispatch
type: RECEIVE.HIDE_ADDRESS, type: RECEIVE.HIDE_ADDRESS,
}); });
// special case: device no-backup permissions not granted
// $FlowIssue: remove this after trezor-connect@7.0.0 release
if (response.payload.code === 403) return;
dispatch({ dispatch({
type: NOTIFICATION.ADD, type: NOTIFICATION.ADD,
payload: { payload: {

@ -347,6 +347,17 @@ export const gotoFirmwareUpdate = (): ThunkAction => (dispatch: Dispatch, getSta
dispatch(goto(`/device/${devUrl}/firmware-update`)); dispatch(goto(`/device/${devUrl}/firmware-update`));
}; };
/*
* Go to NoBackup page
*/
export const gotoBackup = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const { selectedDevice } = getState().wallet;
if (!selectedDevice || !selectedDevice.features) return;
const devUrl: string = `${selectedDevice.features.device_id}${selectedDevice.instance ? `:${selectedDevice.instance}` : ''}`;
dispatch(goto(`/device/${devUrl}/backup`));
};
/* /*
* Try to redirect to initial url * Try to redirect to initial url
*/ */

@ -8,6 +8,7 @@ import * as TOKEN from 'actions/constants/token';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as reducerUtils from 'reducers/utils'; import * as reducerUtils from 'reducers/utils';
import { getVersion } from 'utils/device';
import { initialState } from 'reducers/SelectedAccountReducer'; import { initialState } from 'reducers/SelectedAccountReducer';
import type { import type {
@ -51,10 +52,11 @@ const getExceptionPage = (state: State, selectedAccount: SelectedAccountState):
shortcut: 'not-used', shortcut: 'not-used',
}; };
} }
if (discovery.fwNotSupported) { if (discovery.fwNotSupported) {
return { return {
type: 'fwNotSupported', type: 'fwNotSupported',
title: `${network.name} is not supported with Trezor ${device.features.model}`, title: `${network.name} is not supported with Trezor ${getVersion(device)}`,
message: 'Find more information on Trezor Wiki.', message: 'Find more information on Trezor Wiki.',
shortcut: network.shortcut, shortcut: network.shortcut,
}; };

@ -18,11 +18,11 @@ 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, type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR,
networkType: 'ethereum', networkType: 'ethereum',
state: EthereumState, state: EthereumState,
} | { } | {
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE, type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR,
networkType: 'ripple', networkType: 'ripple',
state: RippleState, state: RippleState,
} | { } | {

@ -3,6 +3,7 @@ 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 * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import { getDuplicateInstanceNumber } from 'reducers/utils'; import { getDuplicateInstanceNumber } from 'reducers/utils';
@ -120,7 +121,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
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 window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; // eslint-disable-line no-underscore-dangle
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/'; // eslint-disable-line no-underscore-dangle // window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://localhost:8088/'; // eslint-disable-line no-underscore-dangle
window.TrezorConnect = TrezorConnect; window.TrezorConnect = TrezorConnect;
} }
@ -131,6 +132,10 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
popup: false, popup: false,
webusb: true, webusb: true,
pendingTransportEvent: (getState().devices.length < 1), pendingTransportEvent: (getState().devices.length < 1),
manifest: {
email: 'info@trezor.io',
appUrl: urlConstants.NEXT_WALLET,
},
}); });
} catch (error) { } catch (error) {
dispatch({ dispatch({

@ -40,6 +40,8 @@ export type WalletAction = {
devices: Array<TrezorDevice> devices: Array<TrezorDevice>
} | { } | {
type: typeof WALLET.SHOW_BETA_DISCLAIMER | typeof WALLET.HIDE_BETA_DISCLAIMER | typeof WALLET.SET_FIRST_LOCATION_CHANGE, type: typeof WALLET.SHOW_BETA_DISCLAIMER | typeof WALLET.HIDE_BETA_DISCLAIMER | typeof WALLET.SET_FIRST_LOCATION_CHANGE,
} | {
type: typeof WALLET.TOGGLE_SIDEBAR,
} }
export const init = (): ThunkAction => (dispatch: Dispatch): void => { export const init = (): ThunkAction => (dispatch: Dispatch): void => {
@ -62,6 +64,10 @@ export const toggleDeviceDropdown = (opened: boolean): WalletAction => ({
opened, opened,
}); });
export const toggleSidebar = (): WalletAction => ({
type: WALLET.TOGGLE_SIDEBAR,
});
// This method will be called after each DEVICE.CONNECT action // This method will be called after each DEVICE.CONNECT action
// if connected device has different "passphrase_protection" settings than saved instances // if connected device has different "passphrase_protection" settings than saved instances
// all saved instances will be removed immediately inside DevicesReducer // all saved instances will be removed immediately inside DevicesReducer

@ -1,4 +1,5 @@
/* @flow */ /* @flow */
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';

@ -5,3 +5,6 @@ export const OPEN_EXTERNAL_WALLET: 'modal__external_wallet' = 'modal__external_w
export const CONTEXT_NONE: 'modal_ctx_none' = 'modal_ctx_none'; export const CONTEXT_NONE: 'modal_ctx_none' = 'modal_ctx_none';
export const CONTEXT_DEVICE: 'modal_ctx_device' = 'modal_ctx_device'; export const CONTEXT_DEVICE: 'modal_ctx_device' = 'modal_ctx_device';
export const CONTEXT_EXTERNAL_WALLET: 'modal_ctx_external-wallet' = 'modal_ctx_external-wallet'; export const CONTEXT_EXTERNAL_WALLET: 'modal_ctx_external-wallet' = 'modal_ctx_external-wallet';
export const OPEN_SCAN_QR: 'modal__open_scan_qr' = 'modal__open_scan_qr';
export const CONTEXT_SCAN_QR: 'modal__ctx_scan_qr' = 'modal__ctx_scan_qr';
export const CONTEXT_CONFIRMATION: 'modal__ctx_confirmation' = 'modal__ctx_confirmation';

@ -7,3 +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';

@ -10,4 +10,6 @@ export const UPDATE_SELECTED_DEVICE: 'wallet__update_selected_device' = 'wallet_
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';

@ -152,6 +152,41 @@ export const toggleAdvanced = (): Action => ({
networkType: 'ethereum', networkType: 'ethereum',
}); });
/*
* Called from UI from "clear" button
*/
export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { network } = getState().selectedAccount;
const { advanced } = getState().sendFormEthereum;
if (!network) return;
// clear transaction draft from session storage
dispatch(SessionStorageActions.clear());
const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice));
const gasLimit = network.defaultGasLimit.toString();
const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel);
dispatch({
type: SEND.CLEAR,
networkType: 'ethereum',
state: {
...initialState,
networkName: network.shortcut,
networkSymbol: network.symbol,
currency: network.symbol,
feeLevels,
selectedFeeLevel,
recommendedGasPrice: gasPrice.toString(),
gasLimit,
gasPrice: gasPrice.toString(),
advanced,
},
});
};
/* /*
* Called from UI on "address" field change * Called from UI on "address" field change
*/ */
@ -613,4 +648,5 @@ export default {
onNonceChange, onNonceChange,
onDataChange, onDataChange,
onSend, onSend,
onClear,
}; };

@ -115,7 +115,7 @@ export const recalculateTotalAmount = ($state: State): PayloadAction<State> => (
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).toString(10); state.amount = new BigNumber(token.balance).minus(pendingAmount).toFixed();
} }
} else { } else {
const b = new BigNumber(account.balance).minus(pendingAmount); const b = new BigNumber(account.balance).minus(pendingAmount);
@ -226,16 +226,16 @@ export const amountValidation = ($state: State): PayloadAction<State> => (dispat
if (!state.amount.match(decimalRegExp)) { if (!state.amount.match(decimalRegExp)) {
state.errors.amount = `Maximum ${token.decimals} decimals allowed`; state.errors.amount = `Maximum ${token.decimals} decimals allowed`;
} else if (new BigNumber(state.total).greaterThan(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).greaterThan(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).lessThanOrEqualTo('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).greaterThan(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';
} }
} }
@ -261,9 +261,9 @@ export const gasLimitValidation = ($state: State): PayloadAction<State> => (disp
state.errors.gasLimit = 'Gas limit is not a number'; state.errors.gasLimit = 'Gas limit is not a number';
} else { } else {
const gl: BigNumber = new BigNumber(gasLimit); const gl: BigNumber = new BigNumber(gasLimit);
if (gl.lessThan(1)) { if (gl.isLessThan(1)) {
state.errors.gasLimit = 'Gas limit is too low'; state.errors.gasLimit = 'Gas limit is too low';
} else if (gl.lessThan(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';
} }
} }
@ -284,9 +284,9 @@ export const gasPriceValidation = ($state: State): PayloadAction<State> => (): S
state.errors.gasPrice = 'Gas price is not a number'; state.errors.gasPrice = 'Gas price is not a number';
} else { } else {
const gp: BigNumber = new BigNumber(gasPrice); const gp: BigNumber = new BigNumber(gasPrice);
if (gp.greaterThan(1000)) { if (gp.isGreaterThan(1000)) {
state.warnings.gasPrice = 'Gas price is too high'; state.warnings.gasPrice = 'Gas price is too high';
} else if (gp.lessThanOrEqualTo('0')) { } else if (gp.isLessThanOrEqualTo('0')) {
state.errors.gasPrice = 'Gas price is too low'; state.errors.gasPrice = 'Gas price is too low';
} }
} }
@ -312,9 +312,9 @@ export const nonceValidation = ($state: State): PayloadAction<State> => (dispatc
state.errors.nonce = 'Nonce is not a valid number'; state.errors.nonce = 'Nonce is not a valid number';
} else { } else {
const n: BigNumber = new BigNumber(nonce); const n: BigNumber = new BigNumber(nonce);
if (n.lessThan(account.nonce)) { if (n.isLessThan(account.nonce)) {
state.warnings.nonce = 'Nonce is lower than recommended'; state.warnings.nonce = 'Nonce is lower than recommended';
} else if (n.greaterThan(account.nonce)) { } else if (n.isGreaterThan(account.nonce)) {
state.warnings.nonce = 'Nonce is greater than recommended'; state.warnings.nonce = 'Nonce is greater than recommended';
} }
} }
@ -339,7 +339,7 @@ export const dataValidation = ($state: State): PayloadAction<State> => (): State
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), 'gwei', 'ether'); return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit).toFixed(), 'gwei', 'ether');
} catch (error) { } catch (error) {
return '0'; return '0';
} }
@ -347,7 +347,12 @@ export const calculateFee = (gasPrice: string, gasLimit: string): string => {
export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => { export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => {
try { try {
return new BigNumber(amount).plus(calculateFee(gasPrice, gasLimit)).toString(10); const bAmount = new BigNumber(amount);
// BigNumber() returns NaN on non-numeric string
if (bAmount.isNaN()) {
throw new Error('Amount is not a number');
}
return bAmount.plus(calculateFee(gasPrice, gasLimit)).toFixed();
} catch (error) { } catch (error) {
return '0'; return '0';
} }
@ -358,8 +363,8 @@ export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimi
// TODO - minus pendings // TODO - minus pendings
const fee = calculateFee(gasPrice, gasLimit); const fee = calculateFee(gasPrice, gasLimit);
const max = balance.minus(fee); const max = balance.minus(fee);
if (max.lessThan(0)) return '0'; if (max.isLessThan(0)) return '0';
return max.toString(10); return max.toFixed();
} catch (error) { } catch (error) {
return '0'; return '0';
} }
@ -368,8 +373,8 @@ 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)).toString(10); const high: string = price.plus(quarter.times(2)).toFixed();
const low: string = price.minus(quarter.times(2)).toString(10); const low: string = price.minus(quarter.times(2)).toFixed();
const customLevel: FeeLevel = selected && selected.value === 'Custom' ? { const customLevel: FeeLevel = selected && selected.value === 'Custom' ? {
value: 'Custom', value: 'Custom',
@ -391,7 +396,7 @@ export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLi
{ {
value: 'Normal', value: 'Normal',
gasPrice: gasPrice.toString(), gasPrice: gasPrice.toString(),
label: `${calculateFee(price.toString(10), gasLimit)} ${symbol}`, label: `${calculateFee(price.toFixed(), gasLimit)} ${symbol}`,
}, },
{ {
value: 'Low', value: 'Low',

@ -118,7 +118,7 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
const updatedAccount = await TrezorConnect.rippleGetAccountInfo({ const updatedAccount = await TrezorConnect.rippleGetAccountInfo({
account: { account: {
address: account.descriptor, descriptor: account.descriptor,
from: account.block, from: account.block,
history: false, history: false,
}, },

@ -76,6 +76,6 @@ export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discover
networkType: 'ripple', networkType: 'ripple',
sequence: account.sequence, sequence: account.sequence,
reserve: '0', reserve: toDecimalAmount(account.reserve, network.decimals),
}; };
}; };

@ -60,7 +60,7 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
} }
if (shouldUpdate) { if (shouldUpdate) {
const validated = dispatch(ValidationActions.validation()); const validated = dispatch(ValidationActions.validation(prevState.sendFormRipple));
dispatch({ dispatch({
type: SEND.VALIDATION, type: SEND.VALIDATION,
networkType: 'ripple', networkType: 'ripple',
@ -119,6 +119,38 @@ export const toggleAdvanced = (): Action => ({
networkType: 'ripple', networkType: 'ripple',
}); });
/*
* Called from UI from "clear" button
*/
export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { network } = getState().selectedAccount;
const { advanced } = getState().sendFormRipple;
if (!network) return;
// clear transaction draft from session storage
dispatch(SessionStorageActions.clear());
const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network));
const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels));
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel);
dispatch({
type: SEND.CLEAR,
networkType: 'ripple',
state: {
...initialState,
networkName: network.shortcut,
networkSymbol: network.symbol,
feeLevels,
selectedFeeLevel,
fee: network.fee.defaultFee,
sequence: '1',
advanced,
},
});
};
/* /*
* Called from UI on "address" field change * Called from UI on "address" field change
*/ */
@ -244,6 +276,23 @@ export const onFeeChange = (fee: string): ThunkAction => (dispatch: Dispatch, ge
}); });
}; };
/*
* Called from UI on "advanced / destination tag" field change
*/
export const onDestinationTagChange = (destinationTag: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const state: State = getState().sendFormRipple;
dispatch({
type: SEND.CHANGE,
networkType: 'ripple',
state: {
...state,
untouched: false,
touched: { ...state.touched, destinationTag: true },
destinationTag,
},
});
};
/* /*
* Called from UI from "send" button * Called from UI from "send" button
*/ */
@ -262,7 +311,13 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
if (!blockchain) return; if (!blockchain) return;
const currentState: State = getState().sendFormRipple; const currentState: State = getState().sendFormRipple;
const amount = fromDecimalAmount(currentState.amount, 6); const payment: { amount: string, destination: string, destinationTag?: number } = {
amount: fromDecimalAmount(currentState.amount, network.decimals),
destination: currentState.address,
};
if (currentState.destinationTag.length > 0) {
payment.destinationTag = parseInt(currentState.destinationTag, 10);
}
const signedTransaction = await TrezorConnect.rippleSignTransaction({ const signedTransaction = await TrezorConnect.rippleSignTransaction({
device: { device: {
@ -276,10 +331,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
fee: currentState.selectedFeeLevel.fee, // Fee must be in the range of 10 to 10,000 drops fee: currentState.selectedFeeLevel.fee, // Fee must be in the range of 10 to 10,000 drops
flags: 0x80000000, flags: 0x80000000,
sequence: account.sequence, sequence: account.sequence,
payment: { payment,
amount,
destination: currentState.address,
},
}, },
}); });
@ -346,5 +398,7 @@ export default {
onFeeLevelChange, onFeeLevelChange,
updateFeeLevels, updateFeeLevels,
onFeeChange, onFeeChange,
onDestinationTagChange,
onSend, onSend,
onClear,
}; };

@ -1,5 +1,5 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as SEND from 'actions/constants/send'; import * as SEND from 'actions/constants/send';
import { findDevice, getPendingAmount } from 'reducers/utils'; import { findDevice, getPendingAmount } from 'reducers/utils';
@ -9,6 +9,7 @@ import type {
Dispatch, Dispatch,
GetState, GetState,
PayloadAction, PayloadAction,
PromiseAction,
BlockchainFeeLevel, BlockchainFeeLevel,
} from 'flowtype'; } from 'flowtype';
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer'; import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
@ -59,7 +60,7 @@ export const onFeeUpdated = (network: string, feeLevels: Array<BlockchainFeeLeve
/* /*
* Recalculate amount, total and fees * Recalculate amount, total and fees
*/ */
export const validation = (): 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));
@ -73,6 +74,10 @@ export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getSt
state = dispatch(addressLabel(state)); state = dispatch(addressLabel(state));
state = dispatch(amountValidation(state)); state = dispatch(amountValidation(state));
state = dispatch(feeValidation(state)); state = dispatch(feeValidation(state));
state = dispatch(destinationTagValidation(state));
if (state.touched.address && prevState.address !== state.address) {
dispatch(addressBalanceValidation(state));
}
return state; return state;
}; };
@ -82,14 +87,14 @@ const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatc
network, network,
pending, pending,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account || !network) return $state; if (!account || account.networkType !== 'ripple' || !network) return $state;
const state = { ...$state }; const state = { ...$state };
const fee = toDecimalAmount(state.selectedFeeLevel.fee, network.decimals); const fee = toDecimalAmount(state.selectedFeeLevel.fee, network.decimals);
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(pendingAmount); const availableBalance = new BigNumber(account.balance).minus(account.reserve).minus(pendingAmount);
state.amount = calculateMaxAmount(availableBalance, fee); state.amount = calculateMaxAmount(availableBalance, fee);
} }
@ -134,6 +139,43 @@ const addressValidation = ($state: State): PayloadAction<State> => (dispatch: Di
return state; return state;
}; };
/*
* Address balance validation
* 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 { network } = getState().selectedAccount;
if (!network) return;
let minAmount: string = '0';
const response = await TrezorConnect.rippleGetAccountInfo({
account: {
descriptor: $state.address,
},
coin: network.shortcut,
});
if (response.success) {
const empty = response.payload.sequence <= 0 && response.payload.balance === '0';
if (empty) {
minAmount = toDecimalAmount(response.payload.reserve, network.decimals);
}
}
// TODO: consider checking local (known) accounts reserve instead of async fetching
// a2 (not empty): rJX2KwzaLJDyFhhtXKi3htaLfaUH2tptEX
// a4 (empty): r9skfe7kZkvqss7oMB3tuj4a59PXD5wRa2
dispatch({
type: SEND.CHANGE,
networkType: 'ripple',
state: {
...getState().sendFormRipple,
minAmount,
},
});
};
/* /*
* Address label assignation * Address label assignation
*/ */
@ -183,7 +225,7 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
account, account,
pending, pending,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account) return state; if (!account || account.networkType !== 'ripple') return state;
const { amount } = state; const { amount } = state;
if (amount.length < 1) { if (amount.length < 1) {
@ -192,13 +234,21 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
state.errors.amount = 'Amount is not a number'; state.errors.amount = 'Amount is not a number';
} else { } else {
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).greaterThan(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)) {
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)) {
state.errors.amount = `Amount is too low. Minimum amount for creating a new account is ${state.minAmount} ${state.networkSymbol}`;
}
return state; return state;
}; };
@ -221,14 +271,27 @@ export const feeValidation = ($state: State): PayloadAction<State> => (dispatch:
state.errors.fee = 'Fee must be an absolute number'; state.errors.fee = 'Fee must be an absolute number';
} else { } else {
const gl: BigNumber = new BigNumber(fee); const gl: BigNumber = new BigNumber(fee);
if (gl.lessThan(network.fee.minFee)) { if (gl.isLessThan(network.fee.minFee)) {
state.errors.fee = 'Fee is below recommended'; state.errors.fee = 'Fee is below recommended';
} else if (gl.greaterThan(network.fee.maxFee)) { } else if (gl.isGreaterThan(network.fee.maxFee)) {
state.errors.fee = 'Fee is above recommended'; state.errors.fee = 'Fee is above recommended';
} }
} }
return state; return state;
}; };
/*
* Destination Tag value validation
*/
export const destinationTagValidation = ($state: State): PayloadAction<State> => (): State => {
const state = { ...$state };
if (!state.touched.destinationTag) return state;
const { destinationTag } = state;
if (destinationTag.length > 0 && !destinationTag.match(ABS_RE)) {
state.errors.destinationTag = 'Destination tag must be an absolute number';
}
return state;
};
/* /*
@ -237,7 +300,12 @@ export const feeValidation = ($state: State): PayloadAction<State> => (dispatch:
const calculateTotal = (amount: string, fee: string): string => { const calculateTotal = (amount: string, fee: string): string => {
try { try {
return new BigNumber(amount).plus(fee).toString(10); const bAmount = new BigNumber(amount);
// BigNumber() returns NaN on non-numeric string
if (bAmount.isNaN()) {
throw new Error('Amount is not a number');
}
return bAmount.plus(fee).toFixed();
} catch (error) { } catch (error) {
return '0'; return '0';
} }
@ -247,8 +315,8 @@ const calculateMaxAmount = (balance: BigNumber, fee: string): string => {
try { try {
// TODO - minus pendings // TODO - minus pendings
const max = balance.minus(fee); const max = balance.minus(fee);
if (max.lessThan(0)) return '0'; if (max.isLessThan(0)) return '0';
return max.toString(10); return max.toFixed();
} catch (error) { } catch (error) {
return '0'; return '0';
} }

@ -0,0 +1,37 @@
import React from 'react';
import styled, { css } from 'styled-components';
import PropTypes from 'prop-types';
import { FADE_IN } from 'config/animations';
const StyledBackdrop = styled.div`
width: 100%;
height: 100%;
position: fixed;
z-index: 100;
left: 0;
top: 0;
background-color: rgba(0,0,0,0.5);
${props => props.animated && css`
animation: ${FADE_IN} 0.3s;
`};
`;
const Backdrop = ({
className,
show,
animated,
onClick,
}) => (
show ? <StyledBackdrop className={className} animated={animated} onClick={onClick} /> : null
);
Backdrop.propTypes = {
show: PropTypes.bool,
className: PropTypes.string,
animated: PropTypes.bool,
onClick: PropTypes.func,
};
export default Backdrop;

@ -17,6 +17,7 @@ type Props = {
isWhite?: boolean, isWhite?: boolean,
isWebUsb?: boolean, isWebUsb?: boolean,
isTransparent?: boolean, isTransparent?: boolean,
dataTest?: string
} }
const Wrapper = styled.button` const Wrapper = styled.button`
@ -37,6 +38,11 @@ const Wrapper = styled.button`
background: ${colors.GREEN_TERTIARY}; background: ${colors.GREEN_TERTIARY};
} }
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
}
${props => props.isDisabled && css` ${props => props.isDisabled && css`
pointer-events: none; pointer-events: none;
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
@ -48,6 +54,10 @@ const Wrapper = styled.button`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
border: 1px solid ${colors.DIVIDER}; border: 1px solid ${colors.DIVIDER};
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
}
&:hover { &:hover {
color: ${colors.TEXT_PRIMARY}; color: ${colors.TEXT_PRIMARY};
background: ${colors.DIVIDER}; background: ${colors.DIVIDER};
@ -64,6 +74,11 @@ const Wrapper = styled.button`
border: 0px; border: 0px;
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
&:focus {
color: ${colors.TEXT_PRIMARY};
box-shadow: none;
}
&:hover, &:hover,
&:active { &:active {
color: ${colors.TEXT_PRIMARY}; color: ${colors.TEXT_PRIMARY};
@ -132,10 +147,12 @@ const Button = ({
isWhite = false, isWhite = false,
isWebUsb = false, isWebUsb = false,
isTransparent = false, isTransparent = false,
dataTest,
}: Props) => { }: Props) => {
const newClassName = isWebUsb ? `${className} trezor-webusb-button` : className; const newClassName = isWebUsb ? `${className} trezor-webusb-button` : className;
return ( return (
<Wrapper <Wrapper
data-test={dataTest}
className={newClassName} className={newClassName}
onClick={onClick} onClick={onClick}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
@ -162,6 +179,7 @@ Button.propTypes = {
isWhite: PropTypes.bool, isWhite: PropTypes.bool,
isWebUsb: PropTypes.bool, isWebUsb: PropTypes.bool,
isTransparent: PropTypes.bool, isTransparent: PropTypes.bool,
dataTest: PropTypes.string,
}; };
export default Button; export default Button;

@ -15,10 +15,13 @@ const Wrapper = styled.div`
position: relative; position: relative;
height: 70px; height: 70px;
width: 320px; width: 320px;
z-index: 10;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0px 25px;
background: ${props => (props.disabled ? colors.GRAY_LIGHT : 'transparent')}; background: ${props => (props.disabled ? colors.GRAY_LIGHT : 'transparent')};
background: ${props => (props.isSelected ? colors.WHITE : 'transparent')}; background: ${props => (props.isSelected ? colors.WHITE : 'transparent')};
cursor: pointer;
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)')};
@ -27,6 +30,10 @@ const Wrapper = styled.div`
box-shadow: none; box-shadow: none;
`} `}
${props => props.disabled && css`
cursor: default;
`}
${props => props.isHoverable && !props.disabled && css` ${props => props.isHoverable && !props.disabled && css`
&:hover { &:hover {
background: ${colors.GRAY_LIGHT}; background: ${colors.GRAY_LIGHT};
@ -34,22 +41,10 @@ const Wrapper = styled.div`
`} `}
`; `;
const ClickWrapper = styled.div`
width: 100%;
display: flex;
padding-left: 25px;
height: 100%;
align-items: center;
cursor: pointer;
${props => props.disabled && css`
cursor: default;
`}
`;
const LabelWrapper = styled.div` const LabelWrapper = styled.div`
flex: 1; flex: 1 1 auto;
padding-left: 18px; padding-left: 18px;
overflow: hidden;
`; `;
const Name = styled.div` const Name = styled.div`
@ -71,8 +66,9 @@ const Status = styled.div`
`; `;
const IconWrapper = styled.div` const IconWrapper = styled.div`
padding-right: 25px;
display: flex; display: flex;
flex: 1 0 0;
justify-content: flex-end;
`; `;
const ImageWrapper = styled.div` const ImageWrapper = styled.div`
@ -102,32 +98,30 @@ const DeviceHeader = ({
disabled = false, disabled = false,
isSelected = false, isSelected = false,
className, className,
testId,
}) => { }) => {
const status = getStatus(device); const status = getStatus(device);
return ( return (
<Wrapper <Wrapper
isSelected={isSelected} isSelected={isSelected}
data-test={testId}
isOpen={isOpen} isOpen={isOpen}
isHoverable={isHoverable} isHoverable={isHoverable}
disabled={disabled} disabled={disabled}
className={className} className={className}
onClick={onClickWrapper}
> >
<ClickWrapper <ImageWrapper>
disabled={disabled} <Dot color={getStatusColor(status)} />
onClick={onClickWrapper} <TrezorImage model={getVersion(device)} />
> </ImageWrapper>
<ImageWrapper> <LabelWrapper>
<Dot color={getStatusColor(status)} /> <Name>{device.instanceLabel}</Name>
<TrezorImage model={getVersion(device)} /> <Status title={getStatusName(status)}>{getStatusName(status)}</Status>
</ImageWrapper> </LabelWrapper>
<LabelWrapper> <IconWrapper>
<Name>{device.instanceLabel}</Name> {icon && !disabled && isAccessible && icon}
<Status>{getStatusName(status)}</Status> </IconWrapper>
</LabelWrapper>
<IconWrapper>
{icon && !disabled && isAccessible && icon}
</IconWrapper>
</ClickWrapper>
</Wrapper> </Wrapper>
); );
}; };
@ -142,6 +136,7 @@ DeviceHeader.propTypes = {
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
onClickWrapper: PropTypes.func.isRequired, onClickWrapper: PropTypes.func.isRequired,
className: PropTypes.string, className: PropTypes.string,
testId: PropTypes.string,
}; };
export default DeviceHeader; export default DeviceHeader;

@ -12,8 +12,6 @@ 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';
declare var COMMITHASH: string;
type Props = { type Props = {
opened: boolean, opened: boolean,
isLanding: boolean, isLanding: boolean,
@ -60,7 +58,7 @@ const Right = styled.div`
const Footer = ({ opened, toggle, isLanding }: Props) => ( const Footer = ({ opened, toggle, isLanding }: Props) => (
<Wrapper> <Wrapper>
<Left> <Left>
<Copy title={COMMITHASH}>&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="./assets/tos.pdf" isGreen>Terms</StyledLink> <StyledLink href="./assets/tos.pdf" isGreen>Terms</StyledLink>
<StyledLink onClick={toggle} isGreen>{ opened ? 'Hide Log' : 'Show Log' }</StyledLink> <StyledLink onClick={toggle} isGreen>{ opened ? 'Hide Log' : 'Show Log' }</StyledLink>

@ -3,17 +3,18 @@ import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import colors from 'config/colors'; import colors from 'config/colors';
import { SCREEN_SIZE } from 'config/variables';
import Icon from 'components/Icon';
import icons from 'config/icons';
import type { toggleSidebar as toggleSidebarType } from 'actions/WalletActions';
const Wrapper = styled.header` const Wrapper = styled.header`
width: 100%; width: 100%;
height: 52px; height: 52px;
background: ${colors.HEADER}; background: ${colors.HEADER};
overflow: hidden;
svg { z-index: 200;
fill: ${colors.WHITE};
height: 28px;
width: 100px;
}
`; `;
const LayoutWrapper = styled.div` const LayoutWrapper = styled.div`
@ -31,12 +32,65 @@ const LayoutWrapper = styled.div`
`; `;
const Left = styled.div` const Left = styled.div`
display: none;
flex: 0 0 33%;
@media screen and (max-width: ${SCREEN_SIZE.SM}) {
display: initial;
}
`;
const MenuToggler = styled.div`
display: none;
white-space: nowrap;
color: ${colors.WHITE};
align-self: center;
align-items: center;
cursor: pointer;
user-select: none;
padding: 10px 0px;
transition: all .1s ease-in;
@media screen and (max-width: ${SCREEN_SIZE.SM}) {
display: flex;
}
`;
const TogglerText = styled.div`
`;
const Logo = styled.div`
flex: 1; flex: 1;
display: flex;
justify-content: flex-start; justify-content: flex-start;
display: flex;
svg {
fill: ${colors.WHITE};
height: 28px;
width: 100px;
}
@media screen and (max-width: ${SCREEN_SIZE.SM}) {
flex: 1 0 33%;
justify-content: center;
}
`; `;
const Right = styled.div``; const MenuLinks = styled.div`
flex: 0;
@media screen and (max-width: ${SCREEN_SIZE.SM}) {
flex: 0 1 33%;
}
`;
const Projects = styled.div`
@media screen and (max-width: ${SCREEN_SIZE.SM}) {
display: none;
}
`;
const A = styled.a` const A = styled.a`
color: ${colors.WHITE}; color: ${colors.WHITE};
@ -58,10 +112,42 @@ const A = styled.a`
} }
`; `;
const Header = (): React$Element<string> => ( type Props = {
<Wrapper> sidebarEnabled?: boolean,
sidebarOpened?: ?boolean,
toggleSidebar?: toggleSidebarType,
};
const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => (
<Wrapper data-test="Main__page__navigation">
<LayoutWrapper> <LayoutWrapper>
<Left> <Left>
{ sidebarEnabled && (
<MenuToggler onClick={toggleSidebar}>
{sidebarOpened ? (
<>
<Icon
size={24}
color={colors.WHITE}
icon={icons.CLOSE}
/>
<TogglerText>Close</TogglerText>
</>
) : (
<>
<Icon
color={colors.WHITE}
size={24}
icon={icons.MENU}
/>
<TogglerText>Menu</TogglerText>
</>
)}
</MenuToggler>
)}
</Left>
<Logo>
<NavLink to="/"> <NavLink to="/">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 163.7 41.9" width="100%" height="100%" preserveAspectRatio="xMinYMin meet"> <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 163.7 41.9" width="100%" height="100%" preserveAspectRatio="xMinYMin meet">
<polygon points="101.1,12.8 118.2,12.8 118.2,17.3 108.9,29.9 118.2,29.9 118.2,35.2 101.1,35.2 101.1,30.7 110.4,18.1 101.1,18.1" /> <polygon points="101.1,12.8 118.2,12.8 118.2,17.3 108.9,29.9 118.2,29.9 118.2,35.2 101.1,35.2 101.1,30.7 110.4,18.1 101.1,18.1" />
@ -73,13 +159,15 @@ const Header = (): React$Element<string> => (
<polygon points="40.5,12.8 58.6,12.8 58.6,18.1 52.4,18.1 52.4,35.2 46.6,35.2 46.6,18.1 40.5,18.1 " /> <polygon points="40.5,12.8 58.6,12.8 58.6,18.1 52.4,18.1 52.4,35.2 46.6,35.2 46.6,18.1 40.5,18.1 " />
</svg> </svg>
</NavLink> </NavLink>
</Left> </Logo>
<Right> <MenuLinks>
<A href="https://trezor.io/" target="_blank" rel="noreferrer noopener">Trezor</A> <Projects>
<A href="https://wiki.trezor.io/" target="_blank" rel="noreferrer noopener">Wiki</A> <A href="https://trezor.io/" target="_blank" rel="noreferrer noopener">Trezor</A>
<A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener">Blog</A> <A href="https://wiki.trezor.io/" target="_blank" rel="noreferrer noopener">Wiki</A>
<A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener">Support</A> <A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener">Blog</A>
</Right> <A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener">Support</A>
</Projects>
</MenuLinks>
</LayoutWrapper> </LayoutWrapper>
</Wrapper> </Wrapper>
); );

@ -12,10 +12,12 @@ const A = styled.a`
font-size: ${FONT_SIZE.SMALL}; font-size: ${FONT_SIZE.SMALL};
${props => props.isGreen && css` ${props => props.isGreen && css`
border-bottom: 1px solid ${colors.GREEN_PRIMARY}; text-decoration: underline;
text-decoration-color: ${colors.GREEN_PRIMARY};
`} `}
${props => props.isGray && css` ${props => props.isGray && css`
border-bottom: 1px solid ${colors.TEXT_SECONDARY}; text-decoration: underline;
text-decoration-color: ${colors.TEXT_SECONDARY};
`} `}
&, &,

@ -25,13 +25,14 @@ const SvgWrapper = styled.svg`
const CircleWrapper = styled.circle` const CircleWrapper = styled.circle`
${props => props.isRoute && css` ${props => props.isRoute && css`
stroke: ${colors.GRAY_LIGHT}; stroke: ${props.transparentRoute ? 'transparent' : colors.GRAY_LIGHT};
`} `}
${props => props.isPath && css` ${props => props.isPath && css`
stroke-width: ${props.transparentRoute ? '2px' : '1px'};
stroke-dasharray: 1, 200; stroke-dasharray: 1, 200;
stroke-dashoffset: 0; stroke-dashoffset: 0;
animation: ${DASH} 1.5s ease-in-out infinite, ${GREEN_COLOR} 6s ease-in-out infinite; animation: ${DASH} 1.5s ease-in-out infinite, ${props.animationColor || GREEN_COLOR} 6s ease-in-out infinite;
stroke-linecap: round; stroke-linecap: round;
`}; `};
`; `;
@ -42,29 +43,31 @@ const StyledParagraph = styled(Paragraph)`
`; `;
const Loader = ({ const Loader = ({
className, text, isWhiteText = false, isSmallText, size = 100, 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}
cx="50" cx="50"
cy="50" cy="50"
r="20" r="20"
fill="none" fill="none"
stroke="" stroke=""
strokeWidth="1"
strokeMiterlimit="10" strokeMiterlimit="10"
isRoute isRoute
transparentRoute={transparentRoute}
/> />
<CircleWrapper <CircleWrapper
animationColor={animationColor}
cx="50" cx="50"
cy="50" cy="50"
r="20" r="20"
fill="none" fill="none"
strokeWidth="1"
strokeMiterlimit="10" strokeMiterlimit="10"
isPath isPath
transparentRoute={transparentRoute}
/> />
</SvgWrapper> </SvgWrapper>
</Wrapper> </Wrapper>
@ -75,6 +78,8 @@ Loader.propTypes = {
isSmallText: PropTypes.bool, isSmallText: PropTypes.bool,
className: PropTypes.string, className: PropTypes.string,
text: PropTypes.string, text: PropTypes.string,
animationColor: PropTypes.object,
transparentRoute: PropTypes.bool,
size: PropTypes.number, size: PropTypes.number,
}; };

@ -22,7 +22,7 @@ const Wrapper = styled.div`
position: relative; position: relative;
color: ${colors.INFO_PRIMARY}; color: ${colors.INFO_PRIMARY};
background: ${colors.INFO_SECONDARY}; background: ${colors.INFO_SECONDARY};
padding: 24px 48px; padding: 24px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: left; text-align: left;
@ -32,9 +32,10 @@ const Wrapper = styled.div`
const Click = styled.div` const Click = styled.div`
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
top: 8px; top: 0;
right: 0; right: 0;
padding: 12px; padding-right: inherit;
padding-top: inherit;
color: inherit; color: inherit;
transition: opacity 0.3s; transition: opacity 0.3s;
@ -61,7 +62,7 @@ const Log = (props: Props): ?React$Element<string> => {
return ( return (
<Wrapper> <Wrapper>
<Click onClick={props.toggle}> <Click onClick={props.toggle}>
<Icon size={25} color={colors.INFO_PRIMARY} icon={icons.CLOSE} /> <Icon size={24} color={colors.INFO_PRIMARY} icon={icons.CLOSE} />
</Click> </Click>
<H2>Log</H2> <H2>Log</H2>
<StyledParagraph isSmaller>Attention: The log contains your XPUBs. Anyone with your XPUBs can see your account history.</StyledParagraph> <StyledParagraph isSmaller>Attention: The log contains your XPUBs. Anyone with your XPUBs can see your account history.</StyledParagraph>

@ -5,9 +5,12 @@ import styled from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import colors from 'config/colors'; import colors from 'config/colors';
import { getPrimaryColor, getSecondaryColor } from 'utils/notification'; import { WHITE_COLOR } from 'config/animations';
import { getPrimaryColor } from 'utils/notification';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import { TRANSITION, FONT_SIZE, FONT_WEIGHT } from 'config/variables'; import {
TRANSITION, FONT_SIZE, FONT_WEIGHT, SCREEN_SIZE,
} from 'config/variables';
type Props = { type Props = {
type: string; type: string;
@ -31,7 +34,8 @@ const LoaderContent = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: default; cursor: default;
background: ${props => getSecondaryColor(props.type)}; color: ${colors.WHITE};
background: ${props => getPrimaryColor(props.type)};
`; `;
const Wrapper = styled.button` const Wrapper = styled.button`
@ -46,6 +50,10 @@ 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}){
padding: 12px 24px;
}
&:hover { &:hover {
color: ${colors.WHITE}; color: ${colors.WHITE};
background: ${props => getPrimaryColor(props.type)}; background: ${props => getPrimaryColor(props.type)};
@ -66,7 +74,7 @@ const NotificationButton = ({
> >
{isLoading && ( {isLoading && (
<LoaderContent type={type}> <LoaderContent type={type}>
<Loader size={30} /> <Loader transparentRoute animationColor={WHITE_COLOR} size={30} />
</LoaderContent> </LoaderContent>
)} )}
{icon && ( {icon && (

@ -17,8 +17,9 @@ type Props = {
cancelable?: boolean; cancelable?: boolean;
title: string; title: string;
className?: string; className?: string;
message?: ?string; message?: ?React.Node;
actions?: Array<CallbackAction>; actions?: Array<CallbackAction>;
isActionInProgress?: boolean;
close?: typeof NotificationActions.close, close?: typeof NotificationActions.close,
loading?: boolean loading?: boolean
}; };
@ -26,22 +27,27 @@ type Props = {
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
position: relative; position: relative;
padding: 24px 48px 9px 24px;
display: flex; display: flex;
flex-direction: row;
text-align: left;
justify-content: center; justify-content: center;
align-items: center;
color: ${props => getPrimaryColor(props.type)}; color: ${props => getPrimaryColor(props.type)};
background: ${props => getSecondaryColor(props.type)}; background: ${props => getSecondaryColor(props.type)};
`; `;
const Content = styled.div`
width: 100%;
max-width: 1170px;
padding: 24px;
display: flex;
flex-direction: row;
text-align: left;
align-items: center;
`;
const Body = styled.div` const Body = styled.div`
display: flex; display: flex;
`; `;
const Message = styled.div` const Message = styled.div`
padding-bottom: 13px;
font-size: ${FONT_SIZE.SMALL}; font-size: ${FONT_SIZE.SMALL};
`; `;
@ -52,10 +58,9 @@ const Title = styled.div`
`; `;
const CloseClick = styled.div` const CloseClick = styled.div`
position: absolute; margin-left: 24px;
right: 0; align-self: flex-start;
top: 0; cursor: pointer;
padding: 20px 10px 0 0;
`; `;
const StyledIcon = styled(Icon)` const StyledIcon = styled(Icon)`
@ -85,7 +90,6 @@ const ActionContent = styled.div`
display: flex; display: flex;
justify-content: right; justify-content: right;
align-items: flex-end; align-items: flex-end;
padding-bottom: 14px;
`; `;
const Notification = (props: Props): React$Element<string> => { const Notification = (props: Props): React$Element<string> => {
@ -93,42 +97,45 @@ const Notification = (props: Props): React$Element<string> => {
return ( return (
<Wrapper className={props.className} type={props.type}> <Wrapper className={props.className} type={props.type}>
{props.loading && <Loader size={50} /> } <Content>
{props.cancelable && ( {props.loading && <Loader size={50} /> }
<CloseClick onClick={() => close()}> <Body>
<Icon <IconWrapper>
color={getPrimaryColor(props.type)} <StyledIcon
icon={icons.CLOSE} color={getPrimaryColor(props.type)}
size={20} icon={getIcon(props.type)}
/> />
</CloseClick> </IconWrapper>
)} <Texts>
<Body> <Title>{ props.title }</Title>
<IconWrapper> { props.message ? <Message>{props.message}</Message> : '' }
<StyledIcon </Texts>
color={getPrimaryColor(props.type)} </Body>
icon={getIcon(props.type)} <AdditionalContent>
/> {props.actions && props.actions.length > 0 && (
</IconWrapper> <ActionContent>
<Texts> {props.actions.map(action => (
<Title>{ props.title }</Title> <NotificationButton
{ props.message ? <Message>{props.message}</Message> : '' } key={action.label}
</Texts> type={props.type}
</Body> isLoading={props.isActionInProgress}
<AdditionalContent> onClick={() => { close(); action.callback(); }}
{props.actions && props.actions.length > 0 && ( >{action.label}
<ActionContent> </NotificationButton>
{props.actions.map(action => ( ))}
<NotificationButton </ActionContent>
key={action.label} )}
type={props.type} </AdditionalContent>
onClick={() => { close(); action.callback(); }} {props.cancelable && (
>{action.label} <CloseClick onClick={() => close()}>
</NotificationButton> <Icon
))} color={getPrimaryColor(props.type)}
</ActionContent> icon={icons.CLOSE}
size={20}
/>
</CloseClick>
)} )}
</AdditionalContent> </Content>
</Wrapper> </Wrapper>
); );
}; };

@ -11,17 +11,16 @@ const styles = isSearchable => ({
width: '100%', width: '100%',
color: colors.TEXT_SECONDARY, color: colors.TEXT_SECONDARY,
}), }),
control: (base, { isDisabled }) => ({ control: (base, { isDisabled, isFocused }) => ({
...base, ...base,
minHeight: 'initial', minHeight: 'initial',
height: '40px', height: '40px',
borderRadius: '2px', borderRadius: '2px',
borderColor: colors.DIVIDER, borderColor: isFocused ? colors.INPUT_FOCUSED_BORDER : colors.DIVIDER,
boxShadow: 'none', boxShadow: isFocused ? `0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW}` : 'none',
background: isDisabled ? colors.LANDING : colors.WHITE, background: isDisabled ? colors.LANDING : colors.WHITE,
'&:hover': { '&:hover': {
cursor: isSearchable ? 'text' : 'pointer', cursor: isSearchable ? 'text' : 'pointer',
borderColor: colors.DIVIDER,
}, },
}), }),
indicatorSeparator: () => ({ indicatorSeparator: () => ({

@ -67,6 +67,11 @@ const StyledTextarea = styled(Textarea)`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
} }
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
}
&:disabled { &:disabled {
pointer-events: none; pointer-events: none;
background: ${colors.GRAY_LIGHT}; background: ${colors.GRAY_LIGHT};
@ -109,6 +114,7 @@ const TopLabel = styled.span`
`; `;
const BottomText = styled.span` const BottomText = styled.span`
margin-top: 10px;
font-size: ${FONT_SIZE.SMALL}; font-size: ${FONT_SIZE.SMALL};
color: ${props => (props.color ? props.color : colors.TEXT_SECONDARY)}; color: ${props => (props.color ? props.color : colors.TEXT_SECONDARY)};
`; `;

@ -31,21 +31,24 @@ const Tooltip = ({
content, content,
readMoreLink, readMoreLink,
children, children,
enterDelayMs,
}) => ( }) => (
<Wrapper className={className}> <Wrapper className={className}>
<RcTooltip <RcTooltip
arrowContent={<div className="rc-tooltip-arrow-inner" />} arrowContent={<div className="rc-tooltip-arrow-inner" />}
placement={placement} placement={placement}
mouseEnterDelay={enterDelayMs || 0}
overlay={() => ( overlay={() => (
<ContentWrapper> <ContentWrapper>
<Content maxWidth={maxWidth}>{content}</Content> <Content maxWidth={maxWidth}>{content}</Content>
{readMoreLink && ( {readMoreLink && (
<Link href={readMoreLink}> <Link href={readMoreLink}>
<ReadMore>Read more</ReadMore> <ReadMore>Learn more</ReadMore>
</Link> </Link>
) )
} }
</ContentWrapper>)} </ContentWrapper>
)}
> >
{children} {children}
</RcTooltip> </RcTooltip>
@ -65,6 +68,7 @@ Tooltip.propTypes = {
PropTypes.string, PropTypes.string,
]), ]),
readMoreLink: PropTypes.string, readMoreLink: PropTypes.string,
enterDelayMs: PropTypes.number,
}; };
export default Tooltip; export default Tooltip;

@ -0,0 +1,74 @@
/* @flow */
import React from 'react';
import PropTypes from 'prop-types';
import COLORS from 'config/colors';
import ICONS from 'config/icons';
import styled from 'styled-components';
import type { TrezorDevice } from 'flowtype';
type Props = {
device: TrezorDevice,
size?: number,
color?: string,
hoverColor?: string,
onClick?: any,
}
const SvgWrapper = styled.svg`
:hover {
path {
fill: ${props => props.hoverColor}
}
}
`;
const Path = styled.path`
fill: ${props => props.color};
`;
const getDeviceIcon = (majorVersion: number) => {
switch (majorVersion) {
case 1:
return ICONS.T1;
case 2:
return ICONS.T2;
default:
return ICONS.T2;
}
};
const DeviceIcon = ({
device,
size = 32,
color = COLORS.TEXT_SECONDARY,
hoverColor,
onClick,
}: Props) => {
const majorVersion = device.features ? device.features.major_version : 2;
return (
<SvgWrapper
hoverColor={hoverColor}
width={`${size}`}
height={`${size}`}
viewBox="0 0 1024 1024"
onClick={onClick}
>
<Path
key={majorVersion}
color={color}
d={getDeviceIcon(majorVersion)}
/>
</SvgWrapper>
);
};
DeviceIcon.propTypes = {
device: PropTypes.object,
size: PropTypes.number,
color: PropTypes.string,
hoverColor: PropTypes.string,
onClick: PropTypes.func,
};
export default DeviceIcon;

@ -15,8 +15,9 @@ const Img = styled.img`
`; `;
const TrezorImage = ({ model }: Props) => { const TrezorImage = ({ model }: Props) => {
const imageName = model === 'One' ? 1 : model;
// $FlowIssue: `require` must be a string literal. // $FlowIssue: `require` must be a string literal.
const src = require(`./images/trezor-${model}.png`); // eslint-disable-line const src = require(`./images/trezor-${imageName}.png`); // eslint-disable-line
return ( return (
<Wrapper> <Wrapper>
<Img model={model} src={src} /> <Img model={model} src={src} />

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
import styled from 'styled-components'; import styled from 'styled-components';
import ICONS from 'config/icons';
const SvgWrapper = styled.svg` const SvgWrapper = styled.svg`
:hover { :hover {
@ -15,9 +16,6 @@ const Path = styled.path`
fill: ${props => props.color}; fill: ${props => props.color};
`; `;
export const HIDDEN = 'M25.421,17.28l-3.167-8.8C22.175,8.24,21.858,8,21.542,8h-2.375c-0.475,0-0.792,0.32-0.792,0.8c0,0.48,0.317,0.8,0.792,0.8h1.821l2.612,7.2h-6.017h-3.167H8.4l2.613-7.2h1.821c0.475,0,0.792-0.32,0.792-0.8c0-0.48-0.317-0.8-0.792-0.8h-2.375c-0.317,0-0.633,0.24-0.712,0.56l-3.167,8.8C6.5,17.36,6.5,17.52,6.5,17.6l0,0l0,0l0,0v4c0,1.36,1.029,2.4,2.375,2.4h3.958c1.346,0,2.375-1.04,2.375-2.4v-3.2h1.583v3.2c0,1.36,1.029,2.4,2.375,2.4h3.958c1.346,0,2.375-1.04,2.375-2.4v-4l0,0l0,0l0,0C25.5,17.52,25.5,17.36,25.421,17.28z';
export const STANDARD = 'M23.333,10.667H10.667H10c-0.367,0-0.667-0.299-0.667-0.667S9.633,9.333,10,9.333h10V10h1.333V8.667C21.333,8.299,21.035,8,20.667,8H10c-1.105,0-2,0.895-2,2v11.333C8,22.806,9.194,24,10.667,24h12.667C23.701,24,24,23.701,24,23.333v-12C24,10.965,23.701,10.667,23.333,10.667z M20,18.667c-0.737,0-1.333-0.597-1.333-1.333C18.667,16.597,19.263,16,20,16s1.333,0.597,1.333,1.333C21.333,18.07,20.737,18.667,20,18.667z';
const Icon = ({ const Icon = ({
type = 'standard', type = 'standard',
size = 24, size = 24,
@ -29,13 +27,13 @@ const Icon = ({
hoverColor={hoverColor} hoverColor={hoverColor}
width={`${size}`} width={`${size}`}
height={`${size}`} height={`${size}`}
viewBox="0 0 32 32" viewBox="0 0 1024 1024"
onClick={onClick} onClick={onClick}
> >
<Path <Path
key={type} key={type}
color={color} color={color}
d={type === 'hidden' ? HIDDEN : STANDARD} d={type === 'hidden' ? ICONS.WALLET_HIDDEN : ICONS.WALLET_STANDARD}
/> />
</SvgWrapper> </SvgWrapper>
); );

@ -59,6 +59,11 @@ const StyledInput = styled.input`
background-color: ${colors.WHITE}; background-color: ${colors.WHITE};
transition: ${TRANSITION.HOVER}; transition: ${TRANSITION.HOVER};
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
}
&:disabled { &:disabled {
pointer-events: none; pointer-events: none;
background: ${colors.GRAY_LIGHT}; background: ${colors.GRAY_LIGHT};
@ -183,7 +188,7 @@ class Input extends PureComponent {
autoComplete="off" autoComplete="off"
height={this.props.height} height={this.props.height}
trezorAction={this.props.trezorAction} trezorAction={this.props.trezorAction}
hasIcon={this.getIcon(this.props.state).length > 0} hasIcon={this.props.icon || this.getIcon(this.props.state).length > 0}
ref={this.props.innerRef} ref={this.props.innerRef}
hasAddon={!!this.props.sideAddons} hasAddon={!!this.props.sideAddons}
type={this.props.type} type={this.props.type}

@ -0,0 +1,175 @@
/* @flow */
import * as React from 'react';
import PropTypes from 'prop-types';
import QrReader from 'react-qr-reader';
import styled from 'styled-components';
import colors from 'config/colors';
import icons from 'config/icons';
import { H2 } from 'components/Heading';
import P from 'components/Paragraph';
import Icon from 'components/Icon';
import Link from 'components/Link';
import { parseUri } from 'utils/cryptoUriParser';
import type { parsedURI } from 'utils/cryptoUriParser';
import type { Props as BaseProps } from '../Container';
const Wrapper = styled.div`
width: 90vw;
max-width: 450px;
padding: 30px 0px;
`;
const Padding = styled.div`
padding: 0px 48px;
`;
const CloseLink = styled(Link)`
position: absolute;
right: 15px;
top: 15px;
`;
const CameraPlaceholder = styled(P)`
text-align: center;
padding: 10px 0;
`;
const Error = styled.div`
padding: 10px 0;
`;
const ErrorTitle = styled(P)`
text-align: center;
color: ${colors.ERROR_PRIMARY};
`;
const ErrorMessage = styled.span`
text-align: center;
color: ${colors.TEXT_PRIMARY};
`;
const StyledQrReader = styled(QrReader)`
padding: 10px 0;
`;
// TODO fix types
type Props = {
onScan: (data: parsedURI) => any,
onError?: (error: any) => any,
onCancel?: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
}
type State = {
readerLoaded: boolean,
error: any,
};
class QrModal extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
readerLoaded: false,
error: null,
};
}
onLoad = () => {
this.setState({
readerLoaded: true,
});
}
handleScan = (data: string) => {
if (data) {
try {
const parsedUri = parseUri(data);
if (parsedUri) {
this.props.onScan(parsedUri);
// reset error
this.setState({
error: null,
});
// close window
this.handleCancel();
}
} catch (error) {
this.handleError(error);
}
}
}
handleError = (err: any) => {
// log thrown error
console.error(err);
if (this.props.onError) {
this.props.onError(err);
}
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError'
|| err.name === 'NotReadableError' || err.name === 'TrackStartError') {
this.setState({
error: 'Permission to access the camera was denied.',
});
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
this.setState({
error: 'The camera was not recognized.',
});
} else {
this.setState({
error: 'Unknown error. See console logs for details.',
});
}
}
handleCancel = () => {
if (this.props.onCancel) {
this.props.onCancel();
}
}
render() {
return (
<Wrapper>
<CloseLink onClick={this.handleCancel}>
<Icon
size={24}
color={colors.TEXT_SECONDARY}
icon={icons.CLOSE}
/>
</CloseLink>
<Padding>
<H2>Scan QR code</H2>
{!this.state.readerLoaded && !this.state.error && <CameraPlaceholder>Waiting for camera...</CameraPlaceholder>}
{this.state.error && (
<Error>
<ErrorTitle>Oops! Something went wrong!</ErrorTitle>
<ErrorMessage>{this.state.error.toString()}</ErrorMessage>
</Error>
)}
</Padding>
{!this.state.error && (
<StyledQrReader
delay={500}
onError={this.handleError}
onScan={this.handleScan}
onLoad={this.onLoad}
style={{ width: '100%' }}
showViewFinder={false}
/>
)}
</Wrapper>
);
}
}
QrModal.propTypes = {
onScan: PropTypes.func.isRequired,
onError: PropTypes.func,
onCancel: PropTypes.func,
};
export default QrModal;

@ -1,10 +1,15 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { H3 } from 'components/Heading'; import { H3 } from 'components/Heading';
import ICONS from 'config/icons'; import DeviceIcon from 'components/images/DeviceIcon';
import Icon from 'components/Icon'; import type { TrezorDevice } from 'flowtype';
type Props = {
device: TrezorDevice;
}
const Wrapper = styled.div``; const Wrapper = styled.div``;
@ -12,13 +17,18 @@ const Header = styled.div`
padding: 48px; padding: 48px;
`; `;
const ConfirmAction = () => ( const ConfirmAction = (props: Props) => (
<Wrapper> <Wrapper>
<Header> <Header>
<Icon icon={ICONS.T1} size={100} /> <DeviceIcon device={props.device} size={100} />
<H3>Confirm action on your Trezor</H3> <H3>Confirm action on your Trezor</H3>
</Header> </Header>
</Wrapper> </Wrapper>
); );
ConfirmAction.propTypes = {
device: PropTypes.object.isRequired,
};
export default ConfirmAction; export default ConfirmAction;

@ -13,11 +13,11 @@ import P from 'components/Paragraph';
import type { Props } from '../../Container'; import type { Props } from '../../Container';
const Wrapper = styled.div` const Wrapper = styled.div`
width: 390px; max-width: 390px;
`; `;
const Header = styled.div` const Header = styled.div`
padding: 24px 48px; padding: 30px 48px;
`; `;
const Content = styled.div` const Content = styled.div`

@ -0,0 +1,85 @@
/* @flow */
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import icons from 'config/icons';
import { getOldWalletUrl } from 'utils/url';
import colors from 'config/colors';
import { H2 } from 'components/Heading';
import P from 'components/Paragraph';
import Icon from 'components/Icon';
import Button from 'components/Button';
import Link from 'components/Link';
import type { TrezorDevice } from 'flowtype';
import type { Props as BaseProps } from '../../Container';
type Props = {
onReceiveConfirmation: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onReceiveConfirmation'>;
device: ?TrezorDevice;
}
const Wrapper = styled.div`
max-width: 370px;
padding: 30px 48px;
`;
const StyledLink = styled(Link)`
position: absolute;
right: 15px;
top: 15px;
`;
const BackupButton = styled(Button)`
width: 100%;
margin-bottom: 10px;
`;
const ProceedButton = styled(Button)`
background: transparent;
border-color: ${colors.WARNING_PRIMARY};
color: ${colors.WARNING_PRIMARY};
&:focus,
&:hover,
&:active {
color: ${colors.WHITE};
background: ${colors.WARNING_PRIMARY};
box-shadow: none;
}
`;
const StyledP = styled(P)`
padding-bottom: 20px;
`;
const Row = styled.div`
display: flex;
flex-direction: column;
`;
const Confirmation = (props: Props) => (
<Wrapper>
<StyledLink onClick={() => props.onReceiveConfirmation(false)}>
<Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
</StyledLink>
<H2>Your Trezor is not backed up</H2>
<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>
<Row>
<Link href={`${getOldWalletUrl(props.device)}/?backup`}>
<BackupButton onClick={() => props.onReceiveConfirmation(false)}>Create a backup in 3 minutes</BackupButton>
</Link>
<ProceedButton isWhite onClick={() => props.onReceiveConfirmation(true)}>Show address, I will take the risk</ProceedButton>
</Row>
</Wrapper>
);
Confirmation.propTypes = {
onReceiveConfirmation: PropTypes.func.isRequired,
};
export default Confirmation;

@ -4,12 +4,11 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import icons from 'config/icons';
import colors from 'config/colors'; import colors from 'config/colors';
import { LINE_HEIGHT, FONT_SIZE } from 'config/variables'; import { LINE_HEIGHT, FONT_SIZE, FONT_WEIGHT } from 'config/variables';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Icon from 'components/Icon'; import DeviceIcon from 'components/images/DeviceIcon';
import { H3 } from 'components/Heading'; import { H3 } from 'components/Heading';
import type { TrezorDevice, State } from 'flowtype'; import type { TrezorDevice, State } from 'flowtype';
@ -20,32 +19,45 @@ type Props = {
} }
const Wrapper = styled.div` const Wrapper = styled.div`
width: 390px; max-width: 390px;
padding: 12px 10px;
`; `;
const Header = styled.div` const Header = styled.div`
padding: 24px 48px; padding: 30px 48px;
`; `;
const Content = styled.div` const Content = styled.div`
border-top: 1px solid ${colors.DIVIDER}; border-top: 1px solid ${colors.DIVIDER};
background: ${colors.MAIN}; background: ${colors.MAIN};
padding: 24px 48px; padding: 30px 48px;
border-radius: 4px;
`; `;
const StyledP = styled(P)` const StyledP = styled(P)`
padding-bottom: 20px;
color: ${colors.TEXT};
font-size: ${FONT_SIZE.BASE};
&:last-child {
padding-bottom: 0px;
}
`;
const Address = styled(StyledP)`
word-wrap: break-word; word-wrap: break-word;
padding: 5px 0;
line-height: ${LINE_HEIGHT.SMALL}; line-height: ${LINE_HEIGHT.SMALL};
`; `;
const Label = styled.div` const Label = styled.div`
padding-top: 5px; padding-bottom: 6px;
font-weight: ${FONT_WEIGHT.MEDIUM};
font-size: ${FONT_SIZE.SMALL}; font-size: ${FONT_SIZE.SMALL};
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
`; `;
const FeeLevelName = styled(StyledP)`
padding-bottom: 0px;
`;
const ConfirmSignTx = (props: Props) => { const ConfirmSignTx = (props: Props) => {
const { const {
amount, amount,
@ -58,17 +70,18 @@ const ConfirmSignTx = (props: Props) => {
return ( return (
<Wrapper> <Wrapper>
<Header> <Header>
<Icon icon={icons.T1} size={60} color={colors.TEXT_SECONDARY} /> <DeviceIcon device={props.device} size={60} color={colors.TEXT_SECONDARY} />
<H3>Confirm transaction on { props.device.label } device</H3> <H3>Confirm transaction on { props.device.label } device</H3>
<P isSmaller>Details are shown on display</P> <P isSmaller>Details are shown on display</P>
</Header> </Header>
<Content> <Content>
<Label>Send</Label> <Label>Send</Label>
<P>{`${amount} ${currency}` }</P> <StyledP>{`${amount} ${currency}` }</StyledP>
<Label>To</Label> <Label>To</Label>
<StyledP>{ address }</StyledP> <Address>{ address }</Address>
<Label>Fee</Label> <Label>Fee</Label>
<P>{ selectedFeeLevel.label }</P> <FeeLevelName>{selectedFeeLevel.value}</FeeLevelName>
<StyledP>{ selectedFeeLevel.label }</StyledP>
</Content> </Content>
</Wrapper> </Wrapper>
); );

@ -2,7 +2,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { getOldWalletUrl } from 'utils/url';
import icons from 'config/icons'; import icons from 'config/icons';
import colors from 'config/colors'; import colors from 'config/colors';
@ -30,22 +30,51 @@ const StyledLink = styled(Link)`
`; `;
const Wrapper = styled.div` const Wrapper = styled.div`
width: 370px; max-width: 370px;
padding: 24px 48px; padding: 30px 0px;
`;
const Content = styled.div`
padding: 0px 48px;
`; `;
const StyledP = styled(P)` const StyledP = styled(P)`
padding: 10px 0px; padding-bottom: 20px;
`;
const Divider = styled.div`
width: 100%;
height: 1px;
background: ${colors.DIVIDER};
margin: 20px 0px;
`; `;
const Row = styled.div` const Row = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px 0;
Button + Button {
margin-top: 10px;
}
`;
const BackupButton = styled(Button)`
width: 100%;
`; `;
const StyledButton = styled(Button)` const WarnButton = styled(Button)`
margin: 0 0 10px 0; background: transparent;
border-color: ${colors.WARNING_PRIMARY};
color: ${colors.WARNING_PRIMARY};
&:focus,
&:hover,
&:active {
color: ${colors.WHITE};
background: ${colors.WARNING_PRIMARY};
box-shadow: none;
}
`; `;
class ConfirmUnverifiedAddress extends PureComponent<Props> { class ConfirmUnverifiedAddress extends PureComponent<Props> {
@ -87,26 +116,48 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
let claim: string; let claim: string;
if (!device.connected) { if (!device.connected) {
deviceStatus = `${device.label} is not connected`; deviceStatus = `Device ${device.label} is not connected`;
claim = 'Please connect your device'; claim = '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: string = device.features && device.features.passphrase_protection ? 'enable' : 'disable'; const enable: string = device.features && device.features.passphrase_protection ? 'enable' : 'disable';
deviceStatus = `${device.label} is unavailable`; deviceStatus = `Device ${device.label} is unavailable`;
claim = `Please ${enable} passphrase settings`; claim = `Please ${enable} passphrase settings`;
} }
const needsBackup = device.features && device.features.needs_backup;
return ( return (
<Wrapper> <Wrapper>
<StyledLink onClick={onCancel}> <Content>
<Icon size={20} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} /> <StyledLink onClick={onCancel}>
</StyledLink> <Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
<H2>{ deviceStatus }</H2> </StyledLink>
<StyledP isSmaller>To prevent phishing attacks, you should verify the address on your Trezor first. { claim } to continue with the verification process.</StyledP> <H2>{ deviceStatus }</H2>
<Row> <StyledP isSmaller>To prevent phishing attacks, you should verify the address on your Trezor first. { claim } to continue with the verification process.</StyledP>
<StyledButton onClick={() => (!account ? this.verifyAddress() : 'false')}>Try again</StyledButton> </Content>
<StyledButton isWhite onClick={() => this.showUnverifiedAddress()}>Show unverified address</StyledButton> <Content>
</Row> <Row>
<Button onClick={() => (!account ? this.verifyAddress() : 'false')}>Try again</Button>
<WarnButton isWhite onClick={() => this.showUnverifiedAddress()}>Show unverified address</WarnButton>
</Row>
</Content>
{needsBackup && <Divider />}
{needsBackup && (
<>
<Content>
<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>
</Content>
<Content>
<Row>
<Link href={`${getOldWalletUrl(device)}/?backup`}>
<BackupButton>Create a backup in 3 minutes</BackupButton>
</Link>
</Row>
</Content>
</>
)}
</Wrapper> </Wrapper>
); );
} }

@ -41,7 +41,7 @@ const StyledLink = styled(Link)`
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px; width: 360px;
padding: 24px 48px; padding: 30px 48px;
`; `;
const Column = styled.div` const Column = styled.div`
@ -138,7 +138,7 @@ class DuplicateDevice extends PureComponent<Props, State> {
return ( return (
<Wrapper> <Wrapper>
<StyledLink onClick={onCancel}> <StyledLink onClick={onCancel}>
<Icon size={20} 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>

@ -4,7 +4,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { H3 } from 'components/Heading'; import { H2 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Button from 'components/Button'; import Button from 'components/Button';
@ -19,21 +19,20 @@ type Props = {
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px; width: 360px;
padding: 24px 48px; padding: 30px 48px;
`; `;
const StyledP = styled(P)` const StyledP = styled(P)`
padding: 7px 0px; padding: 20px 0px;
`;
const StyledButton = styled(Button)`
margin: 0 0 10px 0;
`; `;
const Row = styled.div` const Row = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px 0;
Button + Button {
margin-top: 10px;
}
`; `;
class ForgetDevice extends PureComponent<Props> { class ForgetDevice extends PureComponent<Props> {
@ -62,11 +61,11 @@ class ForgetDevice extends PureComponent<Props> {
render() { render() {
return ( return (
<Wrapper> <Wrapper>
<H3>Forget { this.props.device.instanceLabel }?</H3> <H2>Forget { this.props.device.instanceLabel }?</H2>
<StyledP isSmaller>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.</StyledP> <StyledP isSmaller>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.</StyledP>
<Row> <Row>
<StyledButton onClick={() => this.forget()}>Forget</StyledButton> <Button onClick={() => this.forget()}>Forget</Button>
<StyledButton isWhite onClick={this.props.onCancel}>Don&apos;t forget</StyledButton> <Button isWhite onClick={this.props.onCancel}>Don&apos;t forget</Button>
</Row> </Row>
</Wrapper> </Wrapper>
); );

@ -31,12 +31,12 @@ const ButtonContent = styled.div`
`; `;
const StyledP = styled(P)` const StyledP = styled(P)`
padding: 10px 0; padding: 20px 0;
`; `;
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px; width: 360px;
padding: 24px 48px; padding: 30px 48px;
`; `;
const Text = styled.div` const Text = styled.div`
@ -46,10 +46,10 @@ const Text = styled.div`
const Column = styled.div` const Column = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`;
const StyledButton = styled(Button)` Button + Button {
margin: 5px 0; margin-top: 10px;
}
`; `;
const StyledLoader = styled(Loader)` const StyledLoader = styled(Loader)`
@ -128,7 +128,7 @@ class RememberDevice extends PureComponent<Props, State> {
<H3>Forget {label}?</H3> <H3>Forget {label}?</H3>
<StyledP isSmaller>Would you like Trezor Wallet to forget your { devicePlural }, so that it is still visible even while disconnected?</StyledP> <StyledP isSmaller>Would you like Trezor Wallet to forget your { devicePlural }, so that it is still visible even while disconnected?</StyledP>
<Column> <Column>
<StyledButton onClick={() => this.forget()}> <Button onClick={() => this.forget()}>
<ButtonContent> <ButtonContent>
<Text>Forget</Text> <Text>Forget</Text>
<StyledLoader <StyledLoader
@ -138,12 +138,12 @@ class RememberDevice extends PureComponent<Props, State> {
text={this.state.countdown.toString()} text={this.state.countdown.toString()}
/> />
</ButtonContent> </ButtonContent>
</StyledButton> </Button>
<StyledButton <Button
isWhite isWhite
onClick={() => onRememberDevice(device)} onClick={() => onRememberDevice(device)}
>Remember >Remember
</StyledButton> </Button>
</Column> </Column>
</Wrapper> </Wrapper>
); );

@ -7,7 +7,7 @@ import styled, { css } from 'styled-components';
import icons from 'config/icons'; import icons from 'config/icons';
import colors from 'config/colors'; import colors from 'config/colors';
import { H3 } from 'components/Heading'; import { H2 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Button from 'components/Button'; import Button from 'components/Button';
import Tooltip from 'components/Tooltip'; import Tooltip from 'components/Tooltip';
@ -25,7 +25,6 @@ type Props = {
} }
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px;
`; `;
const Header = styled.div` const Header = styled.div`
@ -36,8 +35,8 @@ const Header = styled.div`
color: ${colors.TEXT_PRIMARY}; color: ${colors.TEXT_PRIMARY};
`; `;
const StyledHeading = styled(H3)` const StyledHeading = styled(H2)`
padding-top: 30px; padding: 30px 48px 10px 48px;
`; `;
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
@ -105,7 +104,7 @@ class WalletType extends PureComponent<Props> {
{ device.state && ( { device.state && (
<StyledLink onClick={onCancel}> <StyledLink onClick={onCancel}>
<Icon <Icon
size={20} size={24}
color={colors.TEXT_SECONDARY} color={colors.TEXT_SECONDARY}
icon={icons.CLOSE} icon={icons.CLOSE}
/> />

@ -26,7 +26,7 @@ const Wrapper = styled.div`
`; `;
const StyledButton = styled(Button)` const StyledButton = styled(Button)`
margin: 10px 0 10px 0; margin-top: 10px;
width: 100%; width: 100%;
`; `;
@ -48,7 +48,7 @@ const CardanoWallet = (props: Props) => (
<Wrapper> <Wrapper>
<StyledLink onClick={props.onCancel}> <StyledLink onClick={props.onCancel}>
<Icon <Icon
size={20} size={24}
color={colors.TEXT_SECONDARY} color={colors.TEXT_SECONDARY}
icon={icons.CLOSE} icon={icons.CLOSE}
/> />

@ -22,11 +22,11 @@ type Props = {
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
max-width: 620px; max-width: 620px;
padding: 24px 48px; padding: 30px 48px;
`; `;
const StyledButton = styled(Button)` const StyledButton = styled(Button)`
margin: 0 0 10px 0; margin-top: 10px;
width: 100%; width: 100%;
`; `;
@ -46,7 +46,7 @@ const NemWallet = (props: Props) => (
<Wrapper> <Wrapper>
<StyledLink onClick={props.onCancel}> <StyledLink onClick={props.onCancel}>
<Icon <Icon
size={20} size={24}
color={colors.TEXT_SECONDARY} color={colors.TEXT_SECONDARY}
icon={icons.CLOSE} icon={icons.CLOSE}
/> />

@ -26,7 +26,7 @@ const Wrapper = styled.div`
`; `;
const StyledButton = styled(Button)` const StyledButton = styled(Button)`
margin: 10px 0 10px 0; margin-top: 10px;
width: 100%; width: 100%;
`; `;
@ -48,7 +48,7 @@ const StellarWallet = (props: Props) => (
<Wrapper> <Wrapper>
<StyledLink onClick={props.onCancel}> <StyledLink onClick={props.onCancel}>
<Icon <Icon
size={20} size={24}
color={colors.TEXT_SECONDARY} color={colors.TEXT_SECONDARY}
icon={icons.CLOSE} icon={icons.CLOSE}
/> />

@ -19,6 +19,7 @@ import PassphraseType from 'components/modals/passphrase/Type';
import ConfirmSignTx from 'components/modals/confirm/SignTx'; import ConfirmSignTx from 'components/modals/confirm/SignTx';
import ConfirmAction from 'components/modals/confirm/Action'; import ConfirmAction from 'components/modals/confirm/Action';
import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress'; import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress';
import ConfirmNoBackup from 'components/modals/confirm/NoBackup';
import ForgetDevice from 'components/modals/device/Forget'; import ForgetDevice from 'components/modals/device/Forget';
import RememberDevice from 'components/modals/device/Remember'; import RememberDevice from 'components/modals/device/Remember';
import DuplicateDevice from 'components/modals/device/Duplicate'; import DuplicateDevice from 'components/modals/device/Duplicate';
@ -29,6 +30,8 @@ import Nem from 'components/modals/external/Nem';
import Cardano from 'components/modals/external/Cardano'; import Cardano from 'components/modals/external/Cardano';
import Stellar from 'components/modals/external/Stellar'; import Stellar from 'components/modals/external/Stellar';
import QrModal from 'components/modals/QrModal';
import type { Props } from './Container'; import type { Props } from './Container';
const ModalContainer = styled.div` const ModalContainer = styled.div`
@ -66,7 +69,8 @@ const getDeviceContextModal = (props: Props) => {
<Pin <Pin
device={modal.device} device={modal.device}
onPinSubmit={modalActions.onPinSubmit} onPinSubmit={modalActions.onPinSubmit}
/>); />
);
case UI.INVALID_PIN: case UI.INVALID_PIN:
return <InvalidPin device={modal.device} />; return <InvalidPin device={modal.device} />;
@ -77,7 +81,8 @@ const getDeviceContextModal = (props: Props) => {
device={modal.device} device={modal.device}
selectedDevice={props.wallet.selectedDevice} selectedDevice={props.wallet.selectedDevice}
onPassphraseSubmit={modalActions.onPassphraseSubmit} onPassphraseSubmit={modalActions.onPassphraseSubmit}
/>); />
);
case 'ButtonRequest_PassphraseType': case 'ButtonRequest_PassphraseType':
return <PassphraseType device={modal.device} />; return <PassphraseType device={modal.device} />;
@ -94,11 +99,11 @@ const getDeviceContextModal = (props: Props) => {
} }
case 'ButtonRequest_ProtectCall': case 'ButtonRequest_ProtectCall':
return <ConfirmAction />; return <ConfirmAction device={modal.device} />;
case 'ButtonRequest_Other': case 'ButtonRequest_Other':
case 'ButtonRequest_ConfirmOutput': case 'ButtonRequest_ConfirmOutput':
return <ConfirmAction />; return <ConfirmAction device={modal.device} />;
case RECEIVE.REQUEST_UNVERIFIED: case RECEIVE.REQUEST_UNVERIFIED:
return ( return (
@ -108,7 +113,8 @@ const getDeviceContextModal = (props: Props) => {
onCancel={modalActions.onCancel} onCancel={modalActions.onCancel}
showAddress={props.receiveActions.showAddress} showAddress={props.receiveActions.showAddress}
showUnverifiedAddress={props.receiveActions.showUnverifiedAddress} showUnverifiedAddress={props.receiveActions.showUnverifiedAddress}
/>); />
);
case CONNECT.REMEMBER_REQUEST: case CONNECT.REMEMBER_REQUEST:
return ( return (
@ -117,7 +123,8 @@ const getDeviceContextModal = (props: Props) => {
instances={modal.instances} instances={modal.instances}
onRememberDevice={modalActions.onRememberDevice} onRememberDevice={modalActions.onRememberDevice}
onForgetDevice={modalActions.onForgetDevice} onForgetDevice={modalActions.onForgetDevice}
/>); />
);
case CONNECT.FORGET_REQUEST: case CONNECT.FORGET_REQUEST:
return ( return (
@ -125,7 +132,8 @@ const getDeviceContextModal = (props: Props) => {
device={modal.device} device={modal.device}
onForgetSingleDevice={modalActions.onForgetSingleDevice} onForgetSingleDevice={modalActions.onForgetSingleDevice}
onCancel={modalActions.onCancel} onCancel={modalActions.onCancel}
/>); />
);
case CONNECT.TRY_TO_DUPLICATE: case CONNECT.TRY_TO_DUPLICATE:
return ( return (
@ -134,7 +142,8 @@ const getDeviceContextModal = (props: Props) => {
devices={props.devices} devices={props.devices}
onDuplicateDevice={modalActions.onDuplicateDevice} onDuplicateDevice={modalActions.onDuplicateDevice}
onCancel={modalActions.onCancel} onCancel={modalActions.onCancel}
/>); />
);
case CONNECT.REQUEST_WALLET_TYPE: case CONNECT.REQUEST_WALLET_TYPE:
return ( return (
@ -142,7 +151,8 @@ const getDeviceContextModal = (props: Props) => {
device={modal.device} device={modal.device}
onWalletTypeRequest={modalActions.onWalletTypeRequest} onWalletTypeRequest={modalActions.onWalletTypeRequest}
onCancel={modalActions.onCancel} onCancel={modalActions.onCancel}
/>); />
);
default: default:
return null; return null;
@ -166,6 +176,33 @@ const getExternalContextModal = (props: Props) => {
} }
}; };
const getQrModal = (props: Props) => {
const { modalActions, selectedAccount } = props;
if (!selectedAccount.network) return null;
const networkType = selectedAccount.network.type;
return (
<QrModal
onCancel={modalActions.onCancel}
onScan={parsedUri => modalActions.onQrScan(parsedUri, networkType)}
/>
);
};
const getConfirmationModal = (props: Props) => {
const { modal, modalActions, wallet } = props;
if (modal.context !== MODAL.CONTEXT_CONFIRMATION) return null;
switch (modal.windowType) {
case 'no-backup':
return (<ConfirmNoBackup device={wallet.selectedDevice} onReceiveConfirmation={modalActions.onReceiveConfirmation} />);
default:
return null;
}
};
// modal container component // modal container component
const Modal = (props: Props) => { const Modal = (props: Props) => {
const { modal } = props; const { modal } = props;
@ -179,6 +216,12 @@ const Modal = (props: Props) => {
case MODAL.CONTEXT_EXTERNAL_WALLET: case MODAL.CONTEXT_EXTERNAL_WALLET:
component = getExternalContextModal(props); component = getExternalContextModal(props);
break; break;
case MODAL.CONTEXT_SCAN_QR:
component = getQrModal(props);
break;
case MODAL.CONTEXT_CONFIRMATION:
component = getConfirmationModal(props);
break;
default: default:
break; break;
} }

@ -31,7 +31,7 @@ type State = {
}; };
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 24px 48px; padding: 30px 48px;
max-width: 390px; max-width: 390px;
`; `;

@ -4,10 +4,9 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import icons from 'config/icons';
import colors from 'config/colors'; import colors from 'config/colors';
import Icon from 'components/Icon'; import DeviceIcon from 'components/images/DeviceIcon';
import { H3 } from 'components/Heading'; import { H3 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
@ -18,8 +17,8 @@ type Props = {
} }
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px; max-width: 360px;
padding: 24px 48px; padding: 30px 48px;
`; `;
const Header = styled.div``; const Header = styled.div``;
@ -27,7 +26,7 @@ const Header = styled.div``;
const PassphraseType = (props: Props) => ( const PassphraseType = (props: Props) => (
<Wrapper> <Wrapper>
<Header> <Header>
<Icon icon={icons.T1} size={60} color={colors.TEXT_SECONDARY} /> <DeviceIcon device={props.device} size={60} color={colors.TEXT_SECONDARY} />
<H3>Complete the action on { props.device.label } device</H3> <H3>Complete the action on { props.device.label } device</H3>
<P isSmaller>If you enter a wrong passphrase, you will not unlock the desired hidden wallet.</P> <P isSmaller>If you enter a wrong passphrase, you will not unlock the desired hidden wallet.</P>
</Header> </Header>

@ -14,7 +14,7 @@ type Props = {
} }
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 24px 48px; padding: 30px 48px;
`; `;
const InvalidPin = (props: Props) => ( const InvalidPin = (props: Props) => (

@ -4,7 +4,7 @@ import * as React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables'; import { FONT_SIZE, FONT_WEIGHT, SCREEN_SIZE } from 'config/variables';
type Props = { type Props = {
onClick: () => void; onClick: () => void;
@ -22,6 +22,12 @@ const Wrapper = styled.button`
border: 1px solid ${colors.DIVIDER}; border: 1px solid ${colors.DIVIDER};
background: ${colors.WHITE}; background: ${colors.WHITE};
transition: all 0.3s; transition: all 0.3s;
cursor: pointer;
@media screen and (max-width: ${SCREEN_SIZE.XS}) {
width: 50px;
height: 50px;
}
&:first-child { &:first-child {
margin-left: 0px; margin-left: 0px;

@ -25,7 +25,7 @@ type State = {
} }
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 24px 48px; padding: 30px 48px;
`; `;
const InputRow = styled.div` const InputRow = styled.div`

@ -0,0 +1,25 @@
/* @flow */
import * as React from 'react';
import Notification from 'components/Notification';
import type { Props } from '../../index';
export default (props: Props) => {
const { selectedDevice } = props.wallet;
const needsBackup = selectedDevice && selectedDevice.features && selectedDevice.features.needs_backup;
if (!needsBackup) return null;
return (
<Notification
key="no-backup"
type="warning"
title="Your Trezor is not backed up!"
message="If your device is ever lost or damaged, your funds will be lost. Backup your device first, to protect your coins against such events."
actions={
[{
label: 'Create a backup',
callback: props.routerActions.gotoBackup,
}]
}
/>
);
};

@ -12,6 +12,7 @@ import * as RouterActions from 'actions/RouterActions';
import OnlineStatus from './components/OnlineStatus'; import OnlineStatus from './components/OnlineStatus';
import UpdateBridge from './components/UpdateBridge'; import UpdateBridge from './components/UpdateBridge';
import UpdateFirmware from './components/UpdateFirmware'; import UpdateFirmware from './components/UpdateFirmware';
import NoBackup from './components/NoBackup';
export type StateProps = { export type StateProps = {
connect: $ElementType<State, 'connect'>; connect: $ElementType<State, 'connect'>;
@ -33,6 +34,7 @@ const Notifications = (props: Props) => (
<OnlineStatus {...props} /> <OnlineStatus {...props} />
<UpdateBridge {...props} /> <UpdateBridge {...props} />
<UpdateFirmware {...props} /> <UpdateFirmware {...props} />
<NoBackup {...props} />
</React.Fragment> </React.Fragment>
); );

@ -8,6 +8,7 @@ import type { Props } from '../../index';
export default (props: Props) => { export default (props: Props) => {
const { network, notification } = props.selectedAccount; const { network, notification } = props.selectedAccount;
if (!network || !notification) return null; if (!network || !notification) return null;
const blockchain = props.blockchain.find(b => b.shortcut === network.shortcut);
if (notification.type === 'backend') { if (notification.type === 'backend') {
// special case: backend is down // special case: backend is down
@ -17,6 +18,7 @@ export default (props: Props) => {
type="error" type="error"
title={notification.title} title={notification.title}
message={notification.message} message={notification.message}
isActionInProgress={blockchain && blockchain.connecting}
actions={ actions={
[{ [{
label: 'Connect', label: 'Connect',

@ -2,18 +2,39 @@
import * as React from 'react'; import * as React from 'react';
import Notification from 'components/Notification'; import Notification from 'components/Notification';
import Bignumber from 'bignumber.js';
import Link from 'components/Link';
import type { Props } from '../../index'; import type { Props } from '../../index';
export default (props: Props) => { export default (props: Props) => {
const { selectedAccount } = props;
const { account } = selectedAccount;
const { location } = props.router; const { location } = props.router;
if (!location) return null;
const notifications: Array<Notification> = []; const notifications: Array<Notification> = [];
// Example:
// if (location.state.device) { if (!location || !selectedAccount || !account) return null;
// notifications.push(<Notification key="example" type="info" title="Static example" />);
// } // Ripple minimum reserve notification
if (account.networkType === 'ripple') {
const { reserve, balance } = account;
const bigBalance = new Bignumber(balance);
const bigReserve = new Bignumber(reserve);
if (bigBalance.isLessThan(bigReserve)) {
notifications.push(
<Notification
key="xrp-warning"
type="warning"
title="Minimum account reserve required"
message={(
<>
{`Ripple addresses require a minimum balance of ${bigReserve.toString()} XRP to activate and maintain the account. `}
<Link isGreen href="https://wiki.trezor.io/Ripple_(XRP)">Learn more</Link>
</>
)}
/>,
);
}
}
return ( return (
<React.Fragment> <React.Fragment>

@ -49,6 +49,12 @@ export const GREEN_COLOR = keyframes`
} }
`; `;
export const WHITE_COLOR = keyframes`
0%, 100% {
stroke: white;
}
`;
export const PULSATE = keyframes` export const PULSATE = keyframes`
0%, 100% { 0%, 100% {
opacity: 0.5; opacity: 0.5;
@ -65,4 +71,31 @@ export const FADE_IN = keyframes`
100% { 100% {
opacity: 1; opacity: 1;
} }
`;
export const SLIDE_DOWN = keyframes`
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0%);
}
`;
export const SLIDE_RIGHT = keyframes`
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0%);
}
`;
export const SLIDE_LEFT = keyframes`
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
`; `;

@ -34,4 +34,7 @@ export default {
LABEL_COLOR: '#A9A9A9', LABEL_COLOR: '#A9A9A9',
TOOLTIP_BACKGROUND: '#333333', TOOLTIP_BACKGROUND: '#333333',
INPUT_FOCUSED_BORDER: '#A9A9A9',
INPUT_FOCUSED_SHADOW: '#d6d7d7',
}; };

@ -24,6 +24,9 @@ export default {
T1: [ T1: [
'M603.2 265.6h-6.4c-25.494-5.341-54.79-8.398-84.8-8.398s-59.305 3.058-87.592 8.879l2.792-0.48h-6.72c-30.053 5.643-52.489 31.68-52.489 62.956 0 0.367 0.003 0.733 0.009 1.099l-0.001-0.055v234.88c0.075 40.921 11.238 79.22 30.643 112.071l-0.563-1.031 35.2 60.48c11.655 19.297 32.515 32.001 56.342 32.001 0.105 0 0.209 0 0.314-0.001h44.144c0.359 0.007 0.783 0.011 1.208 0.011 23.569 0 44.162-12.74 55.269-31.709l0.164-0.302 36.16-64c18.232-31.447 29.027-69.173 29.12-109.413v-232.987c0.005-0.293 0.008-0.639 0.008-0.986 0-31.391-22.599-57.503-52.416-62.954l-0.392-0.059zM629.76 563.2c-0.193 35.364-9.792 68.446-26.418 96.923l0.498-0.923-35.84 64c-6.868 11.865-19.463 19.742-33.906 19.84h-44.174c-0.073 0-0.159 0.001-0.246 0.001-14.427 0-27.041-7.762-33.894-19.338l-0.1-0.183-34.88-59.84c-16.656-28.155-26.515-62.042-26.56-98.227v-235.853c0.133-19.025 13.742-34.833 31.751-38.359l0.249-0.041h6.72c24.050-5.126 51.682-8.062 80-8.062s55.949 2.936 82.608 8.519l-2.608-0.457h6.72c18.258 3.568 31.867 19.375 32 38.386v0.014zM422.4 353.92h179.2c3.535 0 6.4 2.865 6.4 6.4v99.2c0 3.535-2.865 6.4-6.4 6.4h-179.2c-3.535 0-6.4-2.865-6.4-6.4v-99.2c0-3.535 2.865-6.4 6.4-6.4z', 'M603.2 265.6h-6.4c-25.494-5.341-54.79-8.398-84.8-8.398s-59.305 3.058-87.592 8.879l2.792-0.48h-6.72c-30.053 5.643-52.489 31.68-52.489 62.956 0 0.367 0.003 0.733 0.009 1.099l-0.001-0.055v234.88c0.075 40.921 11.238 79.22 30.643 112.071l-0.563-1.031 35.2 60.48c11.655 19.297 32.515 32.001 56.342 32.001 0.105 0 0.209 0 0.314-0.001h44.144c0.359 0.007 0.783 0.011 1.208 0.011 23.569 0 44.162-12.74 55.269-31.709l0.164-0.302 36.16-64c18.232-31.447 29.027-69.173 29.12-109.413v-232.987c0.005-0.293 0.008-0.639 0.008-0.986 0-31.391-22.599-57.503-52.416-62.954l-0.392-0.059zM629.76 563.2c-0.193 35.364-9.792 68.446-26.418 96.923l0.498-0.923-35.84 64c-6.868 11.865-19.463 19.742-33.906 19.84h-44.174c-0.073 0-0.159 0.001-0.246 0.001-14.427 0-27.041-7.762-33.894-19.338l-0.1-0.183-34.88-59.84c-16.656-28.155-26.515-62.042-26.56-98.227v-235.853c0.133-19.025 13.742-34.833 31.751-38.359l0.249-0.041h6.72c24.050-5.126 51.682-8.062 80-8.062s55.949 2.936 82.608 8.519l-2.608-0.457h6.72c18.258 3.568 31.867 19.375 32 38.386v0.014zM422.4 353.92h179.2c3.535 0 6.4 2.865 6.4 6.4v99.2c0 3.535-2.865 6.4-6.4 6.4h-179.2c-3.535 0-6.4-2.865-6.4-6.4v-99.2c0-3.535 2.865-6.4 6.4-6.4z',
], ],
T2: [
'M 625.28 546.304 c 0 4.512 -3.84 8 -8.32 8 l -209.92 0 c -4.48 0 -8.32 -3.488 -8.32 -8 l 0 -202.208 c 0 -4.512 3.84 -8.32 8.32 -8.32 l 209.92 0 c 4.48 0 8.32 3.808 8.32 8.32 l 0 202.208 Z m 18.56 -304.32 l -263.68 0 c -23.04 0 -41.92 18.56 -41.92 41.28 l 0 233.952 c 0 55.04 16 108.768 46.72 155.168 l 64.64 96.992 c 5.12 8 13.76 12.448 23.36 12.448 l 78.4 0 c 9.28 0 17.92 -4.448 23.04 -11.84 l 60.16 -86.048 c 33.6 -47.68 51.2 -103.392 51.2 -161.28 l 0 -239.392 c 0 -22.72 -18.88 -41.28 -41.92 -41.28',
],
COG: [ COG: [
'M739.552 462.144h-71.328c-4.256-13.664-10.208-26.56-17.472-38.56l47.264-47.424c11.2-11.008 11.2-29.056 0-40.192l-20.064-20.032c-11.136-11.104-29.152-11.040-40.192 0l-48.128 48.032c-12.992-7.392-27.072-13.152-42.080-16.992v-62.496c0-15.68-12.672-28.48-28.448-28.48h-28.448c-15.68 0-28.416 12.8-28.416 28.48v62.464c-16.352 4.128-31.68 10.656-45.728 19.2l-40.288-40.224c-11.072-11.040-29.184-11.104-40.288 0l-20.096 20.096c-11.104 11.072-10.976 29.152 0.064 40.288l40.992 40.992c-8.672 15.136-15.168 31.648-18.88 49.152h-53.504c-15.776 0-28.544 12.736-28.544 28.48v28.416c0 15.68 12.768 28.416 28.544 28.416h57.152c5.184 17.152 12.992 32.928 23.008 47.328l-38.656 38.656c-11.136 11.136-11.136 29.216-0.064 40.288l20.064 20.096c11.2 11.040 29.248 11.040 40.32-0.032l43.232-43.2c14.528 7.232 30.336 12.48 46.944 15.2v59.488c0 15.68 12.736 28.448 28.448 28.48h28.448c15.68-0.032 28.448-12.8 28.448-28.48v-66.816c14.336-5.088 27.904-11.872 40.224-20.544l45.76 45.888c11.104 11.072 29.12 11.072 40.224 0l20.096-20.128c11.168-11.072 11.168-29.056-0.096-40.288l-50.144-50.24c6.144-12.512 10.944-25.792 13.92-39.904h67.776c15.744 0 28.448-12.672 28.48-28.448v-28.448c-0.096-15.68-12.8-28.512-28.544-28.512zM504.928 583.072c-39.264 0-71.072-31.776-71.072-71.104 0-39.264 31.808-71.040 71.072-71.040 39.296 0 71.136 31.776 71.136 71.040 0 39.328-31.84 71.104-71.136 71.104z', 'M739.552 462.144h-71.328c-4.256-13.664-10.208-26.56-17.472-38.56l47.264-47.424c11.2-11.008 11.2-29.056 0-40.192l-20.064-20.032c-11.136-11.104-29.152-11.040-40.192 0l-48.128 48.032c-12.992-7.392-27.072-13.152-42.080-16.992v-62.496c0-15.68-12.672-28.48-28.448-28.48h-28.448c-15.68 0-28.416 12.8-28.416 28.48v62.464c-16.352 4.128-31.68 10.656-45.728 19.2l-40.288-40.224c-11.072-11.040-29.184-11.104-40.288 0l-20.096 20.096c-11.104 11.072-10.976 29.152 0.064 40.288l40.992 40.992c-8.672 15.136-15.168 31.648-18.88 49.152h-53.504c-15.776 0-28.544 12.736-28.544 28.48v28.416c0 15.68 12.768 28.416 28.544 28.416h57.152c5.184 17.152 12.992 32.928 23.008 47.328l-38.656 38.656c-11.136 11.136-11.136 29.216-0.064 40.288l20.064 20.096c11.2 11.040 29.248 11.040 40.32-0.032l43.232-43.2c14.528 7.232 30.336 12.48 46.944 15.2v59.488c0 15.68 12.736 28.448 28.448 28.48h28.448c15.68-0.032 28.448-12.8 28.448-28.48v-66.816c14.336-5.088 27.904-11.872 40.224-20.544l45.76 45.888c11.104 11.072 29.12 11.072 40.224 0l20.096-20.128c11.168-11.072 11.168-29.056-0.096-40.288l-50.144-50.24c6.144-12.512 10.944-25.792 13.92-39.904h67.776c15.744 0 28.448-12.672 28.48-28.448v-28.448c-0.096-15.68-12.8-28.512-28.544-28.512zM504.928 583.072c-39.264 0-71.072-31.776-71.072-71.104 0-39.264 31.808-71.040 71.072-71.040 39.296 0 71.136 31.776 71.136 71.040 0 39.328-31.84 71.104-71.136 71.104z',
], ],
@ -67,6 +70,18 @@ export default {
SUCCESS: [ SUCCESS: [
'M692.8 313.92l-1.92-1.92c-6.246-7.057-15.326-11.484-25.44-11.484s-19.194 4.427-25.409 11.448l-0.031 0.036-196.48 224-3.84 1.6-3.84-1.92-48.64-57.28c-7.010-7.905-17.193-12.862-28.533-12.862-21.031 0-38.080 17.049-38.080 38.080 0 7.495 2.165 14.485 5.905 20.377l-0.092-0.155 100.8 148.16c5.391 8.036 14.386 13.292 24.618 13.44h8.662c17.251-0.146 32.385-9.075 41.163-22.529l0.117-0.191 195.2-296.32c4.473-6.632 7.141-14.803 7.141-23.597 0-11.162-4.297-21.32-11.326-28.911l0.025 0.028z', 'M692.8 313.92l-1.92-1.92c-6.246-7.057-15.326-11.484-25.44-11.484s-19.194 4.427-25.409 11.448l-0.031 0.036-196.48 224-3.84 1.6-3.84-1.92-48.64-57.28c-7.010-7.905-17.193-12.862-28.533-12.862-21.031 0-38.080 17.049-38.080 38.080 0 7.495 2.165 14.485 5.905 20.377l-0.092-0.155 100.8 148.16c5.391 8.036 14.386 13.292 24.618 13.44h8.662c17.251-0.146 32.385-9.075 41.163-22.529l0.117-0.191 195.2-296.32c4.473-6.632 7.141-14.803 7.141-23.597 0-11.162-4.297-21.32-11.326-28.911l0.025 0.028z',
], ],
WALLET_STANDARD: [
'M746.656,341.344l-405.312,0l-21.344,0c-11.744,0 -21.344,-9.568 -21.344,-21.344c0,-11.776 9.6,-21.344 21.344,-21.344l320,0l0,21.344l42.656,0l0,-42.656c0,-11.776 -9.536,-21.344 -21.312,-21.344l-341.344,0c-35.36,0 -64,28.64 -64,64l0,362.656c0,47.136 38.208,85.344 85.344,85.344l405.344,0c11.744,0 21.312,-9.568 21.312,-21.344l0,-384c0,-11.776 -9.568,-21.312 -21.344,-21.312Zm-106.656,256c-23.584,0 -42.656,-19.104 -42.656,-42.656c0,-23.584 19.072,-42.688 42.656,-42.688c23.584,0 42.656,19.104 42.656,42.656c0,23.584 -19.072,42.688 -42.656,42.688Z',
],
WALLET_HIDDEN: [
'M813.472,552.96l-101.344,-281.6c-2.528,-7.68 -12.672,-15.36 -22.784,-15.36l-76,0c-15.2,0 -25.344,10.24 -25.344,25.6c0,15.36 10.144,25.6 25.344,25.6l58.272,0l83.584,230.4l-192.544,0l-101.344,0l-192.512,0l83.616,-230.4l58.272,0c15.2,0 25.344,-10.24 25.344,-25.6c0,-15.36 -10.144,-25.6 -25.344,-25.6l-76,0c-10.144,0 -20.256,7.68 -22.784,17.92l-101.344,281.6c-2.56,0 -2.56,5.12 -2.56,7.68l0,128c0,43.52 32.928,76.8 76,76.8l126.656,0c43.072,0 76,-33.28 76,-76.8l0,-102.4l50.656,0l0,102.4c0,43.52 32.928,76.8 76,76.8l126.656,0c43.072,0 76,-33.28 76,-76.8l0,-128c0.032,-2.56 0.032,-7.68 -2.496,-10.24Z',
],
QRCODE: [
'M832 1024l-64 0l0 -128l64 0l0 128Zm-320 0l-64 0l0 -128l64 0l0 128Zm192 0l-128 0l0 -128l128 0l0 128Zm192 -192l64 0l0 64l64 0l0 128l-128 0l0 -192Zm-896 -192l384 0l0 384l-384 0l0 -384Zm320 320l0 -256l-256 0l0 256l256 0Zm-64 -64l-128 0l0 -128l128 0l0 128Zm512 0l-64 0l0 -64l64 0l0 64Zm-192 -128l0 128l-64 0l0 -64l-64 0l0 -64l128 0Zm128 64l-64 0l0 -64l64 0l0 64Zm192 0l-128 0l0 -64l128 0l0 64Zm-256 -64l-64 0l0 -64l64 0l0 64Zm320 -64l-64 0l0 -64l128 0l0 128l-64 0l0 -64Zm-384 0l-128 0l0 -128l128 0l0 128Zm64 -64l64 0l0 -64l128 0l0 128l-192 0l0 -64Zm-320 -128l64 0l0 -64l64 0l0 128l-128 0l0 -64Zm256 0l-64 0l0 -64l192 0l0 128l-128 0l0 -64Zm-576 -64l128 0l0 64l64 0l0 64l-192 0l0 -128Zm896 64l-128 0l0 -64l256 0l0 128l-128 0l0 -64Zm-576 0l-128 0l0 -64l128 0l0 64Zm192 -64l-64 0l0 -64l64 0l0 64Zm-512 -448l384 0l0 384l-384 0l0 -384Zm576 384l-64 0l0 -128l64 0l0 128Zm64 -384l384 0l0 384l-384 0l0 -384Zm-320 320l0 -256l-256 0l0 256l256 0Zm640 0l0 -256l-256 0l0 256l256 0Zm-704 -64l-128 0l0 -128l128 0l0 128Zm640 0l-128 0l0 -128l128 0l0 128Zm-384 -256l0 64l64 0l0 128l-64 0l0 64l-64 0l0 -256l64 0Z',
],
MENU: [
'M192,265.497l640,0l0,119.906l-640,0l0,-119.906Zm0,186.56l640,0l0,119.946l-640,0l0,-119.946Zm0,186.56l640,0l0,119.886l-640,0l0,-119.886Z',
],
}; };
/* /*

@ -1,3 +1,15 @@
// Bootstrap 3 breakpoints
/* XS - Extra Small Devices, Phones */
/* SM - Small Devices, Tablets */
/* MD - Medium Devices, Desktops */
/* LG - Large Devices, Wide Screens */
export const SCREEN_SIZE = {
XS: '480px',
SM: '768px',
MD: '992px',
LG: '1170px',
};
// OLD UNITS // OLD UNITS
// SMALLEST: '10px', // SMALLEST: '10px',
// SMALLER: '12px', // SMALLER: '12px',
@ -13,7 +25,6 @@
// H3: '14px', // H3: '14px',
// H4: '12px', // H4: '12px',
// COUNTER: '11px', // COUNTER: '11px',
export const FONT_SIZE = { export const FONT_SIZE = {
SMALL: '0.8571rem', SMALL: '0.8571rem',
BASE: '1rem', BASE: '1rem',

@ -0,0 +1,5 @@
export default {
NEXT_WALLET: 'https://beta-wallet.trezor.io/next',
OLD_WALLET: 'https://wallet.trezor.io',
OLD_WALLET_BETA: 'https://beta-wallet.trezor.io',
};

@ -38,6 +38,7 @@ import type {
Device, Device,
Features, Features,
DeviceStatus, DeviceStatus,
FirmwareRelease,
DeviceFirmwareStatus, DeviceFirmwareStatus,
DeviceMode, DeviceMode,
DeviceMessageType, DeviceMessageType,
@ -55,6 +56,7 @@ export type AcquiredDevice = $Exact<{
+label: string, +label: string,
+features: Features, +features: Features,
+firmware: DeviceFirmwareStatus, +firmware: DeviceFirmwareStatus,
+firmwareRelease: ?FirmwareRelease,
status: DeviceStatus, status: DeviceStatus,
+mode: DeviceMode, +mode: DeviceMode,
state: ?string, state: ?string,

@ -27,22 +27,21 @@ declare module 'bignumber.js' {
// Methods // Methods
abs(): T_BigNumber; abs(): T_BigNumber;
cmp(n: $npm$big$number$object): $npm$cmp$result;
div(n: $npm$big$number$object): T_BigNumber; div(n: $npm$big$number$object): T_BigNumber;
dividedBy(n: $npm$big$number$object): T_BigNumber; dividedBy(n: $npm$big$number$object): T_BigNumber;
eq(n: $npm$big$number$object): boolean; eq(n: $npm$big$number$object): boolean;
gt(n: $npm$big$number$object): boolean; gt(n: $npm$big$number$object): boolean;
greaterThan(n: $npm$big$number$object): boolean; isGreaterThan(n: $npm$big$number$object): boolean;
gte(n: $npm$big$number$object): boolean; gte(n: $npm$big$number$object): boolean;
lt(n: $npm$big$number$object): boolean; lt(n: $npm$big$number$object): boolean;
lessThan(n: $npm$big$number$object): boolean; isLessThan(n: $npm$big$number$object): boolean;
lte(n: $npm$big$number$object): boolean; lte(n: $npm$big$number$object): boolean;
lessThanOrEqualTo(n: $npm$big$number$object): boolean; isLessThanOrEqualTo(n: $npm$big$number$object): boolean;
isNaN(): boolean;
minus(n: $npm$big$number$object): T_BigNumber; minus(n: $npm$big$number$object): T_BigNumber;
mod(n: $npm$big$number$object): T_BigNumber; mod(n: $npm$big$number$object): T_BigNumber;
plus(n: $npm$big$number$object): T_BigNumber; plus(n: $npm$big$number$object): T_BigNumber;
pow(exp: number): BigNumber; pow(exp: number): BigNumber;
round(dp: ?number, rm: ?RM): T_BigNumber;
sqrt(): T_BigNumber; sqrt(): T_BigNumber;
times(n: $npm$big$number$object): T_BigNumber; times(n: $npm$big$number$object): T_BigNumber;
toExponential(dp: ?number): string; toExponential(dp: ?number): string;

@ -92,8 +92,8 @@ export default (state: State = initialState, action: Action): State => {
case WALLET.CLEAR_UNAVAILABLE_DEVICE_DATA: case WALLET.CLEAR_UNAVAILABLE_DEVICE_DATA:
return clear(state, action.devices); return clear(state, action.devices);
//case CONNECT.FORGET_SINGLE : //case CONNECT.FORGET_SINGLE :
// return forgetAccounts(state, action); // return forgetAccounts(state, action);
case ACCOUNT.UPDATE: case ACCOUNT.UPDATE:
return updateAccount(state, action.payload); return updateAccount(state, action.payload);

@ -16,6 +16,7 @@ export type BlockchainNetwork = {
feeTimestamp: number, feeTimestamp: number,
feeLevels: Array<BlockchainFeeLevel>, feeLevels: Array<BlockchainFeeLevel>,
connected: boolean, connected: boolean,
connecting: boolean,
block: number, block: number,
}; };
@ -23,6 +24,26 @@ export type State = Array<BlockchainNetwork>;
export const initialState: State = []; export const initialState: State = [];
const onStartSubscribe = (state: State, shortcut: string): State => {
const network = state.find(b => b.shortcut === shortcut);
if (network) {
const others = state.filter(b => b !== network);
return others.concat([{
...network,
connecting: true,
}]);
}
return state.concat([{
shortcut,
connected: false,
connecting: true,
block: 0,
feeTimestamp: 0,
feeLevels: [],
}]);
};
const onConnect = (state: State, action: BlockchainConnect): State => { const onConnect = (state: State, action: BlockchainConnect): State => {
const shortcut = action.payload.coin.shortcut.toLowerCase(); const shortcut = action.payload.coin.shortcut.toLowerCase();
const network = state.find(b => b.shortcut === shortcut); const network = state.find(b => b.shortcut === shortcut);
@ -31,13 +52,16 @@ const onConnect = (state: State, action: BlockchainConnect): State => {
const others = state.filter(b => b !== network); const others = state.filter(b => b !== network);
return others.concat([{ return others.concat([{
...network, ...network,
block: info.block,
connected: true, connected: true,
connecting: false,
}]); }]);
} }
return state.concat([{ return state.concat([{
shortcut, shortcut,
connected: true, connected: true,
connecting: false,
block: info.block, block: info.block,
feeTimestamp: 0, feeTimestamp: 0,
feeLevels: [], feeLevels: [],
@ -52,12 +76,14 @@ const onError = (state: State, action: BlockchainError): State => {
return others.concat([{ return others.concat([{
...network, ...network,
connected: false, connected: false,
connecting: false,
}]); }]);
} }
return state.concat([{ return state.concat([{
shortcut, shortcut,
connected: false, connected: false,
connecting: false,
block: 0, block: 0,
feeTimestamp: 0, feeTimestamp: 0,
feeLevels: [], feeLevels: [],
@ -93,6 +119,8 @@ const updateFee = (state: State, shortcut: string, feeLevels: Array<BlockchainFe
export default (state: State = initialState, action: Action): State => { export default (state: State = initialState, action: Action): State => {
switch (action.type) { switch (action.type) {
case BLOCKCHAIN_ACTION.START_SUBSCRIBE:
return onStartSubscribe(state, action.shortcut);
case BLOCKCHAIN_EVENT.CONNECT: case BLOCKCHAIN_EVENT.CONNECT:
return onConnect(state, action); return onConnect(state, action);
case BLOCKCHAIN_EVENT.ERROR: case BLOCKCHAIN_EVENT.ERROR:

@ -18,7 +18,12 @@ export type State = {
} | { } | {
context: typeof MODAL.CONTEXT_EXTERNAL_WALLET, context: typeof MODAL.CONTEXT_EXTERNAL_WALLET,
windowType?: string; windowType?: string;
} } | {
context: typeof MODAL.CONTEXT_SCAN_QR,
} | {
context: typeof MODAL.CONTEXT_CONFIRMATION,
windowType: string;
};
const initialState: State = { const initialState: State = {
context: MODAL.CONTEXT_NONE, context: MODAL.CONTEXT_NONE,
@ -91,6 +96,17 @@ export default function modal(state: State = initialState, action: Action): Stat
windowType: action.id, windowType: action.id,
}; };
case MODAL.OPEN_SCAN_QR:
return {
context: MODAL.CONTEXT_SCAN_QR,
};
case UI.REQUEST_CONFIRMATION:
return {
context: MODAL.CONTEXT_CONFIRMATION,
windowType: action.payload.view,
};
default: default:
return state; return state;
} }

@ -82,6 +82,7 @@ export default (state: State = initialState, action: Action): State => {
case SEND.INIT: case SEND.INIT:
case SEND.CHANGE: case SEND.CHANGE:
case SEND.VALIDATION: case SEND.VALIDATION:
case SEND.CLEAR:
return action.state; return action.state;
case SEND.TOGGLE_ADVANCED: case SEND.TOGGLE_ADVANCED:

@ -21,12 +21,14 @@ export type State = {
touched: {[k: string]: boolean}; touched: {[k: string]: boolean};
address: string; address: string;
amount: string; amount: string;
minAmount: string;
setMax: boolean; setMax: boolean;
feeLevels: Array<FeeLevel>; feeLevels: Array<FeeLevel>;
selectedFeeLevel: FeeLevel; selectedFeeLevel: FeeLevel;
fee: string; fee: string;
feeNeedsUpdate: boolean; feeNeedsUpdate: boolean;
sequence: string; sequence: string;
destinationTag: string;
total: string; total: string;
errors: {[k: string]: string}; errors: {[k: string]: string};
@ -46,6 +48,7 @@ export const initialState: State = {
touched: {}, touched: {},
address: '', address: '',
amount: '', amount: '',
minAmount: '0',
setMax: false, setMax: false,
feeLevels: [], feeLevels: [],
selectedFeeLevel: { selectedFeeLevel: {
@ -56,6 +59,7 @@ export const initialState: State = {
fee: '0', fee: '0',
feeNeedsUpdate: false, feeNeedsUpdate: false,
sequence: '0', sequence: '0',
destinationTag: '',
total: '0', total: '0',
errors: {}, errors: {},
@ -73,6 +77,7 @@ export default (state: State = initialState, action: Action): State => {
case SEND.INIT: case SEND.INIT:
case SEND.CHANGE: case SEND.CHANGE:
case SEND.VALIDATION: case SEND.VALIDATION:
case SEND.CLEAR:
return action.state; return action.state;
case SEND.TOGGLE_ADVANCED: case SEND.TOGGLE_ADVANCED:

@ -6,6 +6,7 @@ import { DEVICE, TRANSPORT } from 'trezor-connect';
import * as MODAL from 'actions/constants/modal'; import * as MODAL from 'actions/constants/modal';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as ACCOUNT from 'actions/constants/account';
import type { Action, RouterLocationState, TrezorDevice } from 'flowtype'; import type { Action, RouterLocationState, TrezorDevice } from 'flowtype';
@ -14,6 +15,7 @@ type State = {
online: boolean; online: boolean;
dropdownOpened: boolean; dropdownOpened: boolean;
showBetaDisclaimer: boolean; showBetaDisclaimer: boolean;
showSidebar: ?boolean;
initialParams: ?RouterLocationState; initialParams: ?RouterLocationState;
initialPathname: ?string; initialPathname: ?string;
firstLocationChange: boolean; firstLocationChange: boolean;
@ -27,6 +29,7 @@ const initialState: State = {
dropdownOpened: false, dropdownOpened: false,
firstLocationChange: true, firstLocationChange: true,
showBetaDisclaimer: false, showBetaDisclaimer: false,
showSidebar: null,
initialParams: null, initialParams: null,
initialPathname: null, initialPathname: null,
disconnectRequest: null, disconnectRequest: null,
@ -71,6 +74,11 @@ export default function wallet(state: State = initialState, action: Action): Sta
...state, ...state,
dropdownOpened: false, dropdownOpened: false,
}; };
case ACCOUNT.UPDATE_SELECTED_ACCOUNT:
return {
...state,
showSidebar: false,
};
case CONNECT.DISCONNECT_REQUEST: case CONNECT.DISCONNECT_REQUEST:
return { return {
@ -94,6 +102,12 @@ export default function wallet(state: State = initialState, action: Action): Sta
selectedDevice: action.device, selectedDevice: action.device,
}; };
case WALLET.TOGGLE_SIDEBAR:
return {
...state,
showSidebar: !state.showSidebar,
};
case WALLET.SHOW_BETA_DISCLAIMER: case WALLET.SHOW_BETA_DISCLAIMER:
return { return {
...state, ...state,

@ -95,6 +95,7 @@ export const getPendingSequence = (pending: Array<Transaction>): number => pendi
}, 0); }, 0);
export const getPendingAmount = (pending: Array<Transaction>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: Transaction): BigNumber => { export const getPendingAmount = (pending: Array<Transaction>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: Transaction): BigNumber => {
if (tx.type !== 'send') return value;
if (!token) { if (!token) {
// regular transactions // regular transactions
// add fees from token txs and amount from regular txs // add fees from token txs and amount from regular txs

@ -12,6 +12,11 @@ export const routes: Array<Route> = [
pattern: '/', pattern: '/',
fields: [], fields: [],
}, },
{
name: 'landing-version',
pattern: '/version',
fields: ['version'],
},
{ {
name: 'landing-bridge', name: 'landing-bridge',
pattern: '/bridge', pattern: '/bridge',
@ -57,6 +62,11 @@ export const routes: Array<Route> = [
pattern: '/device/:device/firmware-update', pattern: '/device/:device/firmware-update',
fields: ['device', 'firmware-update'], fields: ['device', 'firmware-update'],
}, },
{
name: 'wallet-backup',
pattern: '/device/:device/backup',
fields: ['device', 'backup'],
},
{ {
name: 'wallet-device-settings', name: 'wallet-device-settings',
pattern: '/device/:device/settings', pattern: '/device/:device/settings',

@ -8,7 +8,7 @@ import animationStyles from './Animations';
const baseStyles = createGlobalStyle` const baseStyles = createGlobalStyle`
html, body { html, body {
width: 100%; width: 100%;
height: 100%; min-height: 100vh;
position: relative; position: relative;
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
font-weight: ${FONT_WEIGHT.NORMAL}; font-weight: ${FONT_WEIGHT.NORMAL};
@ -43,7 +43,7 @@ const baseStyles = createGlobalStyle`
} }
#trezor-wallet-root { #trezor-wallet-root {
height: 100%; min-height: 100vh;
} }
${animationStyles}; ${animationStyles};

@ -1,67 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`device utils get status 1`] = `"disconnected"`;
exports[`device utils get status 2`] = `"unavailable"`;
exports[`device utils get status 3`] = `"unavailable"`;
exports[`device utils get status 4`] = `"connected"`;
exports[`device utils get status 5`] = `"unacquired"`;
exports[`device utils get status 6`] = `"used-in-other-window"`;
exports[`device utils get status color 1`] = `"#494949"`;
exports[`device utils get status color 2`] = `"#494949"`;
exports[`device utils get status color 3`] = `"#494949"`;
exports[`device utils get status color 4`] = `"#EB8A00"`;
exports[`device utils get status color 5`] = `"#01B757"`;
exports[`device utils get status color 6`] = `"#EB8A00"`;
exports[`device utils get status color 7`] = `"#ED1212"`;
exports[`device utils get status color 8`] = `"#ED1212"`;
exports[`device utils get status name 1`] = `"Status unknown"`;
exports[`device utils get status name 2`] = `"Status unknown"`;
exports[`device utils get status name 3`] = `"Status unknown"`;
exports[`device utils get status name 4`] = `"Used in other window"`;
exports[`device utils get status name 5`] = `"Connected"`;
exports[`device utils get status name 6`] = `"Used in other window"`;
exports[`device utils get status name 7`] = `"Disconnected"`;
exports[`device utils get status name 8`] = `"Unavailable"`;
exports[`device utils get version 1`] = `"1"`;
exports[`device utils get version 2`] = `"1"`;
exports[`device utils get version 3`] = `"1"`;
exports[`device utils get version 4`] = `"1"`;
exports[`device utils get version 5`] = `"1"`;
exports[`device utils get version 6`] = `"T"`;
exports[`device utils isDisabled 1`] = `false`;
exports[`device utils isDisabled 2`] = `true`;
exports[`device utils isWebUSB 1`] = `true`;
exports[`device utils isWebUSB 2`] = `false`;
exports[`device utils isWebUSB 3`] = `true`;

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

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

@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`device utils get icon 1`] = `
Array [
"M693.024 330.944c-99.968-99.936-262.080-99.936-362.048 0s-99.968 262.112 0 362.080c99.968 100 262.144 99.936 362.048 0 99.968-99.904 99.968-262.176 0-362.080zM507.904 300.192c27.008 0 48.992 21.984 48.992 49.088 0 27.296-21.984 49.472-48.992 49.472-27.264 0-49.536-22.176-49.536-49.472 0-27.552 21.728-49.088 49.536-49.088zM586.656 660.8c0 10.304-4.96 15.328-15.264 15.328h-126.464c-10.304 0-15.328-5.024-15.328-15.328v-32.256c0-10.304 5.024-15.264 15.328-15.264h23.36v-136.064h-23.872c-10.304 0-15.264-5.024-15.264-15.328v-32.224c0-10.304 4.96-15.264 15.264-15.264h88.288c10.304 0 15.264 4.96 15.264 15.264v183.648h23.424c10.304 0 15.264 4.96 15.264 15.264v32.224z",
]
`;
exports[`device utils get icon 2`] = `
Array [
"M693.12 330.88c-46.317-46.267-110.276-74.88-180.919-74.88-141.385 0-256 114.615-256 256s114.615 256 256 256c70.642 0 134.602-28.613 180.921-74.882l-0.002 0.002c46.387-46.337 75.081-110.377 75.081-181.12s-28.694-134.783-75.079-181.118l-0.002-0.002zM494.080 344.32h53.12c16 0 18.24 9.28 18.24 14.72v10.24l-10.88 194.56c0 14.4-8 17.28-18.88 17.28h-28.16c-10.56 0-17.28-2.88-18.88-17.92l-10.88-193.92v-10.56c-1.28-4.8 2.24-14.080 16.32-14.080zM521.28 717.76c-0.095 0.001-0.207 0.001-0.319 0.001-27.747 0-50.24-22.493-50.24-50.24s22.493-50.24 50.24-50.24c27.747 0 50.24 22.493 50.24 50.24 0 0.112 0 0.224-0.001 0.336v-0.017c0 0 0 0.001 0 0.001 0 27.634-22.311 50.057-49.903 50.239h-0.017z",
]
`;
exports[`device utils get icon 3`] = `
Array [
"M795.616 735.008l-264.896-465.44c-10.272-18.080-27.168-18.080-37.504 0l-264.864 465.44c-10.272 18.176-1.696 32.992 19.040 32.992h529.184c20.8 0 29.376-14.816 19.040-32.992zM549.76 673.12c0 10.464-8.48 18.976-18.912 18.976h-37.792c-10.336 0-18.912-8.512-18.912-18.976v-37.952c0-10.464 8.576-18.976 18.912-18.976h37.792c10.4 0 18.912 8.544 18.912 18.976v37.952zM549.76 559.264c0 10.464-8.48 18.976-18.912 18.976h-37.792c-10.336 0-18.912-8.512-18.912-18.976v-113.856c0-10.464 8.576-18.976 18.912-18.976h37.792c10.4 0 18.912 8.544 18.912 18.976v113.856z",
]
`;
exports[`device utils get icon 4`] = `
Array [
"M692.8 313.92l-1.92-1.92c-6.246-7.057-15.326-11.484-25.44-11.484s-19.194 4.427-25.409 11.448l-0.031 0.036-196.48 224-3.84 1.6-3.84-1.92-48.64-57.28c-7.010-7.905-17.193-12.862-28.533-12.862-21.031 0-38.080 17.049-38.080 38.080 0 7.495 2.165 14.485 5.905 20.377l-0.092-0.155 100.8 148.16c5.391 8.036 14.386 13.292 24.618 13.44h8.662c17.251-0.146 32.385-9.075 41.163-22.529l0.117-0.191 195.2-296.32c4.473-6.632 7.141-14.803 7.141-23.597 0-11.162-4.297-21.32-11.326-28.911l0.025 0.028z",
]
`;
exports[`device utils get icon 5`] = `undefined`;
exports[`device utils get icon 6`] = `undefined`;
exports[`device utils get status 1`] = `"#1E7FF0"`;
exports[`device utils get status 2`] = `"#ED1212"`;
exports[`device utils get status 3`] = `"#EB8A00"`;
exports[`device utils get status 4`] = `"#01B757"`;
exports[`device utils get status 5`] = `null`;
exports[`device utils get status 6`] = `null`;

@ -1,112 +1,87 @@
import * as dUtils from 'utils/device'; import * as utils from 'utils/device';
describe('device utils', () => { describe('device utils', () => {
it('get status', () => { it('get status', () => {
const deviceMock = [ expect(utils.getStatus({ connected: false }))
{ .toBe('disconnected');
connected: false,
}, expect(utils.getStatus({ connected: true, available: false }))
{ .toBe('unavailable');
connected: true,
available: false, expect(utils.getStatus({
}, connected: true,
{ available: false,
connected: true, type: null,
available: false, })).toBe('unavailable');
type: null,
},
{
connected: true,
available: true,
type: 'acquired',
},
{
connected: true,
available: true,
type: 'unacquired',
},
{
connected: true,
available: true,
type: 'acquired',
status: 'occupied',
},
];
deviceMock.forEach((device) => { expect(utils.getStatus({
expect(dUtils.getStatus(device)).toMatchSnapshot(); connected: true,
}); available: true,
type: 'acquired',
})).toBe('connected');
expect(utils.getStatus({
connected: true,
available: true,
type: 'unacquired',
})).toBe('unacquired');
expect(utils.getStatus({
connected: true,
available: true,
type: 'acquired',
status: 'occupied',
})).toBe('used-in-other-window');
}); });
it('isWebUSB', () => { it('isWebUSB', () => {
const data = [ expect(utils.isWebUSB({ type: 'webusb', version: '1.6.0' })).toBe(true);
{ transport: { type: 'ParallelTransport', version: 'webusb' } }, expect(utils.isWebUSB({ type: 'aaaa', version: 'aaaaaa' })).toBe(false);
{ transport: { type: null, version: 'aaaaaa' } }, expect(utils.isWebUSB({ type: 'webusb' })).toBe(true);
{ transport: { type: 'ParallelTransport', version: 'webusb' } },
];
data.forEach((item) => {
expect(dUtils.isWebUSB(item.transport)).toMatchSnapshot();
});
}); });
it('isDisabled', () => { it('isDisabled', () => {
const data = [ expect(utils.isDisabled(
{ selectedDevice: { features: null }, devices: [1, 2, 3], transport: { version: 'webusb' } }, { selectedDevice: { features: null } },
{ selectedDevice: { features: null }, devices: [], transport: { version: 'test' } }, [1, 2, 3],
]; {
version: 'webusb',
},
)).toBe(false);
data.forEach((item) => { expect(utils.isDisabled(
expect(dUtils.isDisabled(item.selectedDevice, item.devices, item.transport)).toMatchSnapshot(); { features: null }, [], { version: 'test' },
}); )).toBe(true);
}); });
it('get version', () => { it('get version', () => {
const deviceMock = [ expect(utils.getVersion({})).toBe('One');
{ }, expect(utils.getVersion({ features: {} })).toBe('One');
{ features: {} }, expect(utils.getVersion({ features: { major_version: null } })).toBe('One');
{ features: { major_version: null } }, expect(utils.getVersion({ features: { major_version: 0 } })).toBe('One');
{ features: { major_version: 0 } }, expect(utils.getVersion({ features: { major_version: 1 } })).toBe('One');
{ features: { major_version: 1 } }, expect(utils.getVersion({ features: { major_version: 2 } })).toBe('T');
{ features: { major_version: 2 } },
];
deviceMock.forEach((device) => {
expect(dUtils.getVersion(device)).toMatchSnapshot();
});
}); });
it('get status color', () => { it('get status color', () => {
const entry = [ expect(utils.getStatusColor(0)).toBe('#494949');
0, expect(utils.getStatusColor(null)).toBe('#494949');
null, expect(utils.getStatusColor('sdsdsdsd')).toBe('#494949');
'sdsdsdsd', expect(utils.getStatusColor('used-in-other-window')).toBe('#EB8A00');
'used-in-other-window', expect(utils.getStatusColor('connected')).toBe('#01B757');
'connected', expect(utils.getStatusColor('unacquired')).toBe('#EB8A00');
'unacquired', expect(utils.getStatusColor('disconnected')).toBe('#ED1212');
'disconnected', expect(utils.getStatusColor('unavailable')).toBe('#ED1212');
'unavailable',
];
entry.forEach((status) => {
expect(dUtils.getStatusColor(status)).toMatchSnapshot();
});
}); });
it('get status name', () => { it('get status name', () => {
const entry = [ expect(utils.getStatusName(0)).toBe('Status unknown');
0, expect(utils.getStatusName(null)).toBe('Status unknown');
null, expect(utils.getStatusName('sdsdsdsd')).toBe('Status unknown');
'sdsdsdsd', expect(utils.getStatusName('used-in-other-window')).toBe('Used in other window');
'used-in-other-window', expect(utils.getStatusName('connected')).toBe('Connected');
'connected', expect(utils.getStatusName('unacquired')).toBe('Used in other window');
'unacquired', expect(utils.getStatusName('disconnected')).toBe('Disconnected');
'disconnected', expect(utils.getStatusName('unavailable')).toBe('Unavailable');
'unavailable',
];
entry.forEach((status) => {
expect(dUtils.getStatusName(status)).toMatchSnapshot();
});
}); });
}); });

@ -1,53 +1,44 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as ethUtils from '../ethUtils'; import * as utils from '../ethUtils';
describe('eth utils', () => { describe('eth utils', () => {
it('decimalToHex', () => { it('decimalToHex', () => {
const input = [0, 1, 2, 100, 9999999999]; expect(utils.decimalToHex(0)).toBe('0');
expect(utils.decimalToHex(1)).toBe('1');
input.forEach((entry) => { expect(utils.decimalToHex(2)).toBe('2');
expect(ethUtils.decimalToHex(entry)).toMatchSnapshot(); expect(utils.decimalToHex(100)).toBe('64');
}); expect(utils.decimalToHex(9999999999)).toBe('2540be3ff');
}); });
// TODO: decimal as string ?????
it('hexToDecimal', () => { it('hexToDecimal', () => {
const input = ['2540be3ff', '64', '2', '1', '0', '']; expect(utils.hexToDecimal('2540be3ff')).toBe('9999999999');
expect(utils.hexToDecimal(64)).toBe('100');
input.forEach((entry) => { expect(utils.hexToDecimal(2)).toBe('2');
expect(ethUtils.hexToDecimal(entry)).toMatchSnapshot(); expect(utils.hexToDecimal(1)).toBe('1');
}); expect(utils.hexToDecimal(0)).toBe('0');
}); });
it('padLeftEven', () => { it('padLeftEven', () => {
const input = ['2540be3ff']; expect(utils.padLeftEven('2540be3ff')).toBe('02540be3ff');
input.forEach((entry) => {
expect(ethUtils.padLeftEven(entry)).toMatchSnapshot();
});
}); });
it('sanitizeHex', () => { it('sanitizeHex', () => {
const input = ['0x2540be3ff', '1', '2', '100', '999', '']; expect(utils.sanitizeHex('0x2540be3ff')).toBe('0x02540be3ff');
expect(utils.sanitizeHex('1')).toBe('0x01');
input.forEach((entry) => { expect(utils.sanitizeHex('2')).toBe('0x02');
expect(ethUtils.sanitizeHex(entry)).toMatchSnapshot(); expect(utils.sanitizeHex('100')).toBe('0x0100');
}); expect(utils.sanitizeHex('999')).toBe('0x0999');
expect(utils.sanitizeHex('')).toBe('');
}); });
it('strip', () => { it('strip', () => {
const input = ['0x', '0x2540be3ff', '2540be3ff']; expect(utils.strip('0x')).toBe('');
expect(utils.strip('0x2540be3ff')).toBe('02540be3ff');
input.forEach((entry) => { expect(utils.strip('2540be3ff')).toBe('02540be3ff');
expect(ethUtils.strip(entry)).toMatchSnapshot();
});
}); });
it('calcGasPrice', () => { it('calculate gas price', () => {
const input = [{ price: new BigNumber(9898998989), limit: '9' }]; expect(utils.calcGasPrice(new BigNumber(9898998989), 9)).toBe('89090990901');
input.forEach((entry) => {
expect(ethUtils.calcGasPrice(entry.price, entry.limit)).toMatchSnapshot();
});
}); });
}); });

@ -1,49 +1,41 @@
import * as formatUtils from '../formatUtils'; import * as utils from '../formatUtils';
describe('format utils', () => { describe('format utils', () => {
// TODO: check this weird function
it('formatAmount', () => { it('formatAmount', () => {
const input = [ expect(utils.formatAmount(0, { isBitcoin: false, shortcut: 'mbtc' }, 'mbtc')).toBe('0 mbtc');
{ amount: 0, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } }, expect(utils.formatAmount(1000000, { isBitcoin: true }, 'mbtc')).toBe('10 mBTC');
{ amount: 1000000, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } }, expect(utils.formatAmount(0.5, { isBitcoin: true }, 'mbtc')).toBe('0.000005 mBTC');
{ amount: 0.5, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } }, expect(utils.formatAmount(1, { isBitcoin: false, shortcut: 'eth' }, null)).toBe('1e-8 eth');
{ amount: 1, coinInfo: { isBitcoin: false, shortcut: 'eth' } }, expect(utils.formatAmount(99999, { isBitcoin: false, shortcut: 'tau' }, null)).toBe('0.00099999 tau');
{ amount: 99999, coinInfo: { isBitcoin: false, shortcut: 'tau' } },
];
input.forEach((entry) => {
expect(formatUtils.formatAmount(entry.amount, entry.coinInfo, entry.coinInfo.currencyUnits)).toMatchSnapshot();
});
}); });
it('formatTime', () => { it('format time', () => {
const input = [0, 1, 2, 100, 999, 45]; expect(utils.formatTime(0)).toBe('No time estimate');
expect(utils.formatTime(1)).toBe('1 minutes'); // TODO: should be minute
input.forEach((entry) => { expect(utils.formatTime(2)).toBe('2 minutes');
expect(formatUtils.formatTime(entry)).toMatchSnapshot(); expect(utils.formatTime(45)).toBe('45 minutes');
}); expect(utils.formatTime(100)).toBe('1 hour 40 minutes');
expect(utils.formatTime(999)).toBe('16 hours 39 minutes');
}); });
it('btckb2satoshib', () => { it('btckb2satoshib', () => {
const input = [0, 1, 2, 100, 999]; expect(utils.btckb2satoshib(0)).toBe(0);
expect(utils.btckb2satoshib(1)).toBe(100000);
input.forEach((entry) => { expect(utils.btckb2satoshib(2)).toBe(200000);
expect(formatUtils.btckb2satoshib(entry)).toMatchSnapshot(); expect(utils.btckb2satoshib(100)).toBe(10000000);
}); expect(utils.btckb2satoshib(999)).toBe(99900000);
}); });
it('stringToHex', () => { it('string to hex', () => {
const input = ['test', '0001', 'test99999']; expect(utils.stringToHex('test')).toBe('0074006500730074');
expect(utils.stringToHex('0001')).toBe('0030003000300031');
input.forEach((entry) => { expect(utils.stringToHex('test99999')).toBe('007400650073007400390039003900390039');
expect(formatUtils.stringToHex(entry)).toMatchSnapshot();
});
}); });
it('hexToString', () => { it('hex to string', () => {
const input = ['0074006500730074', '0030003000300031', '007400650073007400390039003900390039']; expect(utils.hexToString('0074006500730074')).toBe('test');
expect(utils.hexToString('0030003000300031')).toBe('0001');
input.forEach((entry) => { expect(utils.hexToString('007400650073007400390039003900390039')).toBe('test99999');
expect(formatUtils.hexToString(entry)).toMatchSnapshot();
});
}); });
}); });

@ -1,32 +1,12 @@
import * as nUtils from 'utils/notification'; import * as utils from 'utils/notification';
describe('device utils', () => { describe('notification utils', () => {
it('get status', () => { it('get colors from status', () => {
const types = [ expect(utils.getPrimaryColor('info')).toBe('#1E7FF0');
'info', expect(utils.getPrimaryColor('warning')).toBe('#EB8A00');
'error', expect(utils.getPrimaryColor('error')).toBe('#ED1212');
'warning', expect(utils.getPrimaryColor('success')).toBe('#01B757');
'success', expect(utils.getPrimaryColor('kdsjflds')).toBe(null);
'kdsjflds', expect(utils.getPrimaryColor('')).toBe(null);
'',
];
types.forEach((type) => {
expect(nUtils.getPrimaryColor(type)).toMatchSnapshot();
});
});
it('get icon', () => {
const types = [
'info',
'error',
'warning',
'success',
'kdsjflds',
'',
];
types.forEach((type) => {
expect(nUtils.getIcon(type)).toMatchSnapshot();
});
}); });
}); });

@ -0,0 +1,39 @@
/* eslint-disable no-param-reassign */
/* eslint-disable prefer-destructuring */
/* @flow */
// copy paste from mytrezor (old wallet) https://github.com/satoshilabs/mytrezor/blob/87f8a8d9ca82a27b3941c5ec0f399079903f2bfd/app/components/address-input/address-input.js
export type parsedURI = {
address: string,
amount: ?string,
};
// Parse a string read from a bitcoin QR code into an object
export const parseUri = (uri: string): ?parsedURI => {
const str = stripPrefix(uri);
const query: Array<string> = str.split('?');
const values: Object = (query.length > 1) ? parseQuery(query[1]) : {};
values.address = query[0];
return values;
};
const stripPrefix = (str: string): string => {
if (!str.match(':')) {
return str;
}
const parts = str.split(':');
parts.shift();
return parts.join('');
};
// Parse URL query string (like 'foo=bar&baz=1337) into an object
const parseQuery = (str: string): {} => str.split('&')
.map(val => val.split('='))
.reduce((vals, pair) => {
if (pair.length > 1) {
vals[pair[0]] = pair[1];
}
return vals;
}, {});

@ -79,7 +79,7 @@ export const getStatusName = (deviceStatus: string): string => {
} }
}; };
export const isWebUSB = (transport: Transport) => !!((transport.type && transport.version.indexOf('webusb') >= 0)); export const isWebUSB = (transport: Transport) => !!((transport.type && transport.type === 'webusb'));
export const isDisabled = (selectedDevice: TrezorDevice, devices: Array<TrezorDevice>, transport: Transport) => { export const isDisabled = (selectedDevice: TrezorDevice, devices: Array<TrezorDevice>, transport: Transport) => {
if (isWebUSB(transport)) return false; // always enabled if webusb if (isWebUSB(transport)) return false; // always enabled if webusb
@ -104,7 +104,7 @@ export const getVersion = (device: TrezorDevice): string => {
if (device.features && device.features.major_version > 1) { if (device.features && device.features.major_version > 1) {
version = 'T'; version = 'T';
} else { } else {
version = '1'; version = 'One';
} }
return version; return version;
}; };

@ -56,7 +56,12 @@ export const hexToString = (hex: string): string => {
export const toDecimalAmount = (amount: string | number, decimals: number): string => { export const toDecimalAmount = (amount: string | number, decimals: number): string => {
try { try {
return new BigNumber(amount).div(10 ** decimals).toString(10); const bAmount = new BigNumber(amount);
// BigNumber() returns NaN on non-numeric string
if (bAmount.isNaN()) {
throw new Error('Amount is not a number');
}
return bAmount.div(10 ** decimals).toString(10);
} catch (error) { } catch (error) {
return '0'; return '0';
} }
@ -64,7 +69,12 @@ export const toDecimalAmount = (amount: string | number, decimals: number): stri
export const fromDecimalAmount = (amount: string | number, decimals: number): string => { export const fromDecimalAmount = (amount: string | number, decimals: number): string => {
try { try {
return new BigNumber(amount).times(10 ** decimals).toString(10); const bAmount = new BigNumber(amount);
// BigNumber() returns NaN on non-numeric string
if (bAmount.isNaN()) {
throw new Error('Amount is not a number');
}
return bAmount.times(10 ** decimals).toString(10);
} catch (error) { } catch (error) {
return '0'; return '0';
} }

@ -0,0 +1,24 @@
/* @flow */
import urlConstants from 'constants/urls';
import type { TrezorDevice } from 'flowtype';
const getOldWalletUrl = (device: ?TrezorDevice): string => {
if (!device || !device.firmwareRelease) return urlConstants.OLD_WALLET_BETA;
const release = device.firmwareRelease;
const url = release.channel === 'beta' ? urlConstants.OLD_WALLET_BETA : urlConstants.OLD_WALLET;
return url;
};
// TODO: use uri template to build urls
const getOldWalletReleaseUrl = (device: ?TrezorDevice): string => {
if (!device || !device.firmwareRelease) return urlConstants.OLD_WALLET_BETA;
const release = device.firmwareRelease;
const url = getOldWalletUrl(device);
const version = release.version.join('.');
return `${url}?fw=${version}`;
};
export {
getOldWalletUrl,
getOldWalletReleaseUrl,
};

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

Loading…
Cancel
Save