You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
7.5 KiB
270 lines
7.5 KiB
4 years ago
|
import React, { Component } from 'react';
|
||
|
import PropTypes from 'prop-types';
|
||
|
import log from 'loglevel';
|
||
|
import { BrowserQRCodeReader } from '@zxing/library';
|
||
4 years ago
|
import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
|
||
|
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
|
||
4 years ago
|
import Spinner from '../../../ui/spinner';
|
||
4 years ago
|
import WebcamUtils from '../../../../helpers/utils/webcam-utils';
|
||
4 years ago
|
import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component';
|
||
6 years ago
|
|
||
5 years ago
|
const READY_STATE = {
|
||
|
ACCESSING_CAMERA: 'ACCESSING_CAMERA',
|
||
|
NEED_TO_ALLOW_ACCESS: 'NEED_TO_ALLOW_ACCESS',
|
||
|
READY: 'READY',
|
||
4 years ago
|
};
|
||
5 years ago
|
|
||
6 years ago
|
export default class QrScanner extends Component {
|
||
|
static propTypes = {
|
||
|
hideModal: PropTypes.func.isRequired,
|
||
5 years ago
|
qrCodeDetected: PropTypes.func.isRequired,
|
||
4 years ago
|
};
|
||
6 years ago
|
|
||
|
static contextTypes = {
|
||
|
t: PropTypes.func,
|
||
4 years ago
|
};
|
||
6 years ago
|
|
||
4 years ago
|
constructor(props) {
|
||
4 years ago
|
super(props);
|
||
6 years ago
|
|
||
4 years ago
|
this.state = this.getInitialState();
|
||
|
this.codeReader = null;
|
||
|
this.permissionChecker = null;
|
||
|
this.mounted = false;
|
||
6 years ago
|
|
||
|
// Clear pre-existing qr code data before scanning
|
||
4 years ago
|
this.props.qrCodeDetected(null);
|
||
6 years ago
|
}
|
||
|
|
||
4 years ago
|
componentDidMount() {
|
||
4 years ago
|
this.mounted = true;
|
||
|
this.checkEnvironment();
|
||
5 years ago
|
}
|
||
|
|
||
4 years ago
|
componentDidUpdate(_, prevState) {
|
||
4 years ago
|
const { ready } = this.state;
|
||
5 years ago
|
|
||
|
if (prevState.ready !== ready) {
|
||
|
if (ready === READY_STATE.READY) {
|
||
4 years ago
|
this.initCamera();
|
||
5 years ago
|
} else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) {
|
||
4 years ago
|
this.checkPermissions();
|
||
5 years ago
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
4 years ago
|
getInitialState() {
|
||
5 years ago
|
return {
|
||
|
ready: READY_STATE.ACCESSING_CAMERA,
|
||
|
error: null,
|
||
4 years ago
|
};
|
||
5 years ago
|
}
|
||
|
|
||
|
checkEnvironment = async () => {
|
||
|
try {
|
||
4 years ago
|
const { environmentReady } = await WebcamUtils.checkStatus();
|
||
4 years ago
|
if (
|
||
|
!environmentReady &&
|
||
|
getEnvironmentType() !== ENVIRONMENT_TYPE_FULLSCREEN
|
||
|
) {
|
||
4 years ago
|
const currentUrl = new URL(window.location.href);
|
||
|
const currentHash = currentUrl.hash;
|
||
|
const currentRoute = currentHash ? currentHash.substring(1) : null;
|
||
|
global.platform.openExtensionInBrowser(currentRoute);
|
||
5 years ago
|
}
|
||
|
} catch (error) {
|
||
|
if (this.mounted) {
|
||
4 years ago
|
this.setState({ error });
|
||
5 years ago
|
}
|
||
|
}
|
||
|
// initial attempt is required to trigger permission prompt
|
||
4 years ago
|
this.initCamera();
|
||
|
};
|
||
6 years ago
|
|
||
5 years ago
|
checkPermissions = async () => {
|
||
|
try {
|
||
4 years ago
|
const { permissions } = await WebcamUtils.checkStatus();
|
||
5 years ago
|
if (permissions) {
|
||
|
// Let the video stream load first...
|
||
4 years ago
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||
5 years ago
|
if (!this.mounted) {
|
||
4 years ago
|
return;
|
||
6 years ago
|
}
|
||
4 years ago
|
this.setState({ ready: READY_STATE.READY });
|
||
5 years ago
|
} else if (this.mounted) {
|
||
|
// Keep checking for permissions
|
||
4 years ago
|
this.permissionChecker = setTimeout(this.checkPermissions, 1000);
|
||
5 years ago
|
}
|
||
|
} catch (error) {
|
||
|
if (this.mounted) {
|
||
4 years ago
|
this.setState({ error });
|
||
5 years ago
|
}
|
||
6 years ago
|
}
|
||
4 years ago
|
};
|
||
6 years ago
|
|
||
4 years ago
|
componentWillUnmount() {
|
||
4 years ago
|
this.mounted = false;
|
||
|
clearTimeout(this.permissionChecker);
|
||
|
this.teardownCodeReader();
|
||
4 years ago
|
}
|
||
|
|
||
4 years ago
|
teardownCodeReader() {
|
||
4 years ago
|
if (this.codeReader) {
|
||
4 years ago
|
this.codeReader.reset();
|
||
|
this.codeReader.stop();
|
||
|
this.codeReader = null;
|
||
6 years ago
|
}
|
||
6 years ago
|
}
|
||
|
|
||
5 years ago
|
initCamera = async () => {
|
||
4 years ago
|
// 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.
|
||
4 years ago
|
// It's important to prevent this codeReader from being created twice;
|
||
|
// Firefox otherwise starts 2 video streams, one of which cannot be stopped
|
||
4 years ago
|
if (!this.codeReader) {
|
||
4 years ago
|
this.codeReader = new BrowserQRCodeReader();
|
||
4 years ago
|
}
|
||
5 years ago
|
try {
|
||
4 years ago
|
await this.codeReader.getVideoInputDevices();
|
||
|
this.checkPermissions();
|
||
4 years ago
|
const content = await this.codeReader.decodeFromInputVideoDevice(
|
||
|
undefined,
|
||
|
'video',
|
||
4 years ago
|
);
|
||
|
const result = this.parseContent(content.text);
|
||
5 years ago
|
if (!this.mounted) {
|
||
4 years ago
|
return;
|
||
4 years ago
|
} else if (result.type === 'unknown') {
|
||
4 years ago
|
this.setState({ error: new Error(this.context.t('unknownQrCode')) });
|
||
4 years ago
|
} else {
|
||
4 years ago
|
this.props.qrCodeDetected(result);
|
||
|
this.stopAndClose();
|
||
5 years ago
|
}
|
||
|
} catch (error) {
|
||
|
if (!this.mounted) {
|
||
4 years ago
|
return;
|
||
5 years ago
|
}
|
||
|
if (error.name === 'NotAllowedError') {
|
||
4 years ago
|
log.info(`Permission denied: '${error}'`);
|
||
|
this.setState({ ready: READY_STATE.NEED_TO_ALLOW_ACCESS });
|
||
5 years ago
|
} else {
|
||
4 years ago
|
this.setState({ error });
|
||
5 years ago
|
}
|
||
|
}
|
||
4 years ago
|
};
|
||
6 years ago
|
|
||
4 years ago
|
parseContent(content) {
|
||
4 years ago
|
let type = 'unknown';
|
||
|
let values = {};
|
||
6 years ago
|
|
||
|
// Here we could add more cases
|
||
6 years ago
|
// To parse other type of links
|
||
|
// For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681)
|
||
6 years ago
|
|
||
6 years ago
|
// Ethereum address links - fox ex. ethereum:0x.....1111
|
||
6 years ago
|
if (content.split('ethereum:').length > 1) {
|
||
4 years ago
|
type = 'address';
|
||
|
values = { address: content.split('ethereum:')[1] };
|
||
6 years ago
|
|
||
4 years ago
|
// Regular ethereum addresses - fox ex. 0x.....1111
|
||
6 years ago
|
} else if (content.substring(0, 2).toLowerCase() === '0x') {
|
||
4 years ago
|
type = 'address';
|
||
|
values = { address: content };
|
||
6 years ago
|
}
|
||
4 years ago
|
return { type, values };
|
||
6 years ago
|
}
|
||
|
|
||
|
stopAndClose = () => {
|
||
6 years ago
|
if (this.codeReader) {
|
||
4 years ago
|
this.teardownCodeReader();
|
||
6 years ago
|
}
|
||
4 years ago
|
this.props.hideModal();
|
||
|
};
|
||
6 years ago
|
|
||
6 years ago
|
tryAgain = () => {
|
||
4 years ago
|
clearTimeout(this.permissionChecker);
|
||
5 years ago
|
if (this.codeReader) {
|
||
4 years ago
|
this.teardownCodeReader();
|
||
5 years ago
|
}
|
||
|
this.setState(this.getInitialState(), () => {
|
||
4 years ago
|
this.checkEnvironment();
|
||
|
});
|
||
|
};
|
||
6 years ago
|
|
||
4 years ago
|
renderError() {
|
||
4 years ago
|
const { t } = this.context;
|
||
|
const { error } = this.state;
|
||
6 years ago
|
|
||
4 years ago
|
let title, msg;
|
||
5 years ago
|
if (error.type === 'NO_WEBCAM_FOUND') {
|
||
4 years ago
|
title = t('noWebcamFoundTitle');
|
||
|
msg = t('noWebcamFound');
|
||
5 years ago
|
} else if (error.message === t('unknownQrCode')) {
|
||
4 years ago
|
msg = t('unknownQrCode');
|
||
5 years ago
|
} else {
|
||
4 years ago
|
title = t('unknownCameraErrorTitle');
|
||
|
msg = t('unknownCameraError');
|
||
6 years ago
|
}
|
||
|
|
||
|
return (
|
||
5 years ago
|
<>
|
||
6 years ago
|
<div className="qr-scanner__image">
|
||
4 years ago
|
<img src="images/webcam.svg" width="70" height="70" alt="" />
|
||
6 years ago
|
</div>
|
||
4 years ago
|
{title ? <div className="qr-scanner__title">{title}</div> : null}
|
||
|
<div className="qr-scanner__error">{msg}</div>
|
||
6 years ago
|
<PageContainerFooter
|
||
|
onCancel={this.stopAndClose}
|
||
|
onSubmit={this.tryAgain}
|
||
5 years ago
|
cancelText={t('cancel')}
|
||
|
submitText={t('tryAgain')}
|
||
6 years ago
|
submitButtonType="confirm"
|
||
6 years ago
|
/>
|
||
5 years ago
|
</>
|
||
4 years ago
|
);
|
||
6 years ago
|
}
|
||
|
|
||
4 years ago
|
renderVideo() {
|
||
4 years ago
|
const { t } = this.context;
|
||
|
const { ready } = this.state;
|
||
6 years ago
|
|
||
4 years ago
|
let message;
|
||
5 years ago
|
if (ready === READY_STATE.ACCESSING_CAMERA) {
|
||
4 years ago
|
message = t('accessingYourCamera');
|
||
5 years ago
|
} else if (ready === READY_STATE.READY) {
|
||
4 years ago
|
message = t('scanInstructions');
|
||
5 years ago
|
} else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) {
|
||
4 years ago
|
message = t('youNeedToAllowCameraAccess');
|
||
6 years ago
|
}
|
||
|
|
||
6 years ago
|
return (
|
||
5 years ago
|
<>
|
||
4 years ago
|
<div className="qr-scanner__title">{`${t('scanQrCode')}`}</div>
|
||
6 years ago
|
<div className="qr-scanner__content">
|
||
5 years ago
|
<div className="qr-scanner__content__video-wrapper">
|
||
|
<video
|
||
|
id="video"
|
||
|
style={{
|
||
|
display: ready === READY_STATE.READY ? 'block' : 'none',
|
||
|
}}
|
||
|
/>
|
||
4 years ago
|
{ready === READY_STATE.READY ? null : <Spinner color="#F7C06C" />}
|
||
5 years ago
|
</div>
|
||
6 years ago
|
</div>
|
||
4 years ago
|
<div className="qr-scanner__status">{message}</div>
|
||
5 years ago
|
</>
|
||
4 years ago
|
);
|
||
5 years ago
|
}
|
||
|
|
||
4 years ago
|
render() {
|
||
4 years ago
|
const { error } = this.state;
|
||
5 years ago
|
return (
|
||
|
<div className="qr-scanner">
|
||
|
<div className="qr-scanner__close" onClick={this.stopAndClose}></div>
|
||
4 years ago
|
{error ? this.renderError() : this.renderVideo()}
|
||
6 years ago
|
</div>
|
||
4 years ago
|
);
|
||
6 years ago
|
}
|
||
|
}
|