Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,12 @@ export default function App() {
const startScanning = async () => {
try {
setScanning(true);
await BarcodeScanner.startScanning((barcode) => {
console.log('Barcode detected:', barcode);
setResult(`${barcode.type}: ${barcode.data}`);
await BarcodeScanner.startScanning((barcodes) => {
console.log('Barcodes detected:', barcodes);
if (barcodes.length > 0) {
const barcode = barcodes[0];
setResult(`${barcode.type}: ${barcode.data}`);
}
stopScanning();
});
} catch (error) {
Expand Down Expand Up @@ -188,15 +191,17 @@ const styles = StyleSheet.create({
Starts the barcode scanning process.

```typescript
BarcodeScanner.startScanning((barcode: BarcodeResult) => {
console.log('Type:', barcode.type);
console.log('Data:', barcode.data);
console.log('Raw:', barcode.raw);
BarcodeScanner.startScanning((barcodes: BarcodeResult[]) => {
barcodes.forEach((barcode) => {
console.log('Type:', barcode.type);
console.log('Data:', barcode.data);
console.log('Raw:', barcode.raw);
});
});
```

**Parameters:**
- `callback: (barcode: BarcodeResult) => void` - Called when a barcode is detected
- `callback: (barcodes: BarcodeResult[]) => void` - Called when barcodes are detected

**Returns:** `Promise<void>`

Expand Down Expand Up @@ -401,10 +406,8 @@ export default function App() {
);
return;
}

setScanning(true);
await BarcodeScanner.startScanning((barcode) => {
console.log('Scanned:', barcode);
await BarcodeScanner.startScanning((barcodes) => {
console.log('Scanned:', barcodes);
BarcodeScanner.stopScanning();
setScanning(false);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
Expand Down Expand Up @@ -45,7 +46,7 @@ class CameraManager(private val reactContext: ReactApplicationContext) {
// AtomicBoolean for lock-free scanning flag
private val isScanning = AtomicBoolean(false)
// AtomicReference for thread-safe callback
private val scanCallbackRef = AtomicReference<((WritableMap) -> Unit)?>(null)
private val scanCallbackRef = AtomicReference<((WritableArray) -> Unit)?>(null)

// Lock for synchronizing camera binding operations
private val cameraBindLock = ReentrantLock()
Expand Down Expand Up @@ -105,7 +106,7 @@ class CameraManager(private val reactContext: ReactApplicationContext) {
* Lock-free scanning with atomic CAS operation
* Eliminates race condition between check and set
*/
fun startScanning(callback: (WritableMap) -> Unit) {
fun startScanning(callback: (WritableArray) -> Unit) {
if (!hasCameraPermission()) {
throw SecurityException("Camera permission not granted")
}
Expand Down Expand Up @@ -282,12 +283,21 @@ class CameraManager(private val reactContext: ReactApplicationContext) {

scanner.process(image)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
if (!barcode.rawValue.isNullOrEmpty()) {
val result = createBarcodeResult(barcode)
if (barcodes.isNotEmpty()) {
val results = Arguments.createArray()
var hasValidBarcode = false

for (barcode in barcodes) {
if (!barcode.rawValue.isNullOrEmpty()) {
val result = createBarcodeResult(barcode)
results.pushMap(result)
hasValidBarcode = true
}
}

if (hasValidBarcode) {
val callback = scanCallbackRef.get()
callback?.invoke(result)
break // Process only the first barcode
callback?.invoke(results)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.core.content.ContextCompat
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.WritableArray
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2340,7 +2340,7 @@ PODS:
- React-perflogger (= 0.81.1)
- React-utils (= 0.81.1)
- SocketRocket
- ReactNativeScanner (1.9.0):
- ReactNativeScanner (2.0.1):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2704,7 +2704,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 3eb9096cb139eb433965693bbe541d96eb3d3ec9
ReactCodegen: 4d203eddf6f977caa324640a20f92e70408d648b
ReactCommon: ce5d4226dfaf9d5dacbef57b4528819e39d3a120
ReactNativeScanner: a12dcaf9a2b833163139c0372e17d819e60b7b99
ReactNativeScanner: df211791142d25d9f5a0cc56790172b0b81bb7ad
RNPermissions: 380b0ddaff0bba3d4d0bbe4ed402044bc695752b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 11c9686a21e2cd82a094a723649d9f4507200fb0
Expand Down
10 changes: 6 additions & 4 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ export default function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleBarcodeScanned = (result: BarcodeResult) => {
console.log('Barcode / QR Code scanned:', result.data, result.type);
setScannedData(result);
setScanHistory((prev) => [result, ...prev.slice(0, 9)]); // Keep last 10 scans
const handleBarcodeScanned = (results: BarcodeResult[]) => {
console.log('Barcode / QR Code scanned:', results);
if (results.length > 0) {
setScannedData(results[0] ?? null);
setScanHistory((prev) => [...results, ...prev].slice(0, 20)); // Keep last 20 scans
}
};

const startScanning = async () => {
Expand Down
45 changes: 25 additions & 20 deletions ios/CameraManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,17 @@ actor CameraSessionActor {

// Actor that manages scan callbacks thread-safely
actor CallbackActor {
private var scanCallback: (([String: Any]) -> Void)?
private var scanCallback: (([[String: Any]]) -> Void)?

func setCallback(_ callback: (([String: Any]) -> Void)?) {
func setCallback(_ callback: (([[String: Any]]) -> Void)?) {
scanCallback = callback
}

func getCallback() -> (([String: Any]) -> Void)? {
func getCallback() -> (([[String: Any]]) -> Void)? {
return scanCallback
}

func invokeCallback(with result: [String: Any]) {
func invokeCallback(with result: [[String: Any]]) {
if let callback = scanCallback {
Task { @MainActor in
callback(result)
Expand Down Expand Up @@ -141,7 +141,7 @@ actor CallbackActor {
}

@objc(startScanningWithCallback:error:)
public func startScanning(callback: @escaping ([String: Any]) -> Void) throws {
public func startScanning(callback: @escaping ([[String: Any]]) -> Void) throws {
// Check permission status immediately, but asynchronously request if needed.
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
Expand All @@ -162,21 +162,21 @@ actor CallbackActor {
await self.configureAndStartScanning(callback: callback)
} else {
// Handle denial.
let errorInfo = ["error": "Camera permission denied"]
let errorInfo = [["error": "Camera permission denied"]]
await self.callbackActor.invokeCallback(with: errorInfo)
}
}
}
}
case .denied, .restricted:
// Handle denied/restricted status immediately.
let errorInfo = ["error": "Camera permission not granted"]
let errorInfo = [["error": "Camera permission not granted"]]
Task {
await callbackActor.invokeCallback(with: errorInfo)
}
@unknown default:
// Handle any future unknown cases.
let errorInfo = ["error": "Unknown camera permission status"]
let errorInfo = [["error": "Unknown camera permission status"]]
Task {
await callbackActor.invokeCallback(with: errorInfo)
}
Expand All @@ -185,7 +185,7 @@ actor CallbackActor {

// Private helper with atomic check-and-set
// Eliminates race condition in session initialization
private func configureAndStartScanning(callback: @escaping ([String: Any]) -> Void) async {
private func configureAndStartScanning(callback: @escaping ([[String: Any]]) -> Void) async {
// Atomic operation - no race condition
guard await sessionActor.startScanningIfNotActive() else {
print("⚠️ Session already running, updating callback only")
Expand Down Expand Up @@ -563,20 +563,25 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate {
return
}

guard let results = request.results as? [VNBarcodeObservation],
let firstBarcode = results.first,
let payloadString = firstBarcode.payloadStringValue
else {
guard let results = request.results as? [VNBarcodeObservation], !results.isEmpty else {
return
}

// Create result dictionary
let result = self.createBarcodeResult(
barcode: firstBarcode, payloadString: payloadString)

// Invoke callback through the actor for thread safety
await self.callbackActor.invokeCallback(with: result)
print("📤 Callback invoked with barcode data")
var barcodeResults: [[String: Any]] = []

for barcode in results {
if let payloadString = barcode.payloadStringValue {
let result = self.createBarcodeResult(
barcode: barcode, payloadString: payloadString)
barcodeResults.append(result)
}
}

if !barcodeResults.isEmpty {
// Invoke callback through the actor for thread safety
await self.callbackActor.invokeCallback(with: barcodeResults)
print("📤 Callback invoked with \(barcodeResults.count) barcodes")
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion ios/ReactNativeScanner.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ - (void)startScanning:(RCTPromiseResolveBlock)resolve
NSError *error = nil;
@try {
[_cameraManager
startScanningWithCallback:^(NSDictionary *result) {
startScanningWithCallback:^(NSArray *result) {
[self emitOnBarcodeScanned:result];
}
error:&error];
Expand Down
4 changes: 3 additions & 1 deletion src/NativeReactNativeScanner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CodegenTypes, TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export type BarcodeScannedEvent = {
export type BarcodeResult = {
data: string;
type: string;
bounds?: {
Expand All @@ -16,6 +16,8 @@ export type BarcodeScannedEvent = {
};
};

export type BarcodeScannedEvent = BarcodeResult[];

export interface Spec extends TurboModule {
// Start scanning - results will be emitted via events
startScanning(): Promise<void>;
Expand Down
4 changes: 2 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface BarcodeResult {
};
}

export type BarcodeScannerCallback = (result: BarcodeResult) => void;
export type BarcodeScannerCallback = (results: BarcodeResult[]) => void;

// Scanner API
export class BarcodeScanner {
Expand All @@ -46,7 +46,7 @@ export class BarcodeScanner {
}

this.listener = NativeReactNativeScanner.onBarcodeScanned((event) => {
callback(event as BarcodeResult);
callback(event as unknown as BarcodeResult[]);
});

// Start scanning
Expand Down
Loading