Skip to content

Commit

Permalink
feat: permissions button on the QR Scanner (#729)
Browse files Browse the repository at this point in the history
  • Loading branch information
Darguima authored Feb 27, 2024
1 parent 37290ed commit acb7bd1
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 321 deletions.
2 changes: 1 addition & 1 deletion components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export default function Layout({ title, description, children }: LayoutProps) {
</button>

{/* CONTENT */}
<main className="w-full px-4 pb-6 pt-20 lg:ml-72 lg:px-20">
<main className="flex min-h-screen w-full flex-col px-4 pb-6 pt-20 lg:ml-72 lg:px-20">
<h2 className="select-none font-ibold text-4xl sm:text-5xl">
{title}
</h2>
Expand Down
136 changes: 0 additions & 136 deletions components/QRScanner/BarebonesQRScanner/index.jsx

This file was deleted.

104 changes: 104 additions & 0 deletions components/QRScanner/BarebonesQRScanner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
useRef,
useState,
useEffect,
MutableRefObject,
ReactNode,
} from "react";
import { FEEDBACK, FeedbackType } from "@components/QRScanner";
import useWebcamPermissions from "./useWebcam";
import useQRScanner from "./useQRScanner";

interface Props extends React.HTMLProps<HTMLDivElement> {
handleQRCode: (uuid: string) => void;
isScanPaused: MutableRefObject<boolean>;
unpauseTimeout?: number;
setScanFeedback?: (feedback: FeedbackType) => void;
}

const BarebonesQRScanner: React.FC<Props> = ({
handleQRCode,
isScanPaused,
unpauseTimeout = 700,
setScanFeedback = (_) => {},
...rest
}) => {
const [successReadingCode, setSuccessReadingCode] = useState(false);
const [camMessage, setCamMessage] = useState<ReactNode>("");
const [isCamReady, setIsCamReady] = useState(false);

const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationFrameRef = useRef<number>();

const parseURL = (url: string) => {
try {
const url_obj = new URL(url);

if (url_obj.host !== process.env.NEXT_PUBLIC_QRCODE_HOST) {
setScanFeedback(FEEDBACK.INVALID_QR);
return null;
}

return url_obj.pathname.split("/").at(-1);
} catch {
return null;
}
};

useEffect(() => {
if (!successReadingCode) {
const timeoutId = setTimeout(() => {
setScanFeedback(FEEDBACK.SCANNING);
isScanPaused.current = false;
}, unpauseTimeout);

return () => {
clearTimeout(timeoutId);
};
}
}, [successReadingCode]);

useWebcamPermissions({
videoRef,
onPermissionGranted: () => {
setIsCamReady(true);
},
setCamMessage,
});

useQRScanner({
isCamReady,
videoRef,
canvasRef,
animationFrameRef,
isScanPaused,
parseURL,
handleQRCode,
setSuccessReadingCode,
});

return (
<div
{...rest}
className={
"relative flex aspect-square w-full items-center justify-center overflow-hidden rounded-2xl bg-primary " +
rest.className
}
>
<div className="absolute h-full w-full bg-white opacity-5" />

<video ref={videoRef} className="absolute h-full w-full object-cover" />
<canvas
ref={canvasRef}
className="absolute h-full w-full rounded-2xl object-cover"
/>

<div className="absolute flex h-full w-full items-center justify-center">
<div className="p-16 text-center text-white">{camMessage}</div>
</div>
</div>
);
};

export default BarebonesQRScanner;
97 changes: 97 additions & 0 deletions components/QRScanner/BarebonesQRScanner/useQRScanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { MutableRefObject, useEffect } from "react";
import jsQR from "jsqr";

interface useQRScannerProps {
isCamReady: boolean;
videoRef: MutableRefObject<HTMLVideoElement>;
canvasRef: MutableRefObject<HTMLCanvasElement>;
animationFrameRef: MutableRefObject<number>;
isScanPaused: MutableRefObject<boolean>;
parseURL: (url: string) => string | null;
handleQRCode: (uuid: string) => void;
setSuccessReadingCode: React.Dispatch<React.SetStateAction<boolean>>;
}

const useQRScanner = ({
isCamReady,
videoRef,
canvasRef,
animationFrameRef,
isScanPaused,
parseURL,
handleQRCode,
setSuccessReadingCode,
}: useQRScannerProps) => {
useEffect(() => {
drawQRBoundingBox();
}, [isCamReady]);

const drawQRBoundingBox = () => {
const video = videoRef?.current;
const canvas = canvasRef?.current;

if (!video || !canvas) {
cancelAnimationFrame(animationFrameRef.current);
return null;
}

const canvas2D = canvas.getContext("2d");
let successReadingCode = false;

if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;

// Will use the canvas to get the video image data, and pass it to jsQR, but will then clear the canvas to just draw the bounding box
canvas2D.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = canvas2D.getImageData(
0,
0,
canvas.width,
canvas.height
);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
canvas2D.clearRect(0, 0, canvas.width, canvas.height);

if (code) {
successReadingCode = true;

const {
topLeftCorner,
topRightCorner,
bottomLeftCorner,
bottomRightCorner,
} = code.location;

canvas2D.beginPath();

canvas2D.moveTo(topLeftCorner.x, topLeftCorner.y);
canvas2D.lineTo(topRightCorner.x, topRightCorner.y);
canvas2D.lineTo(bottomRightCorner.x, bottomRightCorner.y);
canvas2D.lineTo(bottomLeftCorner.x, bottomLeftCorner.y);
canvas2D.lineTo(topLeftCorner.x, topLeftCorner.y);

canvas2D.lineWidth = 4;
canvas2D.strokeStyle = "#78f400";
canvas2D.stroke();

if (!isScanPaused.current) {
const uuid = parseURL(code.data);

if (uuid) {
handleQRCode(uuid);
isScanPaused.current = true;
}
}
}
}

setSuccessReadingCode(successReadingCode);

animationFrameRef.current = requestAnimationFrame(drawQRBoundingBox);
};
};

export default useQRScanner;
Loading

0 comments on commit acb7bd1

Please sign in to comment.