Ionic React QR Code Scanner with Capacitor
In the previous article, we’ve given a brief demonstration on how to use the capacitor-plugin-dynamsoft-barcode-reader to create a cross-platform QR code scanner using web technologies with Capacitor. In this article, we are going to take a step further to create an Ionic React QR Code Scanner.
Online demo on netlify: link.
The app uses IonReactRouter to manage navigation. It has two pages: a home page and a scanning page. At the home page, users can see the scanning results and configure whether to enable continuous scanning. At the scanning page, it opens the camera, scans the QR codes and draws the overlay.
The Capacitor plugin uses Dynamsoft Barcode Reader as its backend.
Building an Ionic React QR Code Scanner
Let’s build an Ionic React QR code scanner in steps.
New project
Create a new Ionic React app:
ionic start qr-code-scanner blank --type=react --capacitor
We can start a server to have a live test in browser:
ionic serve
To run it on Android:
ionic capacitor add android
ionic capacitor copy android // sync files
ionic capacitor run android
To run it on iOS:
ionic capacitor add ios
ionic capacitor copy ios // sync files
ionic capacitor open ios // use XCode to open the project
Add camera permission
For iOS, add the following to Info.plist
:
<key>NSCameraUsageDescription</key>
<string>For barcode scanning</string>
For Android, add the following to AndroidManifest.xml
:
<uses-permission android:name="android.permission.CAMERA" />
Install dependencies
Install the capacitor plugins to use Dynamsoft Barcode Reader and open the camera.
npm install capacitor-plugin-dynamsoft-barcode-reader capacitor-plugin-camera
Create a QR code scanner component
React is declarative while the plugin only has imperative APIs. We need to create a QR code scanner component so that we can use the plugin in a declarative manner.
Here is the complete code of the component (the file is saved as src\components\QRCodeScanner.tsx
):
import { MutableRefObject, useEffect, useRef, useState } from 'react';
import './QRCodeScanner.css';
import { DBR, TextResult } from 'capacitor-plugin-dynamsoft-barcode-reader';
import { CameraPreview } from 'capacitor-plugin-camera';
import { Capacitor, PluginListenerHandle } from '@capacitor/core';
export interface QRCodeScannerProps {
torchOn?: boolean;
onScanned?: (results:TextResult[]) => void;
onPlayed?: (result:{orientation:"LANDSCAPE"|"PORTRAIT",resolution:string}) => void;
}
const QRCodeScanner: React.FC<QRCodeScannerProps> = (props:QRCodeScannerProps) => {
const container:MutableRefObject<HTMLDivElement|null> = useRef(null);
const decoding = useRef(false);
const interval = useRef<any>();
const onPlayedListener = useRef<PluginListenerHandle|undefined>();
const [initialized,setInitialized] = useState(false);
useEffect(() => {
const init = async () => {
if (container.current && Capacitor.isNativePlatform() === false) {
await CameraPreview.setElement(container.current);
}
await CameraPreview.initialize();
await CameraPreview.requestCameraPermission();
await DBR.initialize();
console.log("QRCodeScanner mounted");
if (onPlayedListener.current) {
onPlayedListener.current.remove();
}
onPlayedListener.current = await CameraPreview.addListener("onPlayed", async () => {
startDecoding();
const orientation = (await CameraPreview.getOrientation()).orientation;
const resolution = (await CameraPreview.getResolution()).resolution;
if (props.onPlayed) {
props.onPlayed({orientation:orientation,resolution:resolution});
}
});
await CameraPreview.startCamera();
setInitialized(true);
}
init();
return ()=>{
console.log("unmount and stop scan");
stopDecoding();
CameraPreview.stopCamera();
}
}, []);
const startDecoding = () => {
stopDecoding();
interval.current = setInterval(captureAndDecode,100);
}
const stopDecoding = () => {
clearInterval(interval.current);
}
const captureAndDecode = async () => {
if (decoding.current === true) {
return;
}
let results = [];
let dataURL;
decoding.current = true;
try {
if (Capacitor.isNativePlatform()) {
await CameraPreview.saveFrame();
results = (await DBR.decodeBitmap({})).results;
}else{
let frame = await CameraPreview.takeSnapshot({quality:50});
dataURL = "data:image/jpeg;base64,"+frame.base64;
results = await readDataURL(dataURL);
}
if (props.onScanned) {
props.onScanned(results);
}
} catch (error) {
console.log(error);
}
decoding.current = false;
}
const readDataURL = async (dataURL:string) => {
let response = await DBR.decode({source:dataURL});
let results = response.results;
return results;
}
useEffect(() => {
if (initialized) {
if (props.torchOn === true) {
CameraPreview.toggleTorch({on:true});
}else{
CameraPreview.toggleTorch({on:false});
}
}
}, [props.torchOn]);
return (
<>
{!initialized && (
<div>Initializing...</div>
)}
<div ref={container}>
<div className="dce-video-container"></div>
</div>
</>
);
};
export default QRCodeScanner;
Here, we’ve created a functional component. When the component is mounted, it will start the camera and a loop to read barcodes from the camera.
Create a Home page and a Scanner page
- Create
Home.tsx
andScanner.tsx
undersrc\pages
. -
The navigation is managed using
IonReactRouter
inApp.tsx
.const App: React.FC = () => { return ( <IonApp> <IonReactRouter> <IonRouterOutlet> <Route path="/home" component={Home} exact={true} /> <Route path="/scanner" component={Scanner} exact={true} /> <Route exact path="/" render={() => <Redirect to="/home" />} /> </IonRouterOutlet> </IonReactRouter> </IonApp> ); } export default App;
Implement the Home page
When the home page is mounted, initialize the license for Dynamsoft Barcode Reader. You can apply for your license here.
const initLicenseTried = useRef(false);
const [licenseInitialized, setLicenseInitialized] = useState(false);
useEffect(() => {
if (initLicenseTried.current === false) {
initLicenseTried.current = true;
const initLicense = async () => {
try {
await DBR.initLicense({license:"LICENSE-KEY"})
setLicenseInitialized(true);
} catch (error) {
alert(error);
}
}
initLicense();
}
initLicenseTried.current = true;
}, []);
Then, in the home page, create a Start Scanning
button and a checkbox.
const [continuousScan, setContinuousScan] = useState(false);
const handleOption = (e: any) => {
let value = e.detail.value;
let checked = e.detail.checked;
setContinuousScan(checked)
}
//......
<IonButton expand="full" onClick={startScan}>Start Scanning</IonButton>
<IonList>
<IonItem>
<IonLabel>Continuous Scan</IonLabel>
<IonCheckbox slot="end" value="Continuous Scan" checked={continuousScan} onIonChange={(e) => handleOption(e)}/>
</IonItem>
</IonList>
When the Start Scanning
button is pressed, it will navigate to the scanner page with the continuous scan
props.
const startScan = () => {
props.history.push("scanner",{continuousScan:continuousScan})
}
If continuous scan is disabled, the scanner will return the barcode results to the home page if it detects barcodes. We can display them in a list.
const [barcodeResults, setBarcodeResults] = useState([] as TextResult[]);
{(barcodeResults.length>0) &&
<IonListHeader>
<IonLabel>Results:</IonLabel>
</IonListHeader>
}
{barcodeResults.map((tr,idx) => (
<IonItem key={idx}>
<IonLabel>{tr.barcodeFormat + ": " + tr.barcodeText}</IonLabel>
</IonItem>
))}
Implement the Scanner page
-
Add the QR code scanner component in JSX. It will start scanning when it is mounted.
const [barcodeResults,setBarcodeResults] = useState([] as TextResult[]); const onScanned = (results:TextResult[]) => { setBarcodeResults(results); } return ( <IonPage> <IonContent fullscreen> <QRCodeScanner onScanned={onScanned} /> </IonContent> </IonPage> );
-
Get the
continuousScan
prop.const continuousScan = useRef(false); useEffect(() => { const state = props.location.state as { continuousScan?: boolean }; if (state) { if (state.continuousScan) { continuousScan.current = state.continuousScan; } } }, [props.location.state]);
-
Set the background color to transparent when the scanner page is mounted. We have to do this to reveal the camera preview which is behind the webview.
const ionBackground = useRef(""); useEffect(() => { ionBackground.current = document.documentElement.style.getPropertyValue('--ion-background-color'); return () => { document.documentElement.style.setProperty('--ion-background-color', ionBackground.current); } }, []);
-
In
continuous scan
mode, we can draw QR code overlays to show which QR codes are detected.We can do this using SVG which has been discussed in a previous article.
-
Add an SVG element in JSX:
const [viewBox,setViewBox] = useState("0 0 1920 1080"); //...... <svg viewBox={viewBox} className="overlay" xmlns="<http://www.w3.org/2000/svg>" > </svg>
The style of the SVG element:
.overlay { top: 0; left: 0; position: absolute; width: 100%; height: 100%; z-index: 998; }
-
The
viewBox
attribute should match the video frame size. We can get the size from theonPlayed
event.const onPlayed = (result:{orientation:"LANDSCAPE"|"PORTRAIT",resolution:string}) => { console.log(result); document.documentElement.style.setProperty('--ion-background-color', 'transparent'); let width = parseInt(result.resolution.split("x")[0]); let height = parseInt(result.resolution.split("x")[1]); let box:string; if (result.orientation === "PORTRAIT") { box = "0 0 "+height+" "+width; }else{ box = "0 0 "+width+" "+height; } setViewBox(box); }
-
Add polygon and text SVG elements
<svg viewBox={viewBox} className="overlay" xmlns="<http://www.w3.org/2000/svg>" > {barcodeResults.map((tr,idx) => ( <polygon key={"poly-"+idx} xmlns="<http://www.w3.org/2000/svg>" points={getPointsData(tr)} className="barcode-polygon" /> ))} {barcodeResults.map((tr,idx) => ( <text key={"text-"+idx} xmlns="<http://www.w3.org/2000/svg>" x={tr.x1} y={tr.y1} fill="red" fontSize="20" >{tr.barcodeText}</text> ))} </svg>
The barcode result has the following localization attributes which we can use to determine the location of the polygon and text elements:
x1
,x2
,x3
,x4
,y1
,y2
,y3
,y4
.The
getPointsData
function:const getPointsData = (lr:TextResult) => { let pointsData = lr.x1 + "," + lr.y1 + " "; pointsData = pointsData + lr.x2+ "," + lr.y2 + " "; pointsData = pointsData + lr.x3+ "," + lr.y3 + " "; pointsData = pointsData + lr.x4+ "," + lr.y4; return pointsData; }
-
-
It
continuous scan
is set to false, return to the home page when the barcode is scanned.const scanned = useRef(false); const onScanned = (results:TextResult[]) => { console.log(results); if (continuousScan.current) { setBarcodeResults(results); }else{ if (results.length>0 && scanned.current === false) { document.documentElement.style.setProperty('--ion-background-color', ionBackground.current); scanned.current = true; props.history.replace({ state: {results:results} }); props.history.goBack(); } } }
-
Add floating action buttons to toggle the torch and stop scanning.
JSX:
<IonFab vertical="bottom" horizontal="start" slot="fixed"> <IonFabButton> <IonIcon icon={ellipsisHorizontalOutline} /> </IonFabButton> <IonFabList side="top"> <IonFabButton onClick={toggleTorch}> <IonIcon icon={flashlightOutline} /> </IonFabButton> <IonFabButton onClick={() => {goBack()}}> <IonIcon icon={closeOutline} /> </IonFabButton> </IonFabList> </IonFab>
Functions:
const toggleTorch = () => { if (torchOn == false) { setTorchOn(true); }else{ setTorchOn(false); } } const goBack = () => { document.documentElement.style.setProperty('--ion-background-color', ionBackground.current); props.history.goBack(); }
All right, we’ve now completed the demo.
Source Code
Check out the source code to have a try on your own:
https://github.com/xulihang/Ionic-React-QR-Code-Scanner
Disclaimer:
The wrappers and sample code on Dynamsoft Codepool are community editions, shared as-is and not fully tested. Dynamsoft is happy to provide technical support for users exploring these solutions but makes no guarantees.