import React, { Component } from 'react'; import PropTypes from 'prop-types'; import log from 'loglevel'; import { BrowserQRCodeReader } from '@zxing/library'; import { getEnvironmentType } from '../../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app'; import { SECOND } from '../../../../../shared/constants/time'; import Spinner from '../../../ui/spinner'; import WebcamUtils from '../../../../helpers/utils/webcam-utils'; import { getURL } from '../../../../helpers/utils/util'; import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component'; const READY_STATE = { ACCESSING_CAMERA: 'ACCESSING_CAMERA', NEED_TO_ALLOW_ACCESS: 'NEED_TO_ALLOW_ACCESS', READY: 'READY', }; export default class QrScanner extends Component { static propTypes = { hideModal: PropTypes.func.isRequired, qrCodeDetected: PropTypes.func.isRequired, }; static contextTypes = { t: PropTypes.func, }; constructor(props) { super(props); this.state = this.getInitialState(); this.codeReader = null; this.permissionChecker = null; this.mounted = false; // Clear pre-existing qr code data before scanning this.props.qrCodeDetected(null); } componentDidMount() { this.mounted = true; this.checkEnvironment(); } componentDidUpdate(_, prevState) { const { ready } = this.state; if (prevState.ready !== ready) { if (ready === READY_STATE.READY) { this.initCamera(); } else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) { this.checkPermissions(); } } } getInitialState() { return { ready: READY_STATE.ACCESSING_CAMERA, error: null, }; } checkEnvironment = async () => { try { const { environmentReady } = await WebcamUtils.checkStatus(); if ( !environmentReady && getEnvironmentType() !== ENVIRONMENT_TYPE_FULLSCREEN ) { const currentUrl = getURL(window.location.href); const currentHash = currentUrl?.hash; const currentRoute = currentHash ? currentHash.substring(1) : null; global.platform.openExtensionInBrowser(currentRoute); } } catch (error) { if (this.mounted) { this.setState({ error }); } } // initial attempt is required to trigger permission prompt this.initCamera(); }; checkPermissions = async () => { try { const { permissions } = await WebcamUtils.checkStatus(); if (permissions) { // Let the video stream load first... await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); if (!this.mounted) { return; } this.setState({ ready: READY_STATE.READY }); } else if (this.mounted) { // Keep checking for permissions this.permissionChecker = setTimeout(this.checkPermissions, SECOND); } } catch (error) { if (this.mounted) { this.setState({ error }); } } }; componentWillUnmount() { this.mounted = false; clearTimeout(this.permissionChecker); this.teardownCodeReader(); } teardownCodeReader() { if (this.codeReader) { this.codeReader.reset(); this.codeReader.stop(); this.codeReader = null; } } initCamera = async () => { // The `decodeFromInputVideoDevice` call prompts the browser to show // the user the camera permission request. We must then call it again // once we receive permission so that the video displays. // It's important to prevent this codeReader from being created twice; // Firefox otherwise starts 2 video streams, one of which cannot be stopped if (!this.codeReader) { this.codeReader = new BrowserQRCodeReader(); } try { await this.codeReader.getVideoInputDevices(); this.checkPermissions(); const content = await this.codeReader.decodeFromInputVideoDevice( undefined, 'video', ); const result = this.parseContent(content.text); if (!this.mounted) { return; } else if (result.type === 'unknown') { this.setState({ error: new Error(this.context.t('unknownQrCode')) }); } else { this.props.qrCodeDetected(result); this.stopAndClose(); } } catch (error) { if (!this.mounted) { return; } if (error.name === 'NotAllowedError') { log.info(`Permission denied: '${error}'`); this.setState({ ready: READY_STATE.NEED_TO_ALLOW_ACCESS }); } else { this.setState({ error }); } } }; 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.teardownCodeReader(); } this.props.hideModal(); }; tryAgain = () => { clearTimeout(this.permissionChecker); if (this.codeReader) { this.teardownCodeReader(); } this.setState(this.getInitialState(), () => { this.checkEnvironment(); }); }; renderError() { const { t } = this.context; const { error } = this.state; let title, msg; if (error.type === 'NO_WEBCAM_FOUND') { title = t('noWebcamFoundTitle'); msg = t('noWebcamFound'); } else if (error.message === t('unknownQrCode')) { msg = t('unknownQrCode'); } else { title = t('unknownCameraErrorTitle'); msg = t('unknownCameraError'); } return ( <>
{title ?
{title}
: null}
{msg}
); } renderVideo() { const { t } = this.context; const { ready } = this.state; let message; if (ready === READY_STATE.ACCESSING_CAMERA) { message = t('accessingYourCamera'); } else if (ready === READY_STATE.READY) { message = t('scanInstructions'); } else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) { message = t('youNeedToAllowCameraAccess'); } return ( <>
{`${t('scanQrCode')}`}
{message}
); } render() { const { error } = this.state; return (
{error ? this.renderError() : this.renderVideo()}
); } }