diff options
author | brunobar79 <brunobar79@gmail.com> | 2018-08-21 09:51:15 +0800 |
---|---|---|
committer | brunobar79 <brunobar79@gmail.com> | 2018-08-21 09:51:35 +0800 |
commit | ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32 (patch) | |
tree | d0e54e987f7ee42d21dd8a5aeb582c4a2c72e02c /ui | |
parent | 6e813db47309b7773d430fecc6a467be66f6f16b (diff) | |
parent | 887cad973f25f43d2d4502ff31657f156a44b188 (diff) | |
download | tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.gz tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.bz2 tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.lz tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.xz tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.zst tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.zip |
fix merge conflicts
Diffstat (limited to 'ui')
36 files changed, 886 insertions, 144 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index 9edb3511a..6bcc64e17 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -12,6 +12,7 @@ const { fetchLocale } = require('../i18n-helper') const log = require('loglevel') const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums') const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util') +const WebcamUtils = require('../lib/webcam-utils') var actions = { _setBackgroundConnection: _setBackgroundConnection, @@ -33,6 +34,8 @@ var actions = { ALERT_CLOSE: 'UI_ALERT_CLOSE', showAlert: showAlert, hideAlert: hideAlert, + QR_CODE_DETECTED: 'UI_QR_CODE_DETECTED', + qrCodeDetected, // network dropdown open NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN', NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE', @@ -88,7 +91,7 @@ var actions = { connectHardware, checkHardwareStatus, forgetDevice, - unlockTrezorAccount, + unlockHardwareWalletAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, @@ -125,7 +128,8 @@ var actions = { SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - setCurrentCurrency: setCurrentCurrency, + showQrScanner, + setCurrentCurrency, setCurrentAccountTab, // account detail screen SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', @@ -231,6 +235,8 @@ var actions = { UPDATE_TOKENS: 'UPDATE_TOKENS', setRpcTarget: setRpcTarget, setProviderType: setProviderType, + SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH', + setHardwareWalletDefaultHdPath, updateProviderType, // loading overlay SHOW_LOADING: 'SHOW_LOADING_INDICATION', @@ -635,12 +641,12 @@ function addNewAccount () { } } -function checkHardwareStatus (deviceName) { - log.debug(`background.checkHardwareStatus`, deviceName) +function checkHardwareStatus (deviceName, hdPath) { + log.debug(`background.checkHardwareStatus`, deviceName, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.checkHardwareStatus(deviceName, (err, unlocked) => { + background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -677,12 +683,12 @@ function forgetDevice (deviceName) { } } -function connectHardware (deviceName, page) { - log.debug(`background.connectHardware`, deviceName, page) +function connectHardware (deviceName, page, hdPath) { + log.debug(`background.connectHardware`, deviceName, page, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.connectHardware(deviceName, page, (err, accounts) => { + background.connectHardware(deviceName, page, hdPath, (err, accounts) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -698,12 +704,12 @@ function connectHardware (deviceName, page) { } } -function unlockTrezorAccount (index) { - log.debug(`background.unlockTrezorAccount`, index) +function unlockHardwareWalletAccount (index, deviceName, hdPath) { + log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.unlockTrezorAccount(index, (err, accounts) => { + background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -723,6 +729,28 @@ function showInfoPage () { } } +function showQrScanner (ROUTE) { + return (dispatch, getState) => { + return WebcamUtils.checkStatus() + .then(status => { + if (!status.environmentReady) { + // We need to switch to fullscreen mode to ask for permission + global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`) + } else { + dispatch(actions.showModal({ + name: 'QR_SCANNER', + })) + } + }).catch(e => { + dispatch(actions.showModal({ + name: 'QR_SCANNER', + error: true, + errorType: e.type, + })) + }) + } +} + function setCurrentCurrency (currencyCode) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -1809,6 +1837,17 @@ function hideAlert () { } } +/** + * This action will receive two types of values via qrCodeData + * an object with the following structure {type, values} + * or null (used to clear the previous value) + */ +function qrCodeDetected (qrCodeData) { + return { + type: actions.QR_CODE_DETECTED, + value: qrCodeData, + } +} function showLoadingIndication (message) { return { @@ -1817,6 +1856,13 @@ function showLoadingIndication (message) { } } +function setHardwareWalletDefaultHdPath ({ device, path }) { + return { + type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH, + value: {device, path}, + } +} + function hideLoadingIndication () { return { type: actions.HIDE_LOADING, diff --git a/ui/app/app.js b/ui/app/app.js index dbb6146d1..4fcf092ca 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -39,8 +39,7 @@ const Modal = require('./components/modals/index').Modal // Global Alert const Alert = require('./components/alert') -const AppHeader = require('./components/app-header') - +import AppHeader from './components/app-header' import UnlockPage from './components/pages/unlock-page' // Routes diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 9c063d31e..bcada41e3 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -229,6 +229,7 @@ AccountMenu.prototype.renderKeyringType = function (keyring) { let label switch (type) { case 'Trezor Hardware': + case 'Ledger Hardware': label = this.context.t('hardware') break case 'Simple Key Pair': diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index 07ca6cf84..b8b002dcc 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import { matchPath } from 'react-router-dom' @@ -11,7 +11,7 @@ const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require(' const Identicon = require('../identicon') const NetworkIndicator = require('../network') -class AppHeader extends Component { +export default class AppHeader extends PureComponent { static propTypes = { history: PropTypes.object, location: PropTypes.object, @@ -107,20 +107,19 @@ class AppHeader extends Component { onClick={() => history.push(DEFAULT_ROUTE)} > <img - className="app-header__metafox" - src="/images/metamask-fox.svg" + className="app-header__metafox-logo app-header__metafox-logo--horizontal" + src="/images/logo/metamask-logo-horizontal-beta.svg" + height={30} + /> + <img + className="app-header__metafox-logo app-header__metafox-logo--icon" + src="/images/logo/metamask-fox.svg" height={42} width={42} /> - <div className="flex-row"> - <h1>{ this.context.t('appName') }</h1> - <div className="app-header__beta-label"> - { this.context.t('beta') } - </div> - </div> </div> <div className="app-header__account-menu-container"> - <div className="network-component-wrapper"> + <div className="app-header__network-component-wrapper"> <NetworkIndicator network={network} provider={provider} @@ -135,5 +134,3 @@ class AppHeader extends Component { ) } } - -export default AppHeader diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app-header/index.js index daa31f621..6de2f9c78 100644 --- a/ui/app/components/app-header/index.js +++ b/ui/app/components/app-header/index.js @@ -1,2 +1 @@ -import AppHeader from './app-header.container' -module.exports = AppHeader +export { default } from './app-header.container' diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/components/app-header/index.scss index 3ccfd5c15..325844af5 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/components/app-header/index.scss @@ -30,21 +30,19 @@ } } - &__metafox { + &__metafox-logo { cursor: pointer; - } - &__beta-label { - font-family: Roboto; - text-transform: uppercase; - font-weight: 500; - font-size: .8rem; - color: $buttercup; - margin-left: 5px; - line-height: initial; + &--icon { + @media screen and (min-width: $break-large) { + display: none; + } + } - @media screen and (max-width: 575px) { - display: none; + &--horizontal { + @media screen and (max-width: $break-small) { + display: none; + } } } @@ -83,31 +81,10 @@ flex-flow: row nowrap; align-items: center; } -} - -.app-header h1 { - font-family: Roboto; - text-transform: uppercase; - font-weight: 400; - font-size: 1.1rem; - position: relative; - padding-left: 15px; - color: #5b5d67; - @media screen and (max-width: 575px) { - display: none; + &__network-component-wrapper { + display: flex; + flex-direction: row; + align-items: center; } } - -h2.page-subtitle { - text-transform: uppercase; - color: #aeaeae; - font-size: 1em; - margin: 12px; -} - -.network-component-wrapper { - display: flex; - flex-direction: row; - align-items: center; -} diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index b9f99b3d1..f538fd555 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -27,6 +27,7 @@ function EnsInput () { } EnsInput.prototype.onChange = function (recipient) { + const network = this.props.network const networkHasEnsSupport = getNetworkEnsSupport(network) @@ -54,6 +55,7 @@ EnsInput.prototype.render = function () { const opts = extend(props, { list: 'addresses', onChange: this.onChange.bind(this), + qrScanner: true, }) return h('div', { style: { width: '100%', position: 'relative' }, diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index b3e14ce23..35d38e2a3 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -19,3 +19,5 @@ @import './sender-to-recipient/index'; @import './tabs/index'; + +@import './app-header/index'; diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index 049c75eee..38f70c33c 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -60,7 +60,8 @@ AccountDetailsModal.prototype.render = function () { }) let exportPrivateKeyFeatureEnabled = true - if (keyring.type === 'Trezor Hardware') { + // This feature is disabled for hardware wallets + if (keyring.type.search('Hardware') !== -1) { exportPrivateKeyFeatureEnabled = false } diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss index e198cca44..0acccf172 100644 --- a/ui/app/components/modals/index.scss +++ b/ui/app/components/modals/index.scss @@ -1,5 +1,7 @@ @import './customize-gas/index'; +@import './qr-scanner/index'; + .modal-container { width: 100%; height: 100%; diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index f59825ed1..5dda50e52 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -21,6 +21,7 @@ const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') const ConfirmResetAccount = require('./confirm-reset-account') const ConfirmRemoveAccount = require('./confirm-remove-account') +const QRScanner = require('./qr-scanner') const TransactionConfirmed = require('./transaction-confirmed') const WelcomeBeta = require('./welcome-beta') const Notification = require('./notification') @@ -346,6 +347,18 @@ const MODALS = { borderRadius: '8px', }, }, + QR_SCANNER: { + contents: h(QRScanner), + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, DEFAULT: { contents: [], diff --git a/ui/app/components/modals/qr-scanner/index.js b/ui/app/components/modals/qr-scanner/index.js new file mode 100644 index 000000000..470dee1f4 --- /dev/null +++ b/ui/app/components/modals/qr-scanner/index.js @@ -0,0 +1,2 @@ +import QrScanner from './qr-scanner.container' +module.exports = QrScanner diff --git a/ui/app/components/modals/qr-scanner/index.scss b/ui/app/components/modals/qr-scanner/index.scss new file mode 100644 index 000000000..6fa81d51f --- /dev/null +++ b/ui/app/components/modals/qr-scanner/index.scss @@ -0,0 +1,83 @@ +.qr-scanner { + width: 100%; + height: 100%; + background-color: #fff; + display: flex; + flex-flow: column; + border-radius: 8px; + + &__title { + font-size: 1.5rem; + font-weight: 500; + padding: 16px 0; + text-align: center; + } + + &__content { + padding-left: 20px; + padding-right: 20px; + + &__video-wrapper { + overflow: hidden; + width: 100%; + height: 275px; + display: flex; + align-items: center; + justify-content: center; + + video { + transform: scaleX(-1); + width: auto; + height: 275px; + } + } + } + + &__status { + text-align: center; + font-size: 14px; + padding: 15px; + } + + &__image { + font-size: 1.5rem; + font-weight: 500; + padding: 16px 0 0; + text-align: center; + } + + &__error { + text-align: center; + font-size: 16px; + padding: 15px; + } + + &__footer { + padding: 20px; + flex-direction: row; + display: flex; + + button { + margin-right: 15px; + } + + button:last-of-type { + margin-right: 0; + background-color: #009eec; + border: none; + color: #fff; + } + } + + &__close::after { + content: '\00D7'; + font-size: 35px; + color: #9b9b9b; + position: absolute; + top: 4px; + right: 20px; + cursor: pointer; + font-weight: 300; + } +} + diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/modals/qr-scanner/qr-scanner.component.js new file mode 100644 index 000000000..cb8d07d83 --- /dev/null +++ b/ui/app/components/modals/qr-scanner/qr-scanner.component.js @@ -0,0 +1,216 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { BrowserQRCodeReader } from '@zxing/library' +import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars +import Spinner from '../../spinner' +import WebcamUtils from '../../../../lib/webcam-utils' +import PageContainerFooter from '../../page-container/page-container-footer/page-container-footer.component' + +export default class QrScanner extends Component { + static propTypes = { + hideModal: PropTypes.func.isRequired, + qrCodeDetected: PropTypes.func, + scanQrCode: PropTypes.func, + error: PropTypes.bool, + errorType: PropTypes.string, + } + + static contextTypes = { + t: PropTypes.func, + } + + constructor (props, context) { + super(props) + + this.state = { + ready: false, + msg: context.t('accessingYourCamera'), + } + this.codeReader = null + this.permissionChecker = null + this.needsToReinit = false + + // Clear pre-existing qr code data before scanning + this.props.qrCodeDetected(null) + } + + componentDidMount () { + this.initCamera() + } + + async checkPermisisions () { + const { permissions } = await WebcamUtils.checkStatus() + if (permissions) { + clearTimeout(this.permissionChecker) + // Let the video stream load first... + setTimeout(_ => { + this.setState({ + ready: true, + msg: this.context.t('scanInstructions'), + }) + if (this.needsToReinit) { + this.initCamera() + this.needsToReinit = false + } + }, 2000) + } else { + // Keep checking for permissions + this.permissionChecker = setTimeout(_ => { + this.checkPermisisions() + }, 1000) + } + } + + componentWillUnmount () { + clearTimeout(this.permissionChecker) + if (this.codeReader) { + this.codeReader.reset() + } + } + + initCamera () { + this.codeReader = new BrowserQRCodeReader() + this.codeReader.getVideoInputDevices() + .then(videoInputDevices => { + clearTimeout(this.permissionChecker) + this.checkPermisisions() + this.codeReader.decodeFromInputVideoDevice(undefined, 'video') + .then(content => { + const result = this.parseContent(content.text) + if (result.type !== 'unknown') { + this.props.qrCodeDetected(result) + this.stopAndClose() + } else { + this.setState({msg: this.context.t('unknownQrCode')}) + } + }) + .catch(err => { + if (err && err.name === 'NotAllowedError') { + this.setState({msg: this.context.t('youNeedToAllowCameraAccess')}) + clearTimeout(this.permissionChecker) + this.needsToReinit = true + this.checkPermisisions() + } + }) + }).catch(err => { + console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err) + }) + } + + parseContent (content) { + let type = 'unknown' + let values = {} + + // Here we could add more cases + // To parse other type of links + // For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681) + + + // Ethereum address links - fox ex. ethereum:0x.....1111 + if (content.split('ethereum:').length > 1) { + + type = 'address' + values = {'address': content.split('ethereum:')[1] } + + // Regular ethereum addresses - fox ex. 0x.....1111 + } else if (content.substring(0, 2).toLowerCase() === '0x') { + + type = 'address' + values = {'address': content } + + } + return {type, values} + } + + + stopAndClose = () => { + if (this.codeReader) { + this.codeReader.reset() + } + this.setState({ ready: false }) + this.props.hideModal() + } + + tryAgain = () => { + // close the modal + this.stopAndClose() + // wait for the animation and try again + setTimeout(_ => { + this.props.scanQrCode() + }, 1000) + } + + renderVideo () { + return ( + <div className={'qr-scanner__content__video-wrapper'}> + <video + id="video" + style={{ + display: this.state.ready ? 'block' : 'none', + }} + /> + { !this.state.ready ? <Spinner color={'#F7C06C'} /> : null} + </div> + ) + } + + renderErrorModal () { + let title, msg + + if (this.props.error) { + if (this.props.errorType === 'NO_WEBCAM_FOUND') { + title = this.context.t('noWebcamFoundTitle') + msg = this.context.t('noWebcamFound') + } else { + title = this.context.t('unknownCameraErrorTitle') + msg = this.context.t('unknownCameraError') + } + } + + return ( + <div className="qr-scanner"> + <div className="qr-scanner__close" onClick={this.stopAndClose}></div> + + <div className="qr-scanner__image"> + <img src={'images/webcam.svg'} width={70} height={70} /> + </div> + <div className="qr-scanner__title"> + { title } + </div> + <div className={'qr-scanner__error'}> + {msg} + </div> + <PageContainerFooter + onCancel={this.stopAndClose} + onSubmit={this.tryAgain} + cancelText={this.context.t('cancel')} + submitText={this.context.t('tryAgain')} + submitButtonType="confirm" + /> + </div> + ) + } + + render () { + const { t } = this.context + + if (this.props.error) { + return this.renderErrorModal() + } + + return ( + <div className="qr-scanner"> + <div className="qr-scanner__close" onClick={this.stopAndClose}></div> + <div className="qr-scanner__title"> + { `${t('scanQrCode')}` } + </div> + <div className="qr-scanner__content"> + { this.renderVideo() } + </div> + <div className={'qr-scanner__status'}> + {this.state.msg} + </div> + </div> + ) + } +} diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.container.js b/ui/app/components/modals/qr-scanner/qr-scanner.container.js new file mode 100644 index 000000000..d0a35e03b --- /dev/null +++ b/ui/app/components/modals/qr-scanner/qr-scanner.container.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux' +import QrScanner from './qr-scanner.component' + +const { hideModal, qrCodeDetected, showQrScanner } = require('../../../actions') +import { + SEND_ROUTE, +} from '../../../routes' + +const mapStateToProps = state => { + return { + error: state.appState.modal.modalState.props.error, + errorType: state.appState.modal.modalState.props.errorType, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), + scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(QrScanner) diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js index c722d1f55..488a189ea 100644 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -2,16 +2,75 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const genAccountLink = require('../../../../../lib/account-link.js') +const Select = require('react-select').default class AccountList extends Component { constructor (props, context) { super(props) } + getHdPaths () { + return [ + { + label: `Ledger Live`, + value: `m/44'/60'/0'/0/0`, + }, + { + label: `Legacy (MEW / MyCrypto)`, + value: `m/44'/60'/0'`, + }, + ] + } + + goToNextPage = () => { + // If we have < 5 accounts, it's restricted by BIP-44 + if (this.props.accounts.length === 5) { + this.props.getPage(this.props.device, 1, this.props.selectedPath) + } else { + this.props.onAccountRestriction() + } + } + + goToPreviousPage = () => { + this.props.getPage(this.props.device, -1, this.props.selectedPath) + } + + renderHdPathSelector () { + const { onPathChange, selectedPath } = this.props + + const options = this.getHdPaths() + return h('div', [ + h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')), + h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')), + h('div.hw-connect__hdPath', [ + h(Select, { + className: 'hw-connect__hdPath__select', + name: 'hd-path-select', + clearable: false, + value: selectedPath, + options, + onChange: (opt) => { + onPathChange(opt.value) + }, + }), + ]), + ]) + } + + capitalizeDevice (device) { + return device.slice(0, 1).toUpperCase() + device.slice(1) + } + renderHeader () { + const { device } = this.props return ( h('div.hw-connect', [ - h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')), + + h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`), + + device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null, + + h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')), h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')), ]) ) @@ -61,7 +120,7 @@ class AccountList extends Component { h( 'button.hw-list-pagination__button', { - onClick: () => this.props.getPage(-1), + onClick: this.goToPreviousPage, }, `< ${this.context.t('prev')}` ), @@ -69,7 +128,7 @@ class AccountList extends Component { h( 'button.hw-list-pagination__button', { - onClick: () => this.props.getPage(1), + onClick: this.goToNextPage, }, `${this.context.t('next')} >` ), @@ -95,7 +154,7 @@ class AccountList extends Component { h( `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`, { - onClick: this.props.onUnlockAccount.bind(this), + onClick: this.props.onUnlockAccount.bind(this, this.props.device), ...buttonProps, }, [this.context.t('unlock')] @@ -106,7 +165,7 @@ class AccountList extends Component { renderForgetDevice () { return h('div.hw-forget-device-container', {}, [ h('a', { - onClick: this.props.onForgetDevice.bind(this), + onClick: this.props.onForgetDevice.bind(this, this.props.device), }, this.context.t('forgetDevice')), ]) } @@ -125,6 +184,9 @@ class AccountList extends Component { AccountList.propTypes = { + onPathChange: PropTypes.func.isRequired, + selectedPath: PropTypes.string.isRequired, + device: PropTypes.string.isRequired, accounts: PropTypes.array.isRequired, onAccountChange: PropTypes.func.isRequired, onForgetDevice: PropTypes.func.isRequired, @@ -134,6 +196,7 @@ AccountList.propTypes = { history: PropTypes.object, onUnlockAccount: PropTypes.func, onCancel: PropTypes.func, + onAccountRestriction: PropTypes.func, } AccountList.contextTypes = { diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index cb2b86595..b3dfa4ee2 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -5,6 +5,52 @@ const h = require('react-hyperscript') class ConnectScreen extends Component { constructor (props, context) { super(props) + this.state = { + selectedDevice: null, + } + } + + connect = () => { + if (this.state.selectedDevice) { + this.props.connectToHardwareWallet(this.state.selectedDevice) + } + return null + } + + renderConnectToTrezorButton () { + return h( + `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`, + { onClick: _ => this.setState({selectedDevice: 'trezor'}) }, + h('img.hw-connect__btn__img', { + src: 'images/trezor-logo.svg', + }) + ) + } + + renderConnectToLedgerButton () { + return h( + `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`, + { onClick: _ => this.setState({selectedDevice: 'ledger'}) }, + h('img.hw-connect__btn__img', { + src: 'images/ledger-logo.svg', + }) + ) + } + + renderButtons () { + return ( + h('div', {}, [ + h('div.hw-connect__btn-wrapper', {}, [ + this.renderConnectToLedgerButton(), + this.renderConnectToTrezorButton(), + ]), + h( + `button.hw-connect__connect-btn${!this.state.selectedDevice ? '.disabled' : ''}`, + { onClick: this.connect }, + this.context.t('connect') + ), + ]) + ) } renderUnsupportedBrowser () { @@ -12,7 +58,7 @@ class ConnectScreen extends Component { h('div.new-account-connect-form.unsupported-browser', {}, [ h('div.hw-connect', [ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')), - h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')), + h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')), ]), h( 'button.btn-primary.btn--large', @@ -30,29 +76,31 @@ class ConnectScreen extends Component { renderHeader () { return ( h('div.hw-connect__header', {}, [ - h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)), - h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)), + h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)), + h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)), ]) ) } + getAffiliateLinks () { + const links = { + trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`, + ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`, + } + + const text = this.context.t('orderOneHere') + const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger) + + return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }}) + } + renderTrezorAffiliateLink () { - return h('div.hw-connect__get-trezor', {}, [ - h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)), - h('a.hw-connect__get-trezor__link', { - href: 'https://shop.trezor.io/?a=metamask', - target: '_blank', - }, this.context.t('orderOneHere')), + return h('div.hw-connect__get-hw', {}, [ + h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)), + this.getAffiliateLinks(), ]) } - renderConnectToTrezorButton () { - return h( - 'button.btn-primary.btn--large', - { onClick: this.props.connectToTrezor.bind(this) }, - this.props.btnText - ) - } scrollToTutorial = (e) => { if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) @@ -102,7 +150,7 @@ class ConnectScreen extends Component { return ( h('div.hw-connect__footer', {}, [ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)), - this.renderConnectToTrezorButton(), + this.renderButtons(), h('p.hw-connect__footer__msg', {}, [ this.context.t(`havingTroubleConnecting`), h('a.hw-connect__footer__link', { @@ -118,8 +166,8 @@ class ConnectScreen extends Component { return ( h('div.new-account-connect-form', {}, [ this.renderHeader(), + this.renderButtons(), this.renderTrezorAffiliateLink(), - this.renderConnectToTrezorButton(), this.renderLearnMore(), this.renderTutorialSteps(), this.renderFooter(), @@ -136,8 +184,7 @@ class ConnectScreen extends Component { } ConnectScreen.propTypes = { - connectToTrezor: PropTypes.func.isRequired, - btnText: PropTypes.string.isRequired, + connectToHardwareWallet: PropTypes.func.isRequired, browserSupported: PropTypes.bool.isRequired, } diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 6ad44e540..4fe25f629 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -7,17 +7,19 @@ const ConnectScreen = require('./connect-screen') const AccountList = require('./account-list') const { DEFAULT_ROUTE } = require('../../../../routes') const { formatBalance } = require('../../../../util') +const { getPlatform } = require('../../../../../../app/scripts/lib/util') +const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums') class ConnectHardwareForm extends Component { constructor (props, context) { super(props) this.state = { error: null, - btnText: context.t('connectToTrezor'), selectedAccount: null, accounts: [], browserSupported: true, unlocked: false, + device: null, } } @@ -38,25 +40,43 @@ class ConnectHardwareForm extends Component { } async checkIfUnlocked () { - const unlocked = await this.props.checkHardwareStatus('trezor') - if (unlocked) { - this.setState({unlocked: true}) - this.getPage(0) - } + ['trezor', 'ledger'].forEach(async device => { + const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device]) + if (unlocked) { + this.setState({unlocked: true}) + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + }) } - connectToTrezor = () => { + connectToHardwareWallet = (device) => { + // Ledger hardware wallets are not supported on firefox + if (getPlatform() === PLATFORM_FIREFOX && device === 'ledger') { + this.setState({ browserSupported: false, error: null}) + return null + } + if (this.state.accounts.length) { return null } - this.setState({ btnText: this.context.t('connecting')}) - this.getPage(0) + + // Default values + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + + onPathChange = (path) => { + this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path}) + this.getPage(this.state.device, 0, path) } onAccountChange = (account) => { this.setState({selectedAccount: account.toString(), error: null}) } + onAccountRestriction = () => { + this.setState({error: this.context.t('ledgerAccountRestriction') }) + } + showTemporaryAlert () { this.props.showAlert(this.context.t('hardwareWalletConnected')) // Autohide the alert after 5 seconds @@ -65,9 +85,9 @@ class ConnectHardwareForm extends Component { }, 5000) } - getPage = (page) => { + getPage = (device, page, hdPath) => { this.props - .connectHardware('trezor', page) + .connectHardware(device, page, hdPath) .then(accounts => { if (accounts.length) { @@ -77,7 +97,7 @@ class ConnectHardwareForm extends Component { this.showTemporaryAlert() } - const newState = { unlocked: true, error: null } + const newState = { unlocked: true, device, error: null } // Default to the first account if (this.state.selectedAccount === null) { accounts.forEach((a, i) => { @@ -104,20 +124,18 @@ class ConnectHardwareForm extends Component { }) .catch(e => { if (e === 'Window blocked') { - this.setState({ browserSupported: false }) - } else { + this.setState({ browserSupported: false, error: null}) + } else if (e !== 'Window closed' && e !== 'Popup closed') { this.setState({ error: e.toString() }) } - this.setState({ btnText: this.context.t('connectToTrezor') }) }) } - onForgetDevice = () => { - this.props.forgetDevice('trezor') + onForgetDevice = (device) => { + this.props.forgetDevice(device) .then(_ => { this.setState({ error: null, - btnText: this.context.t('connectToTrezor'), selectedAccount: null, accounts: [], unlocked: false, @@ -127,13 +145,13 @@ class ConnectHardwareForm extends Component { }) } - onUnlockAccount = () => { + onUnlockAccount = (device) => { if (this.state.selectedAccount === null) { this.setState({ error: this.context.t('accountSelectionRequired') }) } - this.props.unlockTrezorAccount(this.state.selectedAccount) + this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device) .then(_ => { this.props.history.push(DEFAULT_ROUTE) }).catch(e => { @@ -154,13 +172,15 @@ class ConnectHardwareForm extends Component { renderContent () { if (!this.state.accounts.length) { return h(ConnectScreen, { - connectToTrezor: this.connectToTrezor, - btnText: this.state.btnText, + connectToHardwareWallet: this.connectToHardwareWallet, browserSupported: this.state.browserSupported, }) } return h(AccountList, { + onPathChange: this.onPathChange, + selectedPath: this.props.defaultHdPaths[this.state.device], + device: this.state.device, accounts: this.state.accounts, selectedAccount: this.state.selectedAccount, onAccountChange: this.onAccountChange, @@ -170,6 +190,7 @@ class ConnectHardwareForm extends Component { onUnlockAccount: this.onUnlockAccount, onForgetDevice: this.onForgetDevice, onCancel: this.onCancel, + onAccountRestriction: this.onAccountRestriction, }) } @@ -190,13 +211,15 @@ ConnectHardwareForm.propTypes = { forgetDevice: PropTypes.func, showAlert: PropTypes.func, hideAlert: PropTypes.func, - unlockTrezorAccount: PropTypes.func, + unlockHardwareWalletAccount: PropTypes.func, + setHardwareWalletDefaultHdPath: PropTypes.func, numberOfExistingAccounts: PropTypes.number, history: PropTypes.object, t: PropTypes.func, network: PropTypes.string, accounts: PropTypes.object, address: PropTypes.string, + defaultHdPaths: PropTypes.object, } const mapStateToProps = state => { @@ -204,28 +227,35 @@ const mapStateToProps = state => { metamask: { network, selectedAddress, identities = {}, accounts = [] }, } = state const numberOfExistingAccounts = Object.keys(identities).length + const { + appState: { defaultHdPaths }, + } = state return { network, accounts, address: selectedAddress, numberOfExistingAccounts, + defaultHdPaths, } } const mapDispatchToProps = dispatch => { return { - connectHardware: (deviceName, page) => { - return dispatch(actions.connectHardware(deviceName, page)) + setHardwareWalletDefaultHdPath: ({device, path}) => { + return dispatch(actions.setHardwareWalletDefaultHdPath({device, path})) + }, + connectHardware: (deviceName, page, hdPath) => { + return dispatch(actions.connectHardware(deviceName, page, hdPath)) }, - checkHardwareStatus: (deviceName) => { - return dispatch(actions.checkHardwareStatus(deviceName)) + checkHardwareStatus: (deviceName, hdPath) => { + return dispatch(actions.checkHardwareStatus(deviceName, hdPath)) }, forgetDevice: (deviceName) => { return dispatch(actions.forgetDevice(deviceName)) }, - unlockTrezorAccount: index => { - return dispatch(actions.unlockTrezorAccount(index)) + unlockHardwareWalletAccount: (index, deviceName, hdPath) => { + return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath)) }, showImportPage: () => dispatch(actions.showImportPage()), showConnectPage: () => dispatch(actions.showConnectPage()), diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js index 7a0b1a18e..df7bcb7cc 100644 --- a/ui/app/components/send/send-content/send-content.component.js +++ b/ui/app/components/send/send-content/send-content.component.js @@ -11,6 +11,7 @@ export default class SendContent extends Component { static propTypes = { updateGas: PropTypes.func, + scanQrCode: PropTypes.func, }; render () { @@ -18,7 +19,10 @@ export default class SendContent extends Component { <PageContainerContent> <div className="send-v2__form"> <SendFromRow /> - <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> + <SendToRow + updateGas={(updateData) => this.props.updateGas(updateData)} + scanQrCode={ _ => this.props.scanQrCode()} + /> <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendGasRow /> <SendHexDataRow /> diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js index 892ad5d67..1163dcffc 100644 --- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js +++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js @@ -17,6 +17,7 @@ export default class SendToRow extends Component { updateGas: PropTypes.func, updateSendTo: PropTypes.func, updateSendToError: PropTypes.func, + scanQrCode: PropTypes.func, }; static contextTypes = { @@ -51,6 +52,7 @@ export default class SendToRow extends Component { showError={inError} > <EnsInput + scanQrCode={_ => this.props.scanQrCode()} accounts={toAccounts} closeDropdown={() => closeToDropdown()} dropdownOpen={toDropdownOpen} diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js index 6f1b20c55..0d8ffd179 100644 --- a/ui/app/components/send/send.component.js +++ b/ui/app/components/send/send.component.js @@ -38,12 +38,30 @@ export default class SendTransactionScreen extends PersistentForm { updateAndSetGasTotal: PropTypes.func, updateSendErrors: PropTypes.func, updateSendTokenBalance: PropTypes.func, + scanQrCode: PropTypes.func, + qrCodeDetected: PropTypes.func, + qrCodeData: PropTypes.object, }; static contextTypes = { t: PropTypes.func, }; + componentWillReceiveProps (nextProps) { + if (nextProps.qrCodeData) { + if (nextProps.qrCodeData.type === 'address') { + const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase() + const currentAddress = this.props.to && this.props.to.toLowerCase() + if (currentAddress !== scannedAddress) { + this.props.updateSendTo(scannedAddress) + this.updateGas({ to: scannedAddress }) + // Clean up QR code data after handling + this.props.qrCodeDetected(null) + } + } + } + } + updateGas ({ to: updatedToAddress, amount: value } = {}) { const { amount, @@ -158,6 +176,16 @@ export default class SendTransactionScreen extends PersistentForm { address, }) this.updateGas() + + // Show QR Scanner modal if ?scan=true + if (window.location.search === '?scan=true') { + this.props.scanQrCode() + + // Clear the queryString param after showing the modal + const cleanUrl = location.href.split('?')[0] + history.pushState({}, null, `${cleanUrl}`) + window.location.hash = '#send' + } } componentWillUnmount () { @@ -170,7 +198,10 @@ export default class SendTransactionScreen extends PersistentForm { return ( <div className="page-container"> <SendHeader history={history}/> - <SendContent updateGas={(updateData) => this.updateGas(updateData)}/> + <SendContent + updateGas={(updateData) => this.updateGas(updateData)} + scanQrCode={_ => this.props.scanQrCode()} + /> <SendFooter history={history}/> </div> ) diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js index 44ebd2792..41735de64 100644 --- a/ui/app/components/send/send.container.js +++ b/ui/app/components/send/send.container.js @@ -21,11 +21,15 @@ import { getSendFromObject, getSendTo, getTokenBalance, + getQrCodeData, } from './send.selectors' import { + updateSendTo, updateSendTokenBalance, updateGasData, setGasTotal, + showQrScanner, + qrCodeDetected, } from '../../actions' import { resetSendState, @@ -35,6 +39,10 @@ import { calcGasTotal, } from './send.utils.js' +import { + SEND_ROUTE, +} from '../../routes' + module.exports = compose( withRouter, connect(mapStateToProps, mapDispatchToProps) @@ -60,6 +68,7 @@ function mapStateToProps (state) { tokenBalance: getTokenBalance(state), tokenContract: getSelectedTokenContract(state), tokenToFiatRate: getSelectedTokenToFiatRate(state), + qrCodeData: getQrCodeData(state), } } @@ -89,5 +98,8 @@ function mapDispatchToProps (dispatch) { }, updateSendErrors: newError => dispatch(updateSendErrors(newError)), resetSendState: () => dispatch(resetSendState()), + scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)), + qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), + updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), } } diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index cf07eafe1..ab3f6d34b 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -46,6 +46,7 @@ const selectors = { getTokenExchangeRate, getUnapprovedTxs, transactionsSelector, + getQrCodeData, } module.exports = selectors @@ -282,3 +283,7 @@ function transactionsSelector (state) { : txsToRender .sort((a, b) => b.time - a.time) } + +function getQrCodeData (state) { + return state.appState.qrCodeData +} diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js index 7a9120d24..57e332780 100644 --- a/ui/app/components/send/tests/send-container.test.js +++ b/ui/app/components/send/tests/send-container.test.js @@ -44,6 +44,7 @@ proxyquire('../send.container.js', { getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, getSendFromObject: (s) => `mockFrom:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`, + getQrCodeData: (s) => `mockQrCodeData:${s}`, }, '../../actions': actionSpies, '../../ducks/send.duck': duckActionSpies, @@ -76,6 +77,7 @@ describe('send container', () => { tokenBalance: 'mockTokenBalance:mockState', tokenContract: 'mockTokenContract:mockState', tokenToFiatRate: 'mockTokenToFiatRate:mockState', + qrCodeData: 'mockQrCodeData:mockState', }) }) diff --git a/ui/app/components/send/to-autocomplete/to-autocomplete.js b/ui/app/components/send/to-autocomplete/to-autocomplete.js index 80cfa7a85..49ebf49d9 100644 --- a/ui/app/components/send/to-autocomplete/to-autocomplete.js +++ b/ui/app/components/send/to-autocomplete/to-autocomplete.js @@ -4,6 +4,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const AccountListItem = require('../account-list-item/account-list-item.component').default const connect = require('react-redux').connect +const Tooltip = require('../../tooltip') ToAutoComplete.contextTypes = { t: PropTypes.func, @@ -94,11 +95,12 @@ ToAutoComplete.prototype.render = function () { dropdownOpen, onChange, inError, + qrScanner, } = this.props return h('div.send-v2__to-autocomplete', {}, [ - h('input.send-v2__to-autocomplete__input', { + h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, { placeholder: this.context.t('recipientAddress'), className: inError ? `send-v2__error-border` : '', value: to, @@ -108,7 +110,13 @@ ToAutoComplete.prototype.render = function () { borderColor: inError ? 'red' : null, }, }), - + qrScanner && h(Tooltip, { + title: this.context.t('scanQrCode'), + position: 'bottom', + }, h(`i.fa.fa-qrcode.fa-lg.send-v2__to-autocomplete__qr-code`, { + style: { color: '#33333' }, + onClick: () => this.props.scanQrCode(), + })), !to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, { style: { color: '#dedede' }, onClick: () => this.handleInputEvent(), diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 20c2be0f1..8e092364c 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -118,8 +118,18 @@ WalletView.prototype.render = function () { return kr.accounts.includes(selectedAddress) }) - const type = keyring.type - const isLoose = type !== 'HD Key Tree' + let label = '' + let type + if (keyring) { + type = keyring.type + if (type !== 'HD Key Tree') { + if (type.toLowerCase().search('hardware') !== -1) { + label = this.context.t('hardware') + } else { + label = this.context.t('imported') + } + } + } return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { style: {}, @@ -133,7 +143,7 @@ WalletView.prototype.render = function () { onClick: hideSidebar, }), - h('div.wallet-view__keyring-label.allcaps', isLoose ? this.context.t('imported') : ''), + h('div.wallet-view__keyring-label.allcaps', label), h('div.flex-column.flex-center.wallet-view__name-container', { style: { margin: '0 auto' }, diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index a7a226cc5..38f5f1c50 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -46,7 +46,7 @@ const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec']) // Setter Maps const toBigNumber = { hex: n => new BigNumber(stripHexPrefix(n), 16), - dec: n => new BigNumber(n, 10), + dec: n => new BigNumber(String(n), 10), BN: n => new BigNumber(n.toString(16), 16), } const toNormalizedDenomination = { @@ -154,7 +154,7 @@ const subtractCurrencies = (a, b, options = {}) => { bBase, ...conversionOptions } = options - const value = (new BigNumber(a, aBase)).minus(b, bBase) + const value = (new BigNumber(String(a), aBase)).minus(b, bBase) return converter({ value, diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 96ad5fe64..821a6b612 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -1,7 +1,5 @@ @import './buttons.scss'; -@import './header.scss'; - @import './footer.scss'; @import './network.scss'; diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index b12afb124..9c639e147 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -162,19 +162,99 @@ } .hw-connect { + width: 100%; + &__header { &__title { margin-top: 5px; margin-bottom: 15px; font-size: 22px; - text-align: center; } &__msg { font-size: 14px; color: #9b9b9b; margin-top: 10px; - margin-bottom: 0px; + margin-bottom: 20px; + } + } + + &__btn-wrapper { + flex: 1; + flex-direction: row; + display: flex; + } + + &__connect-btn { + background-color: #259De5; + color: #fff; + border: none; + width: 315px; + min-height: 54px; + font-weight: 300; + font-size: 14px; + margin-bottom: 20px; + margin-top: 20px; + border-radius: 5px; + display: flex; + flex: 1; + margin-left: 20px; + margin-right: 20px; + justify-content: center; + text-transform: uppercase; + } + + &__connect-btn.disabled { + cursor: not-allowed; + opacity: .5; + } + + &__btn { + background: #fbfbfb; + border: 1px solid #e5e5e5; + height: 100px; + width: 150px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + + &__img { + width: 95px; + } + } + + &__btn.selected { + border: 2px solid #00a8ee; + width: 149px; + } + + &__btn:first-child { + margin-right: 15px; + margin-left: 20px; + } + + &__btn:last-child { + margin-right: 20px; + } + + &__hdPath { + display: flex; + flex-direction: row; + margin-top: 15px; + margin-bottom: 30px; + font-size: 14px; + + &__title { + display: flex; + margin-top: 10px; + margin-right: 15px; + } + + &__select { + display: flex; + flex: 1; } } @@ -201,6 +281,13 @@ font-size: 18px; } + &__unlock-title { + padding-top: 10px; + font-weight: 400; + font-size: 22px; + margin-bottom: 15px; + } + &__msg { font-size: 14px; color: #9b9b9b; @@ -213,8 +300,6 @@ } &__footer { - width: 100%; - &__title { padding-top: 15px; padding-bottom: 12px; @@ -228,6 +313,9 @@ color: #9b9b9b; margin-top: 12px; margin-bottom: 27px; + width: 100%; + display: block; + margin-left: 20px; } &__link { @@ -236,10 +324,10 @@ } } - &__get-trezor { + &__get-hw { width: 100%; - padding-bottom: 20px; - padding-top: 20px; + padding-bottom: 10px; + padding-top: 10px; &__msg { font-size: 14px; @@ -361,6 +449,7 @@ margin-top: 10px; &__button { + background: #fff; height: 19px; display: flex; color: #33a4e7; @@ -390,6 +479,8 @@ &.account-list { height: auto; + padding-left: 20px; + padding-right: 20px; } &__buttons { @@ -412,6 +503,7 @@ min-height: 54px; font-weight: 300; font-size: 14px; + margin-bottom: 20px } &__button.unlock { diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index e9c872ea7..806ac8536 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -626,6 +626,23 @@ top: 18px; right: 12px; } + + &__qr-code { + position: absolute; + top: 13px; + right: 33px; + cursor: pointer; + padding: 8px 5px 5px; + border-radius: 4px; + } + + &__qr-code:hover { + background: #f1f1f1; + } + + &__input.with-qr { + padding-right: 65px; + } } &__to-autocomplete, &__memo-text-area, &__hex-data { diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js index a37778c19..76e80a8ac 100644 --- a/ui/app/helpers/confirm-transaction/util.js +++ b/ui/app/helpers/confirm-transaction/util.js @@ -141,7 +141,7 @@ export function hasUnconfirmedTransactions (state) { export function roundExponential (value) { const PRECISION = 4 - const bigNumberValue = new BigNumber(value) + const bigNumberValue = new BigNumber(String(value)) // In JS, numbers with exponentials greater than 20 get displayed as an exponential. return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index e4e4c8581..c246e7904 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -51,6 +51,7 @@ function reduceApp (state, action) { sidebarOpen: false, alertOpen: false, alertMessage: null, + qrCodeData: null, networkDropdownOpen: false, currentView: seedWords ? seedConfView : defaultView, accountDetail: { @@ -66,6 +67,10 @@ function reduceApp (state, action) { isMouseUser: false, gasIsLoading: false, networkNonce: null, + defaultHdPaths: { + trezor: `m/44'/60'/0'/0`, + ledger: `m/44'/60'/0'/0/0`, + }, }, state.appState) switch (action.type) { @@ -91,7 +96,7 @@ function reduceApp (state, action) { sidebarOpen: false, }) - // sidebar methods + // alert methods case actions.ALERT_OPEN: return extend(appState, { alertOpen: true, @@ -104,6 +109,13 @@ function reduceApp (state, action) { alertMessage: null, }) + // qr scanner methods + case actions.QR_CODE_DETECTED: + return extend(appState, { + qrCodeData: action.value, + }) + + // modal methods: case actions.MODAL_OPEN: const { name, ...modalProps } = action.payload @@ -517,6 +529,15 @@ function reduceApp (state, action) { warning: '', }) + case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH: + const { device, path } = action.value + const newDefaults = {...appState.defaultHdPaths} + newDefaults[device] = path + + return extend(appState, { + defaultHdPaths: newDefaults, + }) + case actions.SHOW_LOADING: return extend(appState, { isLoading: true, diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index aa1fc5404..6e760c429 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -159,7 +159,7 @@ export const approveTokenAmountAndToAddressSelector = createSelector( if (tokenDecimals) { tokenAmount = calcTokenAmount(value, tokenDecimals) } - + tokenAmount = roundExponential(tokenAmount) } diff --git a/ui/app/token-util.js b/ui/app/token-util.js index 0d4233766..8798ed266 100644 --- a/ui/app/token-util.js +++ b/ui/app/token-util.js @@ -44,7 +44,7 @@ async function getSymbolAndDecimals (tokenAddress, existingTokens = []) { function calcTokenAmount (value, decimals) { const multiplier = Math.pow(10, Number(decimals || 0)) - return new BigNumber(value).div(multiplier).toNumber() + return new BigNumber(String(value)).div(multiplier).toNumber() } diff --git a/ui/app/util.js b/ui/app/util.js index 8b194e0c7..ade4fec8a 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -271,9 +271,9 @@ function getContractAtAddress (tokenAddress) { return global.eth.contract(abi).at(tokenAddress) } -function exportAsFile (filename, data) { +function exportAsFile (filename, data, type = 'text/csv') { // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz - const blob = new Blob([data], {type: 'text/csv'}) + const blob = new Blob([data], {type}) if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(blob, filename) } else { diff --git a/ui/lib/webcam-utils.js b/ui/lib/webcam-utils.js new file mode 100644 index 000000000..eb717b23a --- /dev/null +++ b/ui/lib/webcam-utils.js @@ -0,0 +1,36 @@ +'use strict' + +import DetectRTC from 'detectrtc' +const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums') +const { getEnvironmentType, getPlatform } = require('../../app/scripts/lib/util') +const { PLATFORM_BRAVE, PLATFORM_FIREFOX } = require('../../app/scripts/lib/enums') + +class WebcamUtils { + + static checkStatus () { + return new Promise((resolve, reject) => { + const isPopup = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP + const isFirefoxOrBrave = getPlatform() === (PLATFORM_FIREFOX || PLATFORM_BRAVE) + try { + DetectRTC.load(_ => { + if (DetectRTC.hasWebcam) { + let environmentReady = true + if ((isFirefoxOrBrave && isPopup) || (isPopup && !DetectRTC.isWebsiteHasWebcamPermissions)) { + environmentReady = false + } + resolve({ + permissions: DetectRTC.isWebsiteHasWebcamPermissions, + environmentReady, + }) + } else { + reject({type: 'NO_WEBCAM_FOUND'}) + } + }) + } catch (e) { + reject({type: 'UNKNOWN_ERROR'}) + } + }) + } +} + +module.exports = WebcamUtils |