Merge pull request #4897 from MetaMask/qr-code-scan
QR Code Scannerfeature/default_network_editable
commit
e2be22a4b7
After Width: | Height: | Size: 1020 B |
@ -0,0 +1,2 @@ |
|||||||
|
import QrScanner from './qr-scanner.container' |
||||||
|
module.exports = QrScanner |
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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) |
@ -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 |
Loading…
Reference in new issue