From 47a8c93e8b9f5bba709eaeacf0310629e12a4851 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:07:17 +0000 Subject: [PATCH 1/2] Return every barcode detected in a frame from onReadCode Both scanners already detect all codes in a frame (MLKit on Android, AVCaptureMetadataOutput on iOS) but only the first one was forwarded to JS. onReadCode now also includes `codes`, an array of { codeStringValue, codeFormat } for every code in the frame. Top-level codeStringValue/codeFormat still mirror the first code, so existing consumers are unaffected. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UjZbt9u2A3dueq2sWj2j5Q --- README.md | 2 +- .../src/main/java/com/rncamerakit/CKCamera.kt | 6 ++-- .../com/rncamerakit/events/ReadCodeEvent.kt | 28 +++++++++++++++---- example/src/BarcodeScreenExample.tsx | 3 +- .../CKCameraViewComponentView.mm | 19 +++++++++++-- ios/ReactNativeCameraKit/CameraProtocol.swift | 2 +- ios/ReactNativeCameraKit/CameraView.swift | 16 +++++++---- ios/ReactNativeCameraKit/CodeFormat.swift | 12 ++++++++ ios/ReactNativeCameraKit/RealCamera.swift | 28 +++++++++++-------- .../SimulatorCamera.swift | 2 +- src/CameraProps.ts | 8 ++++++ src/specs/CameraNativeComponent.ts | 4 +++ 12 files changed, 99 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ce44e5236e..11b9ce15ef 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ To have Play Services [pre-download the model in the background after the app is | `barcodeFrameSize` | `object` | Frame size of barcode scanner. Default: `{ width: 300, height: 150 }` | | `laserColor` | Color | Color of barcode scanner laser visualization. Default: `red` | | `frameColor` | Color | Color of barcode scanner frame visualization. Default: `yellow` | -| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue`. Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}` | +| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue` and `codeFormat` of the first code, and `codes` with every code detected in the same frame (`{ codeStringValue, codeFormat }[]`). Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codes)}` | | **Face detection** | | `faceDetectionEnabled` | `boolean` | Enable real-time face detection. Default: `false` | | `faceDetectionThrottleMs` | `number` | Minimum milliseconds between `onFaceDetected` emits. Default: `100` | diff --git a/android/src/main/java/com/rncamerakit/CKCamera.kt b/android/src/main/java/com/rncamerakit/CKCamera.kt index bf8d9e7f44..950a9a9ee2 100644 --- a/android/src/main/java/com/rncamerakit/CKCamera.kt +++ b/android/src/main/java/com/rncamerakit/CKCamera.kt @@ -580,11 +580,13 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } private fun onBarcodeRead(barcodes: List) { - val codeFormat = CodeFormat.fromBarcodeType(barcodes.first().format); + val codes = barcodes.map { barcode -> + ReadCodePayload(barcode.rawValue, CodeFormat.fromBarcodeType(barcode.format).code) + } val surfaceId = UIManagerHelper.getSurfaceId(currentContext) UIManagerHelper .getEventDispatcherForReactTag(currentContext, id) - ?.dispatchEvent(ReadCodeEvent(surfaceId, id, barcodes.first().rawValue, codeFormat.code)) + ?.dispatchEvent(ReadCodeEvent(surfaceId, id, codes)) } private fun onFaceDetected(payloads: List) { diff --git a/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt b/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt index ff3d3a7465..78f0bd53c2 100644 --- a/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt +++ b/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt @@ -4,19 +4,35 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.Event +data class ReadCodePayload( + val codeStringValue: String?, + val codeFormat: String, +) + class ReadCodeEvent( surfaceId: Int, viewId: Int, - private val codeStringValue: String?, - private val codeFormat: String, + private val codes: List, ) : Event(surfaceId, viewId) { override fun getEventName(): String = EVENT_NAME - override fun getEventData(): WritableMap = - Arguments.createMap().apply { - putString("codeFormat", codeFormat) - putString("codeStringValue", codeStringValue) + override fun getEventData(): WritableMap { + val array = Arguments.createArray() + for (code in codes) { + val map = Arguments.createMap().apply { + putString("codeFormat", code.codeFormat) + putString("codeStringValue", code.codeStringValue) + } + array.pushMap(map) + } + // Top-level fields mirror the first code for backward compatibility + val first = codes.first() + return Arguments.createMap().apply { + putString("codeFormat", first.codeFormat) + putString("codeStringValue", first.codeStringValue) + putArray("codes", array) } + } companion object { const val EVENT_NAME = "topReadCode" diff --git a/example/src/BarcodeScreenExample.tsx b/example/src/BarcodeScreenExample.tsx index 9819938a54..4e26a7dc55 100644 --- a/example/src/BarcodeScreenExample.tsx +++ b/example/src/BarcodeScreenExample.tsx @@ -152,9 +152,10 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => { barcodeFrameSize={{ width: 300, height: 150 }} onReadCode={(event) => { Vibration.vibrate(100); - setBarcode(event.nativeEvent.codeStringValue); + setBarcode(event.nativeEvent.codes.map((code) => code.codeStringValue).join('\n')); console.log('barcode', event.nativeEvent.codeStringValue); console.log('codeFormat', event.nativeEvent.codeFormat); + console.log('codes', event.nativeEvent.codes); }} /> diff --git a/ios/ReactNativeCameraKit/CKCameraViewComponentView.mm b/ios/ReactNativeCameraKit/CKCameraViewComponentView.mm index b7fc511335..47abbeac15 100644 --- a/ios/ReactNativeCameraKit/CKCameraViewComponentView.mm +++ b/ios/ReactNativeCameraKit/CKCameraViewComponentView.mm @@ -104,10 +104,25 @@ - (void)prepareView { [event valueForKey:@"codeFormat"] == nil ? "" : std::string([[event valueForKey:@"codeFormat"] UTF8String]); + NSArray *codesArray = [event valueForKey:@"codes"] ?: @[]; + std::vector codes; + codes.reserve(codesArray.count); + for (NSDictionary *code in codesArray) { + NSString *value = code[@"codeStringValue"]; + NSString *format = code[@"codeFormat"]; + codes.push_back({ + .codeStringValue = value == nil ? "" : std::string([value UTF8String]), + .codeFormat = format == nil ? "" : std::string([format UTF8String]), + }); + } + facebook::react::CKCameraEventEmitter::OnReadCode payload = { + .codeStringValue = codeStringValue, + .codeFormat = codeFormat, + .codes = std::move(codes), + }; std::dynamic_pointer_cast( strongSelf->_eventEmitter) - ->onReadCode( - {.codeStringValue = codeStringValue, .codeFormat = codeFormat}); + ->onReadCode(payload); } }]; [_view setOnOrientationChange:^(NSDictionary *event) { diff --git a/ios/ReactNativeCameraKit/CameraProtocol.swift b/ios/ReactNativeCameraKit/CameraProtocol.swift index 1cf943a42d..0f8de7e84b 100644 --- a/ios/ReactNativeCameraKit/CameraProtocol.swift +++ b/ios/ReactNativeCameraKit/CameraProtocol.swift @@ -30,7 +30,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate { func isBarcodeScannerEnabled( _ isEnabled: Bool, supportedBarcodeTypes: [CodeFormat], - onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?) + onBarcodeRead: ((_ codes: [ScannedCode]) -> Void)?) func update(scannerFrameSize: CGRect?) diff --git a/ios/ReactNativeCameraKit/CameraView.swift b/ios/ReactNativeCameraKit/CameraView.swift index 28720e7348..9cc6207f8e 100644 --- a/ios/ReactNativeCameraKit/CameraView.swift +++ b/ios/ReactNativeCameraKit/CameraView.swift @@ -277,8 +277,8 @@ public class CameraView: UIView { camera.isBarcodeScannerEnabled( scanBarcode, supportedBarcodeTypes: convertedAllowedTypes, - onBarcodeRead: { [weak self] (barcode, codeFormat) in - self?.onBarcodeRead(barcode: barcode, codeFormat: codeFormat) + onBarcodeRead: { [weak self] codes in + self?.onBarcodeRead(codes: codes) }) } @@ -482,16 +482,22 @@ public class CameraView: UIView { return temporaryFileURL } - private func onBarcodeRead(barcode: String, codeFormat: CodeFormat) { + private func onBarcodeRead(codes: [ScannedCode]) { // Throttle barcode detection let now = Date.timeIntervalSinceReferenceDate - guard lastBarcodeDetectedTime + Double(scanThrottleDelay) / 1000 < now else { + guard let first = codes.first, + lastBarcodeDetectedTime + Double(scanThrottleDelay) / 1000 < now else { return } lastBarcodeDetectedTime = now - onReadCode?(["codeStringValue": barcode, "codeFormat": codeFormat.rawValue]) + // Top-level fields mirror the first code for backward compatibility + onReadCode?([ + "codeStringValue": first.value, + "codeFormat": first.codeFormat.rawValue, + "codes": codes.map { $0.asDictionary }, + ]) } private func convertAllowedBarcodeTypes() -> [CodeFormat] { diff --git a/ios/ReactNativeCameraKit/CodeFormat.swift b/ios/ReactNativeCameraKit/CodeFormat.swift index 5371f49701..de7d88ceff 100644 --- a/ios/ReactNativeCameraKit/CodeFormat.swift +++ b/ios/ReactNativeCameraKit/CodeFormat.swift @@ -76,3 +76,15 @@ enum CodeFormat: String, CaseIterable { } } } + +struct ScannedCode { + let value: String + let codeFormat: CodeFormat + + var asDictionary: [String: Any] { + return [ + "codeStringValue": value, + "codeFormat": codeFormat.rawValue, + ] + } +} diff --git a/ios/ReactNativeCameraKit/RealCamera.swift b/ios/ReactNativeCameraKit/RealCamera.swift index 5774b14efb..818ddb66a2 100644 --- a/ios/ReactNativeCameraKit/RealCamera.swift +++ b/ios/ReactNativeCameraKit/RealCamera.swift @@ -44,7 +44,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega private var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization? private var resetFocus: (() -> Void)? private var focusFinished: (() -> Void)? - private var onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)? + private var onBarcodeRead: ((_ codes: [ScannedCode]) -> Void)? private var scannerFrameSize: CGRect? = nil private var barcodeFrameSize: CGSize? = nil private var onOrientationChange: RCTDirectEventBlock? @@ -433,7 +433,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega func isBarcodeScannerEnabled( _ isEnabled: Bool, supportedBarcodeTypes supportedBarcodeType: [CodeFormat], - onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)? + onBarcodeRead: ((_ codes: [ScannedCode]) -> Void)? ) { sessionQueue.async { self.onBarcodeRead = onBarcodeRead @@ -526,18 +526,22 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega _ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection ) { - // Try to retrieve the barcode from the metadata extracted - guard - let machineReadableCodeObject = metadataObjects.first - as? AVMetadataMachineReadableCodeObject, - let codeStringValue = machineReadableCodeObject.stringValue - else { - return + // Retrieve every barcode extracted from the frame, keeping AVFoundation's order + let codes: [ScannedCode] = metadataObjects.compactMap { metadataObject in + guard + let machineReadableCodeObject = metadataObject as? AVMetadataMachineReadableCodeObject, + let codeStringValue = machineReadableCodeObject.stringValue + else { + return nil + } + // Determine the barcode type and convert it to CodeFormat + let codeFormat = CodeFormat.fromAVMetadataObjectType(machineReadableCodeObject.type) + return ScannedCode(value: codeStringValue, codeFormat: codeFormat) } - // Determine the barcode type and convert it to CodeFormat - let barcodeType = CodeFormat.fromAVMetadataObjectType(machineReadableCodeObject.type) - onBarcodeRead?(codeStringValue, barcodeType) + guard !codes.isEmpty else { return } + + onBarcodeRead?(codes) } // MARK: - Vision orientation diff --git a/ios/ReactNativeCameraKit/SimulatorCamera.swift b/ios/ReactNativeCameraKit/SimulatorCamera.swift index 41fe9629e2..f8f867c20a 100644 --- a/ios/ReactNativeCameraKit/SimulatorCamera.swift +++ b/ios/ReactNativeCameraKit/SimulatorCamera.swift @@ -187,7 +187,7 @@ class SimulatorCamera: CameraProtocol { func isBarcodeScannerEnabled( _ isEnabled: Bool, supportedBarcodeTypes: [CodeFormat], - onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)? + onBarcodeRead: ((_ codes: [ScannedCode]) -> Void)? ) {} func update(scannerFrameSize: CGRect?) {} diff --git a/src/CameraProps.ts b/src/CameraProps.ts index aa33dc2548..12cbcc32ce 100644 --- a/src/CameraProps.ts +++ b/src/CameraProps.ts @@ -10,10 +10,18 @@ import { } from './types'; import { Orientation } from './index'; +export type ReadCode = { + codeStringValue: string; + codeFormat: CodeFormat; +}; + export type OnReadCodeData = { nativeEvent: { + /** First code detected in the frame (same as `codes[0]`) */ codeStringValue: string; codeFormat: CodeFormat; + /** All codes detected in the same frame, e.g. when several QR codes are visible at once */ + codes: ReadCode[]; }; }; diff --git a/src/specs/CameraNativeComponent.ts b/src/specs/CameraNativeComponent.ts index ab5a715227..98b5036104 100644 --- a/src/specs/CameraNativeComponent.ts +++ b/src/specs/CameraNativeComponent.ts @@ -18,6 +18,10 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNati type OnReadCodeData = { codeStringValue: string; codeFormat: string; + codes: { + codeStringValue: string; + codeFormat: string; + }[]; }; type OnOrientationChangeData = { From 432f4a557588e475bcf8ac21c99beaa2344185c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 01:23:27 +0000 Subject: [PATCH 2/2] Keep the previous ReadCodeEvent constructor for compatibility Native code constructing ReadCodeEvent with a single code keeps compiling, and an empty code list no longer throws when building the event payload. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UjZbt9u2A3dueq2sWj2j5Q --- .../java/com/rncamerakit/events/ReadCodeEvent.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt b/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt index 78f0bd53c2..f7df563591 100644 --- a/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt +++ b/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt @@ -14,6 +14,14 @@ class ReadCodeEvent( viewId: Int, private val codes: List, ) : Event(surfaceId, viewId) { + // Kept for source compatibility with the previous single-code signature + constructor( + surfaceId: Int, + viewId: Int, + codeStringValue: String?, + codeFormat: String, + ) : this(surfaceId, viewId, listOf(ReadCodePayload(codeStringValue, codeFormat))) + override fun getEventName(): String = EVENT_NAME override fun getEventData(): WritableMap { @@ -26,10 +34,10 @@ class ReadCodeEvent( array.pushMap(map) } // Top-level fields mirror the first code for backward compatibility - val first = codes.first() + val first = codes.firstOrNull() return Arguments.createMap().apply { - putString("codeFormat", first.codeFormat) - putString("codeStringValue", first.codeStringValue) + putString("codeFormat", first?.codeFormat) + putString("codeStringValue", first?.codeStringValue) putArray("codes", array) } }