|
|
|
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 (
|
|
|
|
<>
|
|
|
|
<div className="qr-scanner__image">
|
|
|
|
<img src="images/webcam.svg" width="70" height="70" alt="" />
|
|
|
|
</div>
|
|
|
|
{title ? <div className="qr-scanner__title">{title}</div> : null}
|
|
|
|
<div className="qr-scanner__error">{msg}</div>
|
|
|
|
<PageContainerFooter
|
|
|
|
onCancel={this.stopAndClose}
|
|
|
|
onSubmit={this.tryAgain}
|
|
|
|
cancelText={t('cancel')}
|
|
|
|
submitText={t('tryAgain')}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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 (
|
|
|
|
<>
|
|
|
|
<div className="qr-scanner__title">{`${t('scanQrCode')}`}</div>
|
|
|
|
<div className="qr-scanner__content">
|
|
|
|
<div className="qr-scanner__content__video-wrapper">
|
|
|
|
<video
|
|
|
|
id="video"
|
|
|
|
style={{
|
|
|
|
display: ready === READY_STATE.READY ? 'block' : 'none',
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{ready === READY_STATE.READY ? null : <Spinner color="#F7C06C" />}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="qr-scanner__status">{message}</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const { error } = this.state;
|
|
|
|
return (
|
|
|
|
<div className="qr-scanner">
|
|
|
|
<div className="qr-scanner__close" onClick={this.stopAndClose}></div>
|
|
|
|
{error ? this.renderError() : this.renderVideo()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|