101 lines
2.6 KiB
TypeScript
101 lines
2.6 KiB
TypeScript
// Copyright 2017-2025 @polkadot/react-qr authors & contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import { BrowserQRCodeReader, type IScannerControls } from '@zxing/browser';
|
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
|
|
import { styled } from './styled.js';
|
|
import { createImgSize } from './util.js';
|
|
|
|
interface Props {
|
|
className?: string | undefined;
|
|
delay?: number;
|
|
onError?: undefined | ((error: Error) => void);
|
|
onScan: (data: string) => void;
|
|
size?: string | number | undefined;
|
|
style?: React.CSSProperties | undefined;
|
|
}
|
|
|
|
const DEFAULT_DELAY = 150;
|
|
|
|
const DEFAULT_ERROR = (error: Error): void => {
|
|
console.error('@polkadot/react-qr:Scan', error.message);
|
|
};
|
|
|
|
function Scan ({ className = '', delay = DEFAULT_DELAY, onError = DEFAULT_ERROR, onScan, size, style = {} }: Props): React.ReactElement<Props> {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const controlsRef = useRef<IScannerControls | null>(null);
|
|
|
|
const containerStyle = useMemo(
|
|
() => createImgSize(size),
|
|
[size]
|
|
);
|
|
|
|
const _onError = useCallback(
|
|
(error: Error) => onError(error),
|
|
[onError]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const codeReader = new BrowserQRCodeReader();
|
|
|
|
const startScanning = async () => {
|
|
try {
|
|
const videoInputDevices = await BrowserQRCodeReader.listVideoInputDevices();
|
|
const selectedDeviceId = videoInputDevices[0].deviceId;
|
|
|
|
controlsRef.current = await codeReader.decodeFromVideoDevice(
|
|
selectedDeviceId,
|
|
videoRef.current ?? undefined,
|
|
(result, error) => {
|
|
if (result) {
|
|
onScan(result.getText());
|
|
}
|
|
|
|
if (error && !(error instanceof Error)) {
|
|
_onError(new Error(error));
|
|
}
|
|
}
|
|
);
|
|
} catch (error) {
|
|
_onError(error instanceof Error ? error : new Error('Unknown error occurred'));
|
|
}
|
|
};
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
const timeoutId = setTimeout(startScanning, delay);
|
|
|
|
return () => {
|
|
clearTimeout(timeoutId);
|
|
|
|
if (controlsRef.current) {
|
|
controlsRef.current.stop();
|
|
}
|
|
};
|
|
}, [onScan, _onError, delay]);
|
|
|
|
return (
|
|
<StyledDiv
|
|
className={className}
|
|
style={containerStyle}
|
|
>
|
|
<video
|
|
className='ui--qr-Scan'
|
|
ref={videoRef}
|
|
style={style}
|
|
/>
|
|
</StyledDiv>
|
|
);
|
|
}
|
|
|
|
const StyledDiv = styled.div`
|
|
.ui--qr-Scan {
|
|
display: inline-block;
|
|
height: 100%;
|
|
transform: matrix(-1, 0, 0, 1, 0, 0);
|
|
width: 100%;
|
|
}
|
|
`;
|
|
|
|
export const QrScan = React.memo(Scan);
|