From 710b4e294f66fe6e623fa145cc99be5b79c8210e Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 31 Jul 2018 18:30:40 -0400 Subject: [PATCH] added crossbrowser support and error handling --- app/_locales/en/messages.json | 6 + package-lock.json | 29 +++-- package.json | 1 + ui/app/actions.js | 26 ++++- .../components/modals/qr-scanner/index.scss | 5 + .../modals/qr-scanner/qr-scanner.component.js | 108 ++++++++++-------- .../modals/qr-scanner/qr-scanner.container.js | 9 +- ui/app/components/send/send.component.js | 5 + ui/app/components/send/send.container.js | 8 +- ui/lib/webcam-utils.js | 38 ++++++ 10 files changed, 179 insertions(+), 56 deletions(-) create mode 100644 ui/lib/webcam-utils.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 60e873371..ec96f5b08 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -656,6 +656,9 @@ "notStarted": { "message": "Not Started" }, + "noWebcamFound": { + "message": "We couldn't find any webcam available on your computer. Make sure the device is connected and configured correctly." + }, "oldUI": { "message": "Old UI" }, @@ -1098,6 +1101,9 @@ "unknownQrCode": { "message": "Error: We couldn't identify that QR code" }, + "unknownCameraError": { + "message": "Ooops! Something went wrong while trying to access you camera. Please try again..." + }, "unlock": { "message": "Unlock" }, diff --git a/package-lock.json b/package-lock.json index 3a4007048..3b99e2487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7147,6 +7147,11 @@ } } }, + "detectrtc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/detectrtc/-/detectrtc-1.3.6.tgz", + "integrity": "sha1-2rwDU5gaPadzLelpBxwItt3dW1k=" + }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -8406,12 +8411,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.0.1.tgz", "integrity": "sha512-lxHZOQspexk3DaGj4RBbWy4C/qNOWRnxpaJzNnYD3WEmC8shcJ4tHs7Xv878rzvILfJnSFSCCiKQhng1m80oBQ==", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -8689,12 +8695,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -8736,12 +8743,14 @@ "integrity": "sha512-lxHZOQspexk3DaGj4RBbWy4C/qNOWRnxpaJzNnYD3WEmC8shcJ4tHs7Xv878rzvILfJnSFSCCiKQhng1m80oBQ==", "dev": true, "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "dev": true, "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -8753,6 +8762,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "dev": true, "requires": { "bn.js": "^4.11.0", "create-hash": "^1.1.2", @@ -30717,6 +30727,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, "requires": { "is-typedarray": "^1.0.0" } @@ -31740,6 +31751,7 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.20.3.tgz", "integrity": "sha1-yqRDc9yIFayHZ73ba6cwc5ZMqos=", "requires": { + "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xhr2": "*", @@ -31748,7 +31760,7 @@ "dependencies": { "bignumber.js": { "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", - "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" + "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git" } } }, @@ -32247,7 +32259,8 @@ "dev": true, "requires": { "underscore": "1.8.3", - "web3-core-helpers": "1.0.0-beta.34" + "web3-core-helpers": "1.0.0-beta.34", + "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" }, "dependencies": { "underscore": { @@ -32258,7 +32271,8 @@ }, "websocket": { "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", - "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", + "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible", + "dev": true, "requires": { "debug": "^2.2.0", "nan": "^2.3.3", @@ -33623,7 +33637,8 @@ "yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", + "dev": true }, "yallist": { "version": "2.1.2", diff --git a/package.json b/package.json index 55f99dca8..a39534983 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "debounce-stream": "^2.0.0", "deep-extend": "^0.5.1", "detect-node": "^2.0.3", + "detectrtc": "^1.3.6", "disc": "^1.3.2", "dnode": "^1.2.2", "end-of-stream": "^1.1.0", diff --git a/ui/app/actions.js b/ui/app/actions.js index 0eb47edba..3540f61a5 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, @@ -127,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', @@ -723,6 +725,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()) diff --git a/ui/app/components/modals/qr-scanner/index.scss b/ui/app/components/modals/qr-scanner/index.scss index 314e94069..df65cfbbb 100644 --- a/ui/app/components/modals/qr-scanner/index.scss +++ b/ui/app/components/modals/qr-scanner/index.scss @@ -38,5 +38,10 @@ font-size: 14px; padding: 15px; } + + &__status.error { + padding: 60px 45px 80px; + font-size: 16px; + } } diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/modals/qr-scanner/qr-scanner.component.js index 395008fca..29ce45184 100644 --- a/ui/app/components/modals/qr-scanner/qr-scanner.component.js +++ b/ui/app/components/modals/qr-scanner/qr-scanner.component.js @@ -3,16 +3,14 @@ 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' -const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums') -const { getEnvironmentType } = require('../../../../../app/scripts/lib/util') -const { - SEND_ROUTE, -} = require('../../../routes') +import WebcamUtils from '../../../../lib/webcam-utils' export default class QrScanner extends Component { static propTypes = { hideModal: PropTypes.func.isRequired, qrCodeDetected: PropTypes.func, + error: PropTypes.bool, + errorType: PropTypes.string, } static contextTypes = { @@ -21,46 +19,65 @@ export default class QrScanner extends Component { constructor (props, context) { super(props) + + let initialMsg = context.t('accessingYourCamera') + if (props.error) { + if (props.errorType === 'NO_WEBCAM_FOUND') { + initialMsg = context.t('noWebcamFound') + } else { + initialMsg = context.t('unknownCameraError') + } + } + this.state = { ready: false, - msg: context.t('accessingYourCamera'), + msg: initialMsg, } - this.scanning = false this.codeReader = null + this.permissionChecker = null this.notAllowed = false } componentDidMount () { + this.initCamera() + } - if (!this.scanning) { - this.scanning = true - - 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'), + }) + }, 2000) + + } else { + // Keep checking for permissions + this.permissionChecker = setTimeout(_ => { + console.log('[QR-SCANNER]: time to check again!') + this.checkPermisisions() + }, 1000) } } componentWillUnmount () { - this.codeReader.reset() + clearTimeout(this.permissionChecker) + if (this.codeReader) { + this.codeReader.reset() + } } initCamera () { - this.codeReader = new BrowserQRCodeReader() this.codeReader.getVideoInputDevices() .then(videoInputDevices => { - - setTimeout(_ => { - if (!this.notAllowed) { - this.setState({ - ready: true, - msg: this.context.t('scanInstructions')}) - } - }, 2000) - - - this.codeReader.decodeFromInputVideoDevice(videoInputDevices[0].deviceId, 'video') + 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) @@ -70,18 +87,14 @@ export default class QrScanner extends Component { } }) .catch(err => { - this.notAllowed = true if (err && err.name === 'NotAllowedError') { - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser(`${SEND_ROUTE}`, `scan=true`) - } else { - this.setState({msg: this.context.t('youNeedToAllowCameraAccess')}) - } + this.setState({msg: this.context.t('youNeedToAllowCameraAccess')}) + clearTimeout(this.permissionChecker) + this.checkPermisisions() } - console.error('QR-SCANNER: decodeFromInputVideoDevice threw an exception: ', err) }) }).catch(err => { - console.error('QR-SCANNER: getVideoInputDevices threw an exception: ', err) + console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err) }) } @@ -103,31 +116,36 @@ export default class QrScanner extends Component { stopAndClose = () => { this.codeReader.reset() - this.scanning = false this.setState({ ready: false }) this.props.hideModal() } + renderVideo () { + return ( +
+
+ ) + } + render () { const { t } = this.context return (
- { `${t('scanQrCode')}?` } + { `${t('scanQrCode')}` }
-
-
+ { !this.props.error ? this.renderVideo() : null}
-
+
{this.state.msg}
diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.container.js b/ui/app/components/modals/qr-scanner/qr-scanner.container.js index 198d5ff81..d50abe0ae 100644 --- a/ui/app/components/modals/qr-scanner/qr-scanner.container.js +++ b/ui/app/components/modals/qr-scanner/qr-scanner.container.js @@ -3,6 +3,13 @@ import QrScanner from './qr-scanner.component' const { hideModal, qrCodeDetected } = require('../../../actions') +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()), @@ -10,4 +17,4 @@ const mapDispatchToProps = dispatch => { } } -export default connect(null, mapDispatchToProps)(QrScanner) +export default connect(mapStateToProps, mapDispatchToProps)(QrScanner) diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js index 8305a288e..80b50fad4 100644 --- a/ui/app/components/send/send.component.js +++ b/ui/app/components/send/send.component.js @@ -179,6 +179,11 @@ export default class SendTransactionScreen extends PersistentForm { // 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' } } diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js index 1fb771974..25abb706c 100644 --- a/ui/app/components/send/send.container.js +++ b/ui/app/components/send/send.container.js @@ -28,7 +28,7 @@ import { updateSendTokenBalance, updateGasData, setGasTotal, - showModal, + showQrScanner, } from '../../actions' import { resetSendState, @@ -38,6 +38,10 @@ import { calcGasTotal, } from './send.utils.js' +import { + SEND_ROUTE, +} from '../../routes' + module.exports = compose( withRouter, connect(mapStateToProps, mapDispatchToProps) @@ -93,7 +97,7 @@ function mapDispatchToProps (dispatch) { }, updateSendErrors: newError => dispatch(updateSendErrors(newError)), resetSendState: () => dispatch(resetSendState()), - scanQrCode: () => dispatch(showModal({ name: 'QR_SCANNER' })), + scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)), updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), } } diff --git a/ui/lib/webcam-utils.js b/ui/lib/webcam-utils.js new file mode 100644 index 000000000..e4261dfbc --- /dev/null +++ b/ui/lib/webcam-utils.js @@ -0,0 +1,38 @@ +'use strict' + +import DetectRTC from 'detectrtc' +const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums') +const { getEnvironmentType } = require('../../app/scripts/lib/util') + +class WebcamUtils { + + static checkStatus () { + return new Promise((resolve, reject) => { + const isPopup = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP + const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 + const isBrave = !!window.chrome.ipcRenderer + const isFirefoxOrBrave = isFirefox || isBrave + try { + reject({type: 'NO_WEBCAM_FOUND'}) + // 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