Camera & QR Scanning
The Certi-Lock® app uses React Native Vision Camera with a custom native frame processor to decode QR codes and barcodes in real-time. The scanning module is a custom native plugin that processes camera frames at 1 FPS on both iOS and Android.
Architecture Overview
┌────────────────────────────┐
│ React Native Vision Camera │
│ (Camera Preview) │
├────────────────────────────┤
│ Frame Processor Plugin │
│ visionCameraQr │
│ (1 FPS processing) │
├────────────────────────────┤
│ Native QR Decoder │
│ ├─ iOS: Swift (AVFoundation│
│ │ VNDetectBarcodesReq│
│ └─ Android: Kotlin (MLKit) │
├────────────────────────────┤
│ JavaScript Callback │
│ (Serial number → saveItem) │
└────────────────────────────┘
Vision Camera Setup
Dependencies
| Package | Version | Purpose |
|---|---|---|
react-native-vision-camera | ^4.7.3 | Camera access and frame processing |
react-native-worklets-core | — | Worklet runtime for frame processors |
Babel Configuration
The Worklets plugin is required for frame processor functions to run on a separate thread:
// babel.config.js
module.exports = {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
plugins: [
["inline-import", { extensions: [".sql"] }],
"react-native-worklets-core/plugin",
],
};
Scan Screen Implementation
Camera Permission
The scan screen requests camera permission before activating the camera:
// app/scan.tsx — Simplified
const { hasPermission, requestPermission } = useCameraPermission();
useEffect(() => {
if (!hasPermission) {
requestPermission();
}
}, [hasPermission]);
Camera Configuration
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={isActive}
frameProcessor={frameProcessor}
fps={1}
/>
| Property | Value | Description |
|---|---|---|
device | Back camera | Default rear-facing camera |
isActive | Dynamic | Controlled by screen focus and scan state |
frameProcessor | visionCameraQr plugin | Custom native QR decoder |
fps | 1 | Process one frame per second (battery optimization) |
Frame Processor
The frame processor calls the custom native plugin to decode barcodes:
const frameProcessor = useFrameProcessor((frame) => {
"worklet";
const result = scanQr(frame);
if (result && result.value) {
runOnJS(handleScan)(result.value);
}
}, []);
Key behaviors:
- Runs on a background worklet thread (not the JS thread)
- Processes at 1 FPS to conserve battery
- Returns decoded QR/barcode value to JavaScript via
runOnJS - Uses a scan lock to prevent duplicate scans while processing
Custom Native Module: visionCameraQr
The QR scanning logic is implemented as a custom React Native Vision Camera frame processor plugin with native code for both platforms.
Module Structure
modules/
└── vision-camera-qr/
├── ios/
│ └── VisionCameraQr.swift # iOS implementation
└── android/
└── VisionCameraQr.kt # Android implementation
iOS Implementation (Swift)
The iOS module uses Apple's Vision framework (VNDetectBarcodesRequest) to detect barcodes in camera frames:
Supported symbologies:
- QR Code
- Code 128
- Code 39
- EAN-13
- EAN-8
- UPC-E
- Data Matrix
Android Implementation (Kotlin)
The Android module uses Google's ML Kit Barcode Scanning API:
Supported formats:
- QR Code
- Code 128
- Code 39
- EAN-13
- EAN-8
- UPC-A / UPC-E
- Data Matrix
Plugin Registration
The native plugin is registered as a Vision Camera frame processor plugin and accessed in JavaScript via:
import { scanQr } from "@/modules/vision-camera-qr";
Scan-to-Save Flow
When a barcode is successfully decoded, the following sequence executes:
1. Scan Lock
A scan lock prevents duplicate processing while the save operation is in progress:
const [isScanning, setIsScanning] = useState(false);
const handleScan = async (serialNumber: string) => {
if (isScanning) return;
setIsScanning(true);
try {
await saveItem(serialNumber);
router.push(`/details/${newItemId}`);
} catch (error) {
Toast.show({ type: "error", text1: "Scan failed" });
} finally {
setIsScanning(false);
}
};
2. Duplicate Detection
Before saving, the app checks if the serial number already exists in the local database:
// If duplicate found
Toast.show({
type: "info",
text1: "Item already scanned",
text2: "This serial number is already in your history.",
});
// Navigate to existing item instead of creating duplicate
3. Save Item
If the serial number is new, saveItem() orchestrates the full data fetch and storage pipeline:
Serial Number
│
├──► Algolia API → Product data (name, subtitle, details)
│
├──► Image API → Sealed image → SHA256 hash → Save to filesystem
│
└──► SQLite INSERT → New item record with all fields
4. Navigation
On successful save, the app navigates to the item details screen:
router.push(`/details/${newItemId}`);
Camera Deactivation
The camera is deactivated when:
- User navigates away from the scan screen
- A scan is being processed (scan lock active)
- App goes to background
- Camera permission is denied
This is managed via the isActive prop, which responds to screen focus state:
const isFocused = useIsFocused();
const appState = useAppState();
const isActive = isFocused && appState === "active" && !isScanning;
Troubleshooting
Camera Permission Denied
If the user denies camera permission, the scan screen displays a permission request message. The user must grant camera access in device Settings manually.
No QR Code Detected
- Ensure the QR code is well-lit and in focus
- The frame processor runs at 1 FPS — hold the camera steady for 1-2 seconds
- Check that the barcode format is one of the supported symbologies
Build Issues with Vision Camera
# iOS: Clean and rebuild
cd ios && pod install && cd ..
npx expo run:ios
# Android: Clean Gradle cache
cd android && ./gradlew clean && cd ..
npx expo run:android
Worklets Plugin Errors
If you see "Worklets are not supported" errors, ensure the Babel plugin is properly configured:
// babel.config.js — Must include:
plugins: ["react-native-worklets-core/plugin"]
Clear the Metro cache after configuration changes:
npx expo start --clear