Skip to main content

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

PackageVersionPurpose
react-native-vision-camera^4.7.3Camera access and frame processing
react-native-worklets-coreWorklet 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}
/>
PropertyValueDescription
deviceBack cameraDefault rear-facing camera
isActiveDynamicControlled by screen focus and scan state
frameProcessorvisionCameraQr pluginCustom native QR decoder
fps1Process 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