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

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

4
.gitignore vendored

@ -19,4 +19,6 @@ logs
_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:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- node_modules
- ${CYPRESS_CACHE_FOLDER}
stages:
- test
- build
- deploy
- integration tests
lint:
stage: test
@ -62,7 +67,23 @@ build stable:
expire_in: 1 week
paths:
- 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:
stage: deploy
@ -138,3 +159,24 @@ delete review:
- branches
tags:
- 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
__added__
- 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
You can try this wallet live [HERE](https://beta-wallet.trezor.io/next/)
To install dependencies run `npm install` or `yarn`
To start locally run `npm run dev` or `yarn run dev`
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
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: [
'./support/setupJest.js',
],
transform: {
'^.+\\.jsx?$': 'babel-jest',
},
};

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

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

@ -1,3 +1,4 @@
/* eslint-disable import/no-named-as-default-member */
/* @flow */
import TrezorConnect, { UI } from 'trezor-connect';
@ -9,6 +10,10 @@ import type {
ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice,
} from 'flowtype';
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 = {
type: typeof MODAL.CLOSE
@ -16,8 +21,11 @@ export type ModalAction = {
type: typeof MODAL.OPEN_EXTERNAL_WALLET,
id: string,
url: string,
} | {
type: typeof MODAL.OPEN_SCAN_QR,
};
export const onPinSubmit = (value: string): Action => {
TrezorConnect.uiResponse({ type: UI.RECEIVE_PIN, payload: value });
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 => ({
type: CONNECT.REMEMBER,
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 {
onPinSubmit,
onPassphraseSubmit,
onReceiveConfirmation,
onRememberDevice,
onForgetDevice,
onForgetSingleDevice,
@ -149,4 +192,6 @@ export default {
onDuplicateDevice,
onWalletTypeRequest,
gotoExternalWallet,
openQrModal,
onQrScan,
};

@ -94,6 +94,10 @@ export const showAddress = (path: Array<number>): AsyncAction => async (dispatch
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({
type: NOTIFICATION.ADD,
payload: {

@ -347,6 +347,17 @@ export const gotoFirmwareUpdate = (): ThunkAction => (dispatch: Dispatch, getSta
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
*/

@ -8,6 +8,7 @@ import * as TOKEN from 'actions/constants/token';
import * as PENDING from 'actions/constants/pendingTx';
import * as reducerUtils from 'reducers/utils';
import { getVersion } from 'utils/device';
import { initialState } from 'reducers/SelectedAccountReducer';
import type {
@ -51,10 +52,11 @@ const getExceptionPage = (state: State, selectedAccount: SelectedAccountState):
shortcut: 'not-used',
};
}
if (discovery.fwNotSupported) {
return {
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.',
shortcut: network.shortcut,
};

@ -18,11 +18,11 @@ import * as EthereumSendFormActions from './ethereum/SendFormActions';
import * as RippleSendFormActions from './ripple/SendFormActions';
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',
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',
state: RippleState,
} | {

@ -3,6 +3,7 @@ import TrezorConnect, {
DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT,
} from 'trezor-connect';
import { CONTEXT_NONE } from 'actions/constants/modal';
import urlConstants from 'constants/urls';
import * as CONNECT from 'actions/constants/TrezorConnect';
import * as NOTIFICATION from 'actions/constants/notification';
import { getDuplicateInstanceNumber } from 'reducers/utils';
@ -120,7 +121,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
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://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;
}
@ -131,6 +132,10 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
popup: false,
webusb: true,
pendingTransportEvent: (getState().devices.length < 1),
manifest: {
email: 'info@trezor.io',
appUrl: urlConstants.NEXT_WALLET,
},
});
} catch (error) {
dispatch({

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

@ -1,4 +1,5 @@
/* @flow */
export const START_SUBSCRIBE: 'blockchain__start_subscribe' = 'blockchain__start_subscribe';
export const READY: 'blockchain__ready' = 'blockchain__ready';
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_DEVICE: 'modal_ctx_device' = 'modal_ctx_device';
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_ERROR: 'send__tx_error' = 'send__tx_error';
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 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',
});
/*
* 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
*/
@ -613,4 +648,5 @@ export default {
onNonceChange,
onDataChange,
onSend,
onClear,
};

@ -115,7 +115,7 @@ export const recalculateTotalAmount = ($state: State): PayloadAction<State> => (
if (isToken) {
const token = findToken(tokens, account.descriptor, state.currency, account.deviceState);
if (token) {
state.amount = new BigNumber(token.balance).minus(pendingAmount).toString(10);
state.amount = new BigNumber(token.balance).minus(pendingAmount).toFixed();
}
} else {
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)) {
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`;
} 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';
} else if (new BigNumber(state.amount).lessThanOrEqualTo('0')) {
} else if (new BigNumber(state.amount).isLessThanOrEqualTo('0')) {
state.errors.amount = 'Amount is too low';
}
} else if (!state.amount.match(ETH_18_RE)) {
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';
}
}
@ -261,9 +261,9 @@ export const gasLimitValidation = ($state: State): PayloadAction<State> => (disp
state.errors.gasLimit = 'Gas limit is not a number';
} else {
const gl: BigNumber = new BigNumber(gasLimit);
if (gl.lessThan(1)) {
if (gl.isLessThan(1)) {
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';
}
}
@ -284,9 +284,9 @@ export const gasPriceValidation = ($state: State): PayloadAction<State> => (): S
state.errors.gasPrice = 'Gas price is not a number';
} else {
const gp: BigNumber = new BigNumber(gasPrice);
if (gp.greaterThan(1000)) {
if (gp.isGreaterThan(1000)) {
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';
}
}
@ -312,9 +312,9 @@ export const nonceValidation = ($state: State): PayloadAction<State> => (dispatc
state.errors.nonce = 'Nonce is not a valid number';
} else {
const n: BigNumber = new BigNumber(nonce);
if (n.lessThan(account.nonce)) {
if (n.isLessThan(account.nonce)) {
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';
}
}
@ -339,7 +339,7 @@ export const dataValidation = ($state: State): PayloadAction<State> => (): State
export const calculateFee = (gasPrice: string, gasLimit: string): string => {
try {
return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether');
return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit).toFixed(), 'gwei', 'ether');
} catch (error) {
return '0';
}
@ -347,7 +347,12 @@ export const calculateFee = (gasPrice: string, gasLimit: string): string => {
export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => {
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) {
return '0';
}
@ -358,8 +363,8 @@ export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimi
// TODO - minus pendings
const fee = calculateFee(gasPrice, gasLimit);
const max = balance.minus(fee);
if (max.lessThan(0)) return '0';
return max.toString(10);
if (max.isLessThan(0)) return '0';
return max.toFixed();
} catch (error) {
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> => {
const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice;
const quarter: BigNumber = price.dividedBy(4);
const high: string = price.plus(quarter.times(2)).toString(10);
const low: string = price.minus(quarter.times(2)).toString(10);
const high: string = price.plus(quarter.times(2)).toFixed();
const low: string = price.minus(quarter.times(2)).toFixed();
const customLevel: FeeLevel = selected && selected.value === 'Custom' ? {
value: 'Custom',
@ -391,7 +396,7 @@ export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLi
{
value: 'Normal',
gasPrice: gasPrice.toString(),
label: `${calculateFee(price.toString(10), gasLimit)} ${symbol}`,
label: `${calculateFee(price.toFixed(), gasLimit)} ${symbol}`,
},
{
value: 'Low',

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

@ -76,6 +76,6 @@ export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discover
networkType: 'ripple',
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) {
const validated = dispatch(ValidationActions.validation());
const validated = dispatch(ValidationActions.validation(prevState.sendFormRipple));
dispatch({
type: SEND.VALIDATION,
networkType: 'ripple',
@ -119,6 +119,38 @@ export const toggleAdvanced = (): Action => ({
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
*/
@ -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
*/
@ -262,7 +311,13 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
if (!blockchain) return;
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({
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
flags: 0x80000000,
sequence: account.sequence,
payment: {
amount,
destination: currentState.address,
},
payment,
},
});
@ -346,5 +398,7 @@ export default {
onFeeLevelChange,
updateFeeLevels,
onFeeChange,
onDestinationTagChange,
onSend,
onClear,
};

@ -1,5 +1,5 @@
/* @flow */
import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js';
import * as SEND from 'actions/constants/send';
import { findDevice, getPendingAmount } from 'reducers/utils';
@ -9,6 +9,7 @@ import type {
Dispatch,
GetState,
PayloadAction,
PromiseAction,
BlockchainFeeLevel,
} from 'flowtype';
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
@ -59,7 +60,7 @@ export const onFeeUpdated = (network: string, feeLevels: Array<BlockchainFeeLeve
/*
* 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
// to avoid overrides across state history
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(amountValidation(state));
state = dispatch(feeValidation(state));
state = dispatch(destinationTagValidation(state));
if (state.touched.address && prevState.address !== state.address) {
dispatch(addressBalanceValidation(state));
}
return state;
};
@ -82,14 +87,14 @@ const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatc
network,
pending,
} = getState().selectedAccount;
if (!account || !network) return $state;
if (!account || account.networkType !== 'ripple' || !network) return $state;
const state = { ...$state };
const fee = toDecimalAmount(state.selectedFeeLevel.fee, network.decimals);
if (state.setMax) {
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);
}
@ -134,6 +139,43 @@ const addressValidation = ($state: State): PayloadAction<State> => (dispatch: Di
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
*/
@ -183,7 +225,7 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
account,
pending,
} = getState().selectedAccount;
if (!account) return state;
if (!account || account.networkType !== 'ripple') return state;
const { amount } = state;
if (amount.length < 1) {
@ -192,13 +234,21 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
state.errors.amount = 'Amount is not a number';
} else {
const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol);
if (!state.amount.match(XRP_6_RE)) {
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';
}
}
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;
};
@ -221,14 +271,27 @@ export const feeValidation = ($state: State): PayloadAction<State> => (dispatch:
state.errors.fee = 'Fee must be an absolute number';
} else {
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';
} else if (gl.greaterThan(network.fee.maxFee)) {
} else if (gl.isGreaterThan(network.fee.maxFee)) {
state.errors.fee = 'Fee is above recommended';
}
}
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 => {
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) {
return '0';
}
@ -247,8 +315,8 @@ const calculateMaxAmount = (balance: BigNumber, fee: string): string => {
try {
// TODO - minus pendings
const max = balance.minus(fee);
if (max.lessThan(0)) return '0';
return max.toString(10);
if (max.isLessThan(0)) return '0';
return max.toFixed();
} catch (error) {
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,
isWebUsb?: boolean,
isTransparent?: boolean,
dataTest?: string
}
const Wrapper = styled.button`
@ -37,6 +38,11 @@ const Wrapper = styled.button`
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`
pointer-events: none;
color: ${colors.TEXT_SECONDARY};
@ -48,6 +54,10 @@ const Wrapper = styled.button`
color: ${colors.TEXT_SECONDARY};
border: 1px solid ${colors.DIVIDER};
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
}
&:hover {
color: ${colors.TEXT_PRIMARY};
background: ${colors.DIVIDER};
@ -64,6 +74,11 @@ const Wrapper = styled.button`
border: 0px;
color: ${colors.TEXT_SECONDARY};
&:focus {
color: ${colors.TEXT_PRIMARY};
box-shadow: none;
}
&:hover,
&:active {
color: ${colors.TEXT_PRIMARY};
@ -132,10 +147,12 @@ const Button = ({
isWhite = false,
isWebUsb = false,
isTransparent = false,
dataTest,
}: Props) => {
const newClassName = isWebUsb ? `${className} trezor-webusb-button` : className;
return (
<Wrapper
data-test={dataTest}
className={newClassName}
onClick={onClick}
onMouseEnter={onMouseEnter}
@ -162,6 +179,7 @@ Button.propTypes = {
isWhite: PropTypes.bool,
isWebUsb: PropTypes.bool,
isTransparent: PropTypes.bool,
dataTest: PropTypes.string,
};
export default Button;

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

@ -12,8 +12,6 @@ import colors from 'config/colors';
import { FONT_SIZE } from 'config/variables';
import * as LogActions from 'actions/LogActions';
declare var COMMITHASH: string;
type Props = {
opened: boolean,
isLanding: boolean,
@ -60,7 +58,7 @@ const Right = styled.div`
const Footer = ({ opened, toggle, isLanding }: Props) => (
<Wrapper>
<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="./assets/tos.pdf" isGreen>Terms</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 { NavLink } from 'react-router-dom';
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`
width: 100%;
height: 52px;
background: ${colors.HEADER};
svg {
fill: ${colors.WHITE};
height: 28px;
width: 100px;
}
overflow: hidden;
z-index: 200;
`;
const LayoutWrapper = styled.div`
@ -31,12 +32,65 @@ const LayoutWrapper = 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;
display: flex;
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`
color: ${colors.WHITE};
@ -58,10 +112,42 @@ const A = styled.a`
}
`;
const Header = (): React$Element<string> => (
<Wrapper>
type Props = {
sidebarEnabled?: boolean,
sidebarOpened?: ?boolean,
toggleSidebar?: toggleSidebarType,
};
const Header = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: Props) => (
<Wrapper data-test="Main__page__navigation">
<LayoutWrapper>
<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="/">
<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" />
@ -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 " />
</svg>
</NavLink>
</Left>
<Right>
<A href="https://trezor.io/" target="_blank" rel="noreferrer noopener">Trezor</A>
<A href="https://wiki.trezor.io/" target="_blank" rel="noreferrer noopener">Wiki</A>
<A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener">Blog</A>
<A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener">Support</A>
</Right>
</Logo>
<MenuLinks>
<Projects>
<A href="https://trezor.io/" target="_blank" rel="noreferrer noopener">Trezor</A>
<A href="https://wiki.trezor.io/" target="_blank" rel="noreferrer noopener">Wiki</A>
<A href="https://blog.trezor.io/" target="_blank" rel="noreferrer noopener">Blog</A>
<A href="https://trezor.io/support/" target="_blank" rel="noreferrer noopener">Support</A>
</Projects>
</MenuLinks>
</LayoutWrapper>
</Wrapper>
);

@ -12,10 +12,12 @@ const A = styled.a`
font-size: ${FONT_SIZE.SMALL};
${props => props.isGreen && css`
border-bottom: 1px solid ${colors.GREEN_PRIMARY};
text-decoration: underline;
text-decoration-color: ${colors.GREEN_PRIMARY};
`}
${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`
${props => props.isRoute && css`
stroke: ${colors.GRAY_LIGHT};
stroke: ${props.transparentRoute ? 'transparent' : colors.GRAY_LIGHT};
`}
${props => props.isPath && css`
stroke-width: ${props.transparentRoute ? '2px' : '1px'};
stroke-dasharray: 1, 200;
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;
`};
`;
@ -42,29 +43,31 @@ const StyledParagraph = styled(Paragraph)`
`;
const Loader = ({
className, text, isWhiteText = false, isSmallText, size = 100,
className, text, isWhiteText = false, isSmallText, size = 100, animationColor, transparentRoute,
}) => (
<Wrapper className={className} size={size}>
<StyledParagraph isSmallText={isSmallText} isWhiteText={isWhiteText}>{text}</StyledParagraph>
<SvgWrapper viewBox="25 25 50 50">
<CircleWrapper
animationColor={animationColor}
cx="50"
cy="50"
r="20"
fill="none"
stroke=""
strokeWidth="1"
strokeMiterlimit="10"
isRoute
transparentRoute={transparentRoute}
/>
<CircleWrapper
animationColor={animationColor}
cx="50"
cy="50"
r="20"
fill="none"
strokeWidth="1"
strokeMiterlimit="10"
isPath
transparentRoute={transparentRoute}
/>
</SvgWrapper>
</Wrapper>
@ -75,6 +78,8 @@ Loader.propTypes = {
isSmallText: PropTypes.bool,
className: PropTypes.string,
text: PropTypes.string,
animationColor: PropTypes.object,
transparentRoute: PropTypes.bool,
size: PropTypes.number,
};

@ -22,7 +22,7 @@ const Wrapper = styled.div`
position: relative;
color: ${colors.INFO_PRIMARY};
background: ${colors.INFO_SECONDARY};
padding: 24px 48px;
padding: 24px;
display: flex;
flex-direction: column;
text-align: left;
@ -32,9 +32,10 @@ const Wrapper = styled.div`
const Click = styled.div`
cursor: pointer;
position: absolute;
top: 8px;
top: 0;
right: 0;
padding: 12px;
padding-right: inherit;
padding-top: inherit;
color: inherit;
transition: opacity 0.3s;
@ -61,7 +62,7 @@ const Log = (props: Props): ?React$Element<string> => {
return (
<Wrapper>
<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>
<H2>Log</H2>
<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 Icon from 'components/Icon';
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 { TRANSITION, FONT_SIZE, FONT_WEIGHT } from 'config/variables';
import {
TRANSITION, FONT_SIZE, FONT_WEIGHT, SCREEN_SIZE,
} from 'config/variables';
type Props = {
type: string;
@ -31,7 +34,8 @@ const LoaderContent = styled.div`
justify-content: center;
align-items: center;
cursor: default;
background: ${props => getSecondaryColor(props.type)};
color: ${colors.WHITE};
background: ${props => getPrimaryColor(props.type)};
`;
const Wrapper = styled.button`
@ -46,6 +50,10 @@ const Wrapper = styled.button`
border: 1px solid ${props => getPrimaryColor(props.type)};
transition: ${TRANSITION.HOVER};
@media screen and (max-width: ${SCREEN_SIZE.SM}){
padding: 12px 24px;
}
&:hover {
color: ${colors.WHITE};
background: ${props => getPrimaryColor(props.type)};
@ -66,7 +74,7 @@ const NotificationButton = ({
>
{isLoading && (
<LoaderContent type={type}>
<Loader size={30} />
<Loader transparentRoute animationColor={WHITE_COLOR} size={30} />
</LoaderContent>
)}
{icon && (

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

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

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

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

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import colors from 'config/colors';
import styled from 'styled-components';
import ICONS from 'config/icons';
const SvgWrapper = styled.svg`
:hover {
@ -15,9 +16,6 @@ const Path = styled.path`
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 = ({
type = 'standard',
size = 24,
@ -29,13 +27,13 @@ const Icon = ({
hoverColor={hoverColor}
width={`${size}`}
height={`${size}`}
viewBox="0 0 32 32"
viewBox="0 0 1024 1024"
onClick={onClick}
>
<Path
key={type}
color={color}
d={type === 'hidden' ? HIDDEN : STANDARD}
d={type === 'hidden' ? ICONS.WALLET_HIDDEN : ICONS.WALLET_STANDARD}
/>
</SvgWrapper>
);

@ -59,6 +59,11 @@ const StyledInput = styled.input`
background-color: ${colors.WHITE};
transition: ${TRANSITION.HOVER};
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
}
&:disabled {
pointer-events: none;
background: ${colors.GRAY_LIGHT};
@ -183,7 +188,7 @@ class Input extends PureComponent {
autoComplete="off"
height={this.props.height}
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}
hasAddon={!!this.props.sideAddons}
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 */
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { H3 } from 'components/Heading';
import ICONS from 'config/icons';
import Icon from 'components/Icon';
import DeviceIcon from 'components/images/DeviceIcon';
import type { TrezorDevice } from 'flowtype';
type Props = {
device: TrezorDevice;
}
const Wrapper = styled.div``;
@ -12,13 +17,18 @@ const Header = styled.div`
padding: 48px;
`;
const ConfirmAction = () => (
const ConfirmAction = (props: Props) => (
<Wrapper>
<Header>
<Icon icon={ICONS.T1} size={100} />
<DeviceIcon device={props.device} size={100} />
<H3>Confirm action on your Trezor</H3>
</Header>
</Wrapper>
);
ConfirmAction.propTypes = {
device: PropTypes.object.isRequired,
};
export default ConfirmAction;

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

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

@ -41,7 +41,7 @@ const StyledLink = styled(Link)`
const Wrapper = styled.div`
width: 360px;
padding: 24px 48px;
padding: 30px 48px;
`;
const Column = styled.div`
@ -138,7 +138,7 @@ class DuplicateDevice extends PureComponent<Props, State> {
return (
<Wrapper>
<StyledLink onClick={onCancel}>
<Icon size={20} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
<Icon size={24} color={colors.TEXT_SECONDARY} icon={icons.CLOSE} />
</StyledLink>
<H3>Clone { device.label }?</H3>
<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 styled from 'styled-components';
import { H3 } from 'components/Heading';
import { H2 } from 'components/Heading';
import P from 'components/Paragraph';
import Button from 'components/Button';
@ -19,21 +19,20 @@ type Props = {
const Wrapper = styled.div`
width: 360px;
padding: 24px 48px;
padding: 30px 48px;
`;
const StyledP = styled(P)`
padding: 7px 0px;
`;
const StyledButton = styled(Button)`
margin: 0 0 10px 0;
padding: 20px 0px;
`;
const Row = styled.div`
display: flex;
flex-direction: column;
padding: 10px 0;
Button + Button {
margin-top: 10px;
}
`;
class ForgetDevice extends PureComponent<Props> {
@ -62,11 +61,11 @@ class ForgetDevice extends PureComponent<Props> {
render() {
return (
<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>
<Row>
<StyledButton onClick={() => this.forget()}>Forget</StyledButton>
<StyledButton isWhite onClick={this.props.onCancel}>Don&apos;t forget</StyledButton>
<Button onClick={() => this.forget()}>Forget</Button>
<Button isWhite onClick={this.props.onCancel}>Don&apos;t forget</Button>
</Row>
</Wrapper>
);

@ -31,12 +31,12 @@ const ButtonContent = styled.div`
`;
const StyledP = styled(P)`
padding: 10px 0;
padding: 20px 0;
`;
const Wrapper = styled.div`
width: 360px;
padding: 24px 48px;
padding: 30px 48px;
`;
const Text = styled.div`
@ -46,10 +46,10 @@ const Text = styled.div`
const Column = styled.div`
display: flex;
flex-direction: column;
`;
const StyledButton = styled(Button)`
margin: 5px 0;
Button + Button {
margin-top: 10px;
}
`;
const StyledLoader = styled(Loader)`
@ -128,7 +128,7 @@ class RememberDevice extends PureComponent<Props, State> {
<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>
<Column>
<StyledButton onClick={() => this.forget()}>
<Button onClick={() => this.forget()}>
<ButtonContent>
<Text>Forget</Text>
<StyledLoader
@ -138,12 +138,12 @@ class RememberDevice extends PureComponent<Props, State> {
text={this.state.countdown.toString()}
/>
</ButtonContent>
</StyledButton>
<StyledButton
</Button>
<Button
isWhite
onClick={() => onRememberDevice(device)}
>Remember
</StyledButton>
</Button>
</Column>
</Wrapper>
);

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

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

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

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

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

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

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

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

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

@ -25,7 +25,7 @@ type State = {
}
const Wrapper = styled.div`
padding: 24px 48px;
padding: 30px 48px;
`;
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 UpdateBridge from './components/UpdateBridge';
import UpdateFirmware from './components/UpdateFirmware';
import NoBackup from './components/NoBackup';
export type StateProps = {
connect: $ElementType<State, 'connect'>;
@ -33,6 +34,7 @@ const Notifications = (props: Props) => (
<OnlineStatus {...props} />
<UpdateBridge {...props} />
<UpdateFirmware {...props} />
<NoBackup {...props} />
</React.Fragment>
);

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

@ -2,18 +2,39 @@
import * as React from 'react';
import Notification from 'components/Notification';
import Bignumber from 'bignumber.js';
import Link from 'components/Link';
import type { Props } from '../../index';
export default (props: Props) => {
const { selectedAccount } = props;
const { account } = selectedAccount;
const { location } = props.router;
if (!location) return null;
const notifications: Array<Notification> = [];
// Example:
// if (location.state.device) {
// notifications.push(<Notification key="example" type="info" title="Static example" />);
// }
if (!location || !selectedAccount || !account) return null;
// 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 (
<React.Fragment>

@ -49,6 +49,12 @@ export const GREEN_COLOR = keyframes`
}
`;
export const WHITE_COLOR = keyframes`
0%, 100% {
stroke: white;
}
`;
export const PULSATE = keyframes`
0%, 100% {
opacity: 0.5;
@ -65,4 +71,31 @@ export const FADE_IN = keyframes`
100% {
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',
TOOLTIP_BACKGROUND: '#333333',
INPUT_FOCUSED_BORDER: '#A9A9A9',
INPUT_FOCUSED_SHADOW: '#d6d7d7',
};

@ -24,6 +24,9 @@ export default {
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',
],
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: [
'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: [
'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
// SMALLEST: '10px',
// SMALLER: '12px',
@ -13,7 +25,6 @@
// H3: '14px',
// H4: '12px',
// COUNTER: '11px',
export const FONT_SIZE = {
SMALL: '0.8571rem',
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,
Features,
DeviceStatus,
FirmwareRelease,
DeviceFirmwareStatus,
DeviceMode,
DeviceMessageType,
@ -55,6 +56,7 @@ export type AcquiredDevice = $Exact<{
+label: string,
+features: Features,
+firmware: DeviceFirmwareStatus,
+firmwareRelease: ?FirmwareRelease,
status: DeviceStatus,
+mode: DeviceMode,
state: ?string,

@ -27,22 +27,21 @@ declare module 'bignumber.js' {
// Methods
abs(): T_BigNumber;
cmp(n: $npm$big$number$object): $npm$cmp$result;
div(n: $npm$big$number$object): T_BigNumber;
dividedBy(n: $npm$big$number$object): T_BigNumber;
eq(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;
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;
lessThanOrEqualTo(n: $npm$big$number$object): boolean;
isLessThanOrEqualTo(n: $npm$big$number$object): boolean;
isNaN(): boolean;
minus(n: $npm$big$number$object): T_BigNumber;
mod(n: $npm$big$number$object): T_BigNumber;
plus(n: $npm$big$number$object): T_BigNumber;
pow(exp: number): BigNumber;
round(dp: ?number, rm: ?RM): T_BigNumber;
sqrt(): T_BigNumber;
times(n: $npm$big$number$object): T_BigNumber;
toExponential(dp: ?number): string;

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

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

@ -18,7 +18,12 @@ export type State = {
} | {
context: typeof MODAL.CONTEXT_EXTERNAL_WALLET,
windowType?: string;
}
} | {
context: typeof MODAL.CONTEXT_SCAN_QR,
} | {
context: typeof MODAL.CONTEXT_CONFIRMATION,
windowType: string;
};
const initialState: State = {
context: MODAL.CONTEXT_NONE,
@ -91,6 +96,17 @@ export default function modal(state: State = initialState, action: Action): Stat
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:
return state;
}

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

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

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

@ -95,6 +95,7 @@ export const getPendingSequence = (pending: Array<Transaction>): number => pendi
}, 0);
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) {
// regular transactions
// add fees from token txs and amount from regular txs

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

@ -8,7 +8,7 @@ import animationStyles from './Animations';
const baseStyles = createGlobalStyle`
html, body {
width: 100%;
height: 100%;
min-height: 100vh;
position: relative;
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
font-weight: ${FONT_WEIGHT.NORMAL};
@ -43,7 +43,7 @@ const baseStyles = createGlobalStyle`
}
#trezor-wallet-root {
height: 100%;
min-height: 100vh;
}
${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', () => {
it('get status', () => {
const deviceMock = [
{
connected: false,
},
{
connected: true,
available: false,
},
{
connected: true,
available: false,
type: null,
},
{
connected: true,
available: true,
type: 'acquired',
},
{
connected: true,
available: true,
type: 'unacquired',
},
{
connected: true,
available: true,
type: 'acquired',
status: 'occupied',
},
];
expect(utils.getStatus({ connected: false }))
.toBe('disconnected');
expect(utils.getStatus({ connected: true, available: false }))
.toBe('unavailable');
expect(utils.getStatus({
connected: true,
available: false,
type: null,
})).toBe('unavailable');
deviceMock.forEach((device) => {
expect(dUtils.getStatus(device)).toMatchSnapshot();
});
expect(utils.getStatus({
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', () => {
const data = [
{ transport: { type: 'ParallelTransport', version: 'webusb' } },
{ transport: { type: null, version: 'aaaaaa' } },
{ transport: { type: 'ParallelTransport', version: 'webusb' } },
];
data.forEach((item) => {
expect(dUtils.isWebUSB(item.transport)).toMatchSnapshot();
});
expect(utils.isWebUSB({ type: 'webusb', version: '1.6.0' })).toBe(true);
expect(utils.isWebUSB({ type: 'aaaa', version: 'aaaaaa' })).toBe(false);
expect(utils.isWebUSB({ type: 'webusb' })).toBe(true);
});
it('isDisabled', () => {
const data = [
{ selectedDevice: { features: null }, devices: [1, 2, 3], transport: { version: 'webusb' } },
{ selectedDevice: { features: null }, devices: [], transport: { version: 'test' } },
];
expect(utils.isDisabled(
{ selectedDevice: { features: null } },
[1, 2, 3],
{
version: 'webusb',
},
)).toBe(false);
data.forEach((item) => {
expect(dUtils.isDisabled(item.selectedDevice, item.devices, item.transport)).toMatchSnapshot();
});
expect(utils.isDisabled(
{ features: null }, [], { version: 'test' },
)).toBe(true);
});
it('get version', () => {
const deviceMock = [
{ },
{ features: {} },
{ features: { major_version: null } },
{ features: { major_version: 0 } },
{ features: { major_version: 1 } },
{ features: { major_version: 2 } },
];
deviceMock.forEach((device) => {
expect(dUtils.getVersion(device)).toMatchSnapshot();
});
expect(utils.getVersion({})).toBe('One');
expect(utils.getVersion({ features: {} })).toBe('One');
expect(utils.getVersion({ features: { major_version: null } })).toBe('One');
expect(utils.getVersion({ features: { major_version: 0 } })).toBe('One');
expect(utils.getVersion({ features: { major_version: 1 } })).toBe('One');
expect(utils.getVersion({ features: { major_version: 2 } })).toBe('T');
});
it('get status color', () => {
const entry = [
0,
null,
'sdsdsdsd',
'used-in-other-window',
'connected',
'unacquired',
'disconnected',
'unavailable',
];
entry.forEach((status) => {
expect(dUtils.getStatusColor(status)).toMatchSnapshot();
});
expect(utils.getStatusColor(0)).toBe('#494949');
expect(utils.getStatusColor(null)).toBe('#494949');
expect(utils.getStatusColor('sdsdsdsd')).toBe('#494949');
expect(utils.getStatusColor('used-in-other-window')).toBe('#EB8A00');
expect(utils.getStatusColor('connected')).toBe('#01B757');
expect(utils.getStatusColor('unacquired')).toBe('#EB8A00');
expect(utils.getStatusColor('disconnected')).toBe('#ED1212');
expect(utils.getStatusColor('unavailable')).toBe('#ED1212');
});
it('get status name', () => {
const entry = [
0,
null,
'sdsdsdsd',
'used-in-other-window',
'connected',
'unacquired',
'disconnected',
'unavailable',
];
entry.forEach((status) => {
expect(dUtils.getStatusName(status)).toMatchSnapshot();
});
expect(utils.getStatusName(0)).toBe('Status unknown');
expect(utils.getStatusName(null)).toBe('Status unknown');
expect(utils.getStatusName('sdsdsdsd')).toBe('Status unknown');
expect(utils.getStatusName('used-in-other-window')).toBe('Used in other window');
expect(utils.getStatusName('connected')).toBe('Connected');
expect(utils.getStatusName('unacquired')).toBe('Used in other window');
expect(utils.getStatusName('disconnected')).toBe('Disconnected');
expect(utils.getStatusName('unavailable')).toBe('Unavailable');
});
});
});

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

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

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

@ -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) => {
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) {
version = 'T';
} else {
version = '1';
version = 'One';
}
return version;
};

@ -56,7 +56,12 @@ export const hexToString = (hex: string): string => {
export const toDecimalAmount = (amount: string | number, decimals: number): string => {
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) {
return '0';
}
@ -64,7 +69,12 @@ export const toDecimalAmount = (amount: string | number, decimals: number): stri
export const fromDecimalAmount = (amount: string | number, decimals: number): string => {
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) {
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