-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: permissions button on the QR Scanner (#729)
- Loading branch information
Showing
12 changed files
with
497 additions
and
321 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.