1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-26 20:39:08 +01:00
metamask-extension/ui/components/app/qr-hardware-popover/base-reader.js
Aaron Chen a931316a53
Introduce QR based signer into MetaMask (#12065)
* support qr based signer

* add CSP for fire fox

* get QR Hardware wallet name from device

* fix qrHardware state missing in runtime

* support qr based signer sign transaction

* refine Request Signature modal ui

* remove feature toggle

* refine ui

* fix notification is closing even there is a pending qr hardware transaction

* add chinese translation, refine ui, fix qr process was breaking in some case

* support import accounts by pubkeys

* refine qr-based wallet ui and fix bugs

* update @keystonehq/metamask-airgapped-keyring to fix that the signing hd path was inconsistent in some edge case

* fix: avoid unnecessay navigation, fix ci

* refactor qr-hardware-popover with @zxing/browser

* update lavamoat policy, remove firefox CSP

* refine qr reader ui, ignore unnecessary warning display

* code refactor, use async functions insteads promise

Co-authored-by: Soralit <soralitria@gmail.com>
2021-11-23 13:58:39 -03:30

218 lines
6.1 KiB
JavaScript

import React, { useEffect, useRef, useState } from 'react';
import log from 'loglevel';
import { URDecoder } from '@ngraveio/bc-ur';
import PropTypes from 'prop-types';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
import WebcamUtils from '../../../helpers/utils/webcam-utils';
import PageContainerFooter from '../../ui/page-container/page-container-footer/page-container-footer.component';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { SECOND } from '../../../../shared/constants/time';
import EnhancedReader from './enhanced-reader';
const READY_STATE = {
ACCESSING_CAMERA: 'ACCESSING_CAMERA',
NEED_TO_ALLOW_ACCESS: 'NEED_TO_ALLOW_ACCESS',
READY: 'READY',
};
const BaseReader = ({
isReadingWallet,
handleCancel,
handleSuccess,
setErrorTitle,
}) => {
const t = useI18nContext();
const [ready, setReady] = useState(READY_STATE.ACCESSING_CAMERA);
const [error, setError] = useState(null);
const [urDecoder, setURDecoder] = useState(new URDecoder());
let permissionChecker = null;
const mounted = useRef(false);
const reset = () => {
setReady(READY_STATE.ACCESSING_CAMERA);
setError(null);
setURDecoder(new URDecoder());
};
const checkEnvironment = async () => {
try {
const { environmentReady } = await WebcamUtils.checkStatus();
if (
!environmentReady &&
getEnvironmentType() !== ENVIRONMENT_TYPE_FULLSCREEN
) {
const currentUrl = new URL(window.location.href);
const currentHash = currentUrl.hash;
const currentRoute = currentHash ? currentHash.substring(1) : null;
global.platform.openExtensionInBrowser(currentRoute);
}
} catch (e) {
if (mounted.current) {
setError(e);
}
}
// initial attempt is required to trigger permission prompt
// eslint-disable-next-line no-use-before-define
return initCamera();
};
const 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 (!mounted.current) {
return;
}
setReady(READY_STATE.READY);
} else if (mounted.current) {
// Keep checking for permissions
permissionChecker = setTimeout(checkPermissions, SECOND);
setReady(READY_STATE.NEED_TO_ALLOW_ACCESS);
}
} catch (e) {
if (mounted.current) {
setError(e);
}
}
};
const handleScan = (data) => {
try {
if (!data) {
return;
}
urDecoder.receivePart(data);
if (urDecoder.isComplete()) {
const result = urDecoder.resultUR();
handleSuccess(result).catch(setError);
}
} catch (e) {
if (isReadingWallet) {
setErrorTitle(t('QRHardwareUnknownQRCodeTitle'));
} else {
setErrorTitle(t('QRHardwareInvalidTransactionTitle'));
}
setError(new Error(t('unknownQrCode')));
}
};
const initCamera = () => {
try {
checkPermissions();
} catch (e) {
if (!mounted.current) {
return;
}
if (e.name === 'NotAllowedError') {
log.info(`Permission denied: '${e}'`);
setReady(READY_STATE.NEED_TO_ALLOW_ACCESS);
} else {
setError(e);
}
}
};
useEffect(() => {
mounted.current = true;
checkEnvironment();
return () => {
mounted.current = false;
clearTimeout(permissionChecker);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (ready === READY_STATE.READY) {
initCamera();
} else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) {
checkPermissions();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ready]);
const tryAgain = () => {
clearTimeout(permissionChecker);
reset();
checkEnvironment();
};
const renderError = () => {
let title, msg;
if (error.type === 'NO_WEBCAM_FOUND') {
title = t('noWebcamFoundTitle');
msg = t('noWebcamFound');
} else if (error.message === t('unknownQrCode')) {
if (isReadingWallet) {
msg = t('QRHardwareUnknownWalletQRCode');
} else {
msg = t('unknownQrCode');
}
} else if (error.message === t('QRHardwareMismatchedSignId')) {
msg = t('QRHardwareMismatchedSignId');
} 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={() => {
setErrorTitle('');
handleCancel();
}}
onSubmit={() => {
setErrorTitle('');
tryAgain();
}}
cancelText={t('cancel')}
submitText={t('tryAgain')}
submitButtonType="confirm"
/>
</>
);
};
const renderVideo = () => {
let message;
if (ready === READY_STATE.ACCESSING_CAMERA) {
message = t('accessingYourCamera');
} else if (ready === READY_STATE.READY) {
message = t('QRHardwareScanInstructions');
} else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) {
message = t('youNeedToAllowCameraAccess');
}
return (
<>
<div className="qr-scanner__content">
<EnhancedReader handleScan={handleScan} />
</div>
{message && <div className="qr-scanner__status">{message}</div>}
</>
);
};
return (
<div className="qr-scanner">{error ? renderError() : renderVideo()}</div>
);
};
BaseReader.propTypes = {
isReadingWallet: PropTypes.bool.isRequired,
handleCancel: PropTypes.func.isRequired,
handleSuccess: PropTypes.func.isRequired,
setErrorTitle: PropTypes.func.isRequired,
};
export default BaseReader;