react-native-fabric-barcode-scanner 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ import AVFoundation
2
+ import UIKit
3
+
4
+ private final class MetadataOutputHandler: NSObject, AVCaptureMetadataOutputObjectsDelegate {
5
+ var onBarcodeScanned: ((NSDictionary) -> Void)?
6
+
7
+ func metadataOutput(_ output: AVCaptureMetadataOutput,
8
+ didOutput metadataObjects: [AVMetadataObject],
9
+ from connection: AVCaptureConnection) {
10
+ for object in metadataObjects {
11
+ guard let readable = object as? AVMetadataMachineReadableCodeObject,
12
+ let value = readable.stringValue else { continue }
13
+ onBarcodeScanned?([
14
+ "value": value,
15
+ "type": RNScannerHelper.name(for: readable.type),
16
+ "timestamp": Int32(Date().timeIntervalSince1970 * 1000)
17
+ ] as NSDictionary)
18
+ }
19
+ }
20
+ }
21
+
22
+ @objc(RNScannerView)
23
+ public class RNScannerView: UIView {
24
+
25
+ private let session = AVCaptureSession()
26
+ private var previewLayer: AVCaptureVideoPreviewLayer?
27
+ private let metadataOutput = AVCaptureMetadataOutput()
28
+ private let metadataHandler = MetadataOutputHandler()
29
+ private let sessionQueue = DispatchQueue(label: "com.rncustomscanner.session")
30
+ private var isSessionConfigured = false
31
+
32
+ @objc public var isActive: Bool = true {
33
+ didSet {
34
+ sessionQueue.async {
35
+ if !self.isSessionConfigured {
36
+ self.configureSessionIfNeeded()
37
+ } else {
38
+ self.updateSessionState()
39
+ }
40
+ }
41
+ }
42
+ }
43
+ @objc public var torchOn: Bool = false { didSet { RNScannerHelper().setTorchEnabled(torchOn) } }
44
+ @objc public var scanFormats: [String] = RNScannerHelper.supportedFormats { didSet { applyMetadataFilters() } }
45
+
46
+ // Wired up by ScannerViewManager to emit the Fabric event
47
+ @objc public var onBarcodeScanned: ((NSDictionary) -> Void)? {
48
+ didSet { metadataHandler.onBarcodeScanned = onBarcodeScanned }
49
+ }
50
+
51
+ public override init(frame: CGRect) {
52
+ super.init(frame: frame)
53
+ sessionQueue.async {
54
+ self.configureSessionIfNeeded()
55
+ }
56
+ }
57
+ public required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") }
58
+
59
+ // Only builds the AVCaptureSession if permission is already granted.
60
+ // Requesting permission is entirely JS's responsibility via
61
+ // ScannerModule.requestCameraPermission() — this view never triggers
62
+ // the system permission dialog itself.
63
+ private func configureSessionIfNeeded() {
64
+ guard !isSessionConfigured else {
65
+ updateSessionState()
66
+ return
67
+ }
68
+ guard AVCaptureDevice.authorizationStatus(for: .video) == .authorized else {
69
+ return
70
+ }
71
+ buildSessionGraph()
72
+ }
73
+
74
+ private func buildSessionGraph() {
75
+ session.beginConfiguration()
76
+ guard let device = AVCaptureDevice.default(for: .video),
77
+ let input = try? AVCaptureDeviceInput(device: device),
78
+ session.canAddInput(input) else {
79
+ session.commitConfiguration()
80
+ return
81
+ }
82
+ session.addInput(input)
83
+ guard session.canAddOutput(metadataOutput) else {
84
+ session.commitConfiguration()
85
+ return
86
+ }
87
+ session.addOutput(metadataOutput)
88
+ metadataOutput.setMetadataObjectsDelegate(metadataHandler, queue: DispatchQueue.main)
89
+ session.commitConfiguration()
90
+ isSessionConfigured = true
91
+
92
+ DispatchQueue.main.async { [weak self] in
93
+ guard let self = self else { return }
94
+ self.applyMetadataFilters()
95
+ let layer = AVCaptureVideoPreviewLayer(session: self.session)
96
+ layer.videoGravity = .resizeAspectFill
97
+ layer.frame = self.bounds
98
+ self.layer.addSublayer(layer)
99
+ self.previewLayer = layer
100
+ self.updateSessionState()
101
+ }
102
+ }
103
+
104
+ private func applyMetadataFilters() {
105
+ let requested = scanFormats.compactMap { RNScannerHelper.avFormat(for: $0) }
106
+ let available = metadataOutput.availableMetadataObjectTypes
107
+ metadataOutput.metadataObjectTypes = requested.filter { available.contains($0) }
108
+ }
109
+
110
+ private func updateSessionState() {
111
+ sessionQueue.async {
112
+ guard self.isSessionConfigured else { return }
113
+ if self.isActive && !self.session.isRunning {
114
+ self.session.startRunning()
115
+ } else if !self.isActive && self.session.isRunning {
116
+ self.session.stopRunning()
117
+ }
118
+ }
119
+ }
120
+
121
+ public override func layoutSubviews() {
122
+ super.layoutSubviews()
123
+ previewLayer?.frame = bounds
124
+ }
125
+
126
+ public override func removeFromSuperview() {
127
+ sessionQueue.async { [session] in
128
+ if session.isRunning { session.stopRunning() }
129
+ }
130
+ super.removeFromSuperview()
131
+ }
132
+ }
@@ -0,0 +1,65 @@
1
+ #import "RNCustomScannerSpec.h"
2
+ #import "RNCustomScanner-Swift.h"
3
+
4
+ using namespace facebook::react;
5
+
6
+ @interface ScannerModule : NativeScannerModuleSpecBase <NativeScannerModuleSpec>
7
+ @end
8
+
9
+ @implementation ScannerModule {
10
+ RNScannerHelper *_helper;
11
+ }
12
+
13
+ RCT_EXPORT_MODULE(ScannerModule)
14
+
15
+ + (BOOL)requiresMainQueueSetup {
16
+ return NO;
17
+ }
18
+
19
+ - (instancetype)init {
20
+ if (self = [super init]) {
21
+ _helper = [RNScannerHelper new];
22
+ }
23
+ return self;
24
+ }
25
+
26
+ - (void)checkCameraPermission:(RCTPromiseResolveBlock)resolve
27
+ reject:(RCTPromiseRejectBlock)reject {
28
+ resolve([_helper checkCameraPermission]);
29
+ }
30
+
31
+ - (void)requestCameraPermission:(RCTPromiseResolveBlock)resolve
32
+ reject:(RCTPromiseRejectBlock)reject {
33
+ [_helper requestCameraPermission:^(NSString *result) {
34
+ resolve(result);
35
+ }];
36
+ }
37
+
38
+ - (void)setTorchEnabled:(BOOL)enabled {
39
+ [_helper setTorchEnabled:enabled];
40
+ }
41
+
42
+ - (void)openCameraSettings {
43
+ [_helper openCameraSettings];
44
+ }
45
+
46
+ - (facebook::react::ModuleConstants<JS::NativeScannerModule::Constants>)getConstants {
47
+ std::vector<NSString *> formats;
48
+ for (NSString *f in RNScannerHelper.supportedFormats) {
49
+ formats.push_back(f);
50
+ }
51
+ return facebook::react::typedConstants<JS::NativeScannerModule::Constants>({
52
+ .SUPPORTED_FORMATS = formats
53
+ });
54
+ }
55
+
56
+ - (facebook::react::ModuleConstants<JS::NativeScannerModule::Constants>)constantsToExport {
57
+ return [self getConstants];
58
+ }
59
+
60
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
61
+ (const facebook::react::ObjCTurboModule::InitParams &)params {
62
+ return std::make_shared<facebook::react::NativeScannerModuleSpecJSI>(params);
63
+ }
64
+
65
+ @end
@@ -0,0 +1,105 @@
1
+ import AVFoundation
2
+ import Foundation
3
+ import UIKit
4
+
5
+ @objc(RNScannerHelper)
6
+ public class RNScannerHelper: NSObject {
7
+
8
+ @objc public static var supportedFormats: [String] {
9
+ return ["qr", "ean13", "ean8", "upce", "code128", "code39",
10
+ "code93", "pdf417", "aztec", "datamatrix", "itf14", "interleaved2of5"]
11
+ }
12
+
13
+ // NOTE: intentionally NOT @objc — AVMetadataObject.ObjectType does not
14
+ // bridge cleanly to Objective-C in the generated -Swift.h header and
15
+ // causes "expected a type" build errors if exposed. These are internal
16
+ // Swift-only helpers; call them only from other Swift code.
17
+ internal static func avFormat(for name: String) -> AVMetadataObject.ObjectType? {
18
+ switch name {
19
+ case "qr": return .qr
20
+ case "ean13": return .ean13
21
+ case "ean8": return .ean8
22
+ case "upce": return .upce
23
+ case "code128": return .code128
24
+ case "code39": return .code39
25
+ case "code93": return .code93
26
+ case "pdf417": return .pdf417
27
+ case "aztec": return .aztec
28
+ case "datamatrix": return .dataMatrix
29
+ case "itf14": return .itf14
30
+ case "interleaved2of5": return .interleaved2of5
31
+ default: return nil
32
+ }
33
+ }
34
+
35
+ internal static func name(for avType: AVMetadataObject.ObjectType) -> String {
36
+ switch avType {
37
+ case .qr: return "qr"
38
+ case .ean13: return "ean13"
39
+ case .ean8: return "ean8"
40
+ case .upce: return "upce"
41
+ case .code128: return "code128"
42
+ case .code39: return "code39"
43
+ case .code93: return "code93"
44
+ case .pdf417: return "pdf417"
45
+ case .aztec: return "aztec"
46
+ case .dataMatrix: return "datamatrix"
47
+ case .itf14: return "itf14"
48
+ case .interleaved2of5: return "interleaved2of5"
49
+ default: return "unknown"
50
+ }
51
+ }
52
+
53
+ public override init() {
54
+ super.init()
55
+ }
56
+
57
+ @objc public func checkCameraPermission() -> String {
58
+ switch AVCaptureDevice.authorizationStatus(for: .video) {
59
+ case .authorized: return "granted"
60
+ case .denied: return "blocked"
61
+ case .restricted: return "restricted"
62
+ case .notDetermined: return "not-determined"
63
+ @unknown default: return "not-determined"
64
+ }
65
+ }
66
+
67
+ @objc public func requestCameraPermission(_ completion: @escaping (String) -> Void) {
68
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
69
+
70
+ switch status {
71
+ case .authorized:
72
+ completion("granted")
73
+ case .denied:
74
+ completion("blocked")
75
+ case .restricted:
76
+ completion("restricted")
77
+ case .notDetermined:
78
+ AVCaptureDevice.requestAccess(for: .video) { granted in
79
+ DispatchQueue.main.async {
80
+ completion(granted ? "granted" : "blocked")
81
+ }
82
+ }
83
+ @unknown default:
84
+ completion("not-determined")
85
+ }
86
+ }
87
+
88
+ @objc public func openCameraSettings() {
89
+ DispatchQueue.main.async {
90
+ guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
91
+ UIApplication.shared.open(url)
92
+ }
93
+ }
94
+
95
+ @objc public func setTorchEnabled(_ enabled: Bool) {
96
+ guard let device = AVCaptureDevice.default(for: .video), device.hasTorch else { return }
97
+ do {
98
+ try device.lockForConfiguration()
99
+ device.torchMode = enabled ? .on : .off
100
+ device.unlockForConfiguration()
101
+ } catch {
102
+ // Best-effort: torch failures are non-fatal for scanning
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,67 @@
1
+ #import "RNCustomScanner-Swift.h"
2
+ #import <React/RCTViewComponentView.h>
3
+ #import <react/renderer/components/RNCustomScannerSpec/ComponentDescriptors.h>
4
+ #import <react/renderer/components/RNCustomScannerSpec/EventEmitters.h>
5
+ #import <react/renderer/components/RNCustomScannerSpec/Props.h>
6
+ #import <react/renderer/components/RNCustomScannerSpec/RCTComponentViewHelpers.h>
7
+
8
+ using namespace facebook::react;
9
+
10
+ @interface ScannerView : RCTViewComponentView <RCTScannerViewViewProtocol>
11
+ @end
12
+
13
+ @implementation ScannerView {
14
+ RNScannerView *_scannerView;
15
+ }
16
+
17
+ + (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider {
18
+ return concreteComponentDescriptorProvider<ScannerViewComponentDescriptor>();
19
+ }
20
+
21
+ - (instancetype)initWithFrame:(CGRect)frame {
22
+ if (self = [super initWithFrame:frame]) {
23
+ static const auto defaultProps = std::make_shared<const ScannerViewProps>();
24
+ _props = defaultProps;
25
+
26
+ _scannerView = [[RNScannerView alloc] initWithFrame:self.bounds];
27
+ __weak ScannerView *weakSelf = self;
28
+ _scannerView.onBarcodeScanned = ^(NSDictionary<NSString *, id> *payload) {
29
+ [weakSelf emitBarcodeScanned:payload];
30
+ };
31
+ self.contentView = _scannerView;
32
+ }
33
+ return self;
34
+ }
35
+
36
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
37
+ const auto &newProps = *std::static_pointer_cast<const ScannerViewProps>(props);
38
+
39
+ _scannerView.isActive = newProps.isActive;
40
+ _scannerView.torchOn = newProps.torchOn;
41
+
42
+ if (!newProps.scanFormats.empty()) {
43
+ NSMutableArray<NSString *> *formats = [NSMutableArray new];
44
+ for (const auto &format : newProps.scanFormats) {
45
+ [formats addObject:[NSString stringWithUTF8String:format.c_str()]];
46
+ }
47
+ _scannerView.scanFormats = formats;
48
+ }
49
+
50
+ [super updateProps:props oldProps:oldProps];
51
+ }
52
+
53
+ - (void)emitBarcodeScanned:(NSDictionary<NSString *, id> *)payload {
54
+ if (!_eventEmitter) return;
55
+ const auto emitter = std::static_pointer_cast<const ScannerViewEventEmitter>(_eventEmitter);
56
+ emitter->onBarcodeScanned(ScannerViewEventEmitter::OnBarcodeScanned{
57
+ .value = std::string([payload[@"value"] UTF8String]),
58
+ .type = std::string([payload[@"type"] UTF8String]),
59
+ .timestamp = [payload[@"timestamp"] intValue]
60
+ });
61
+ }
62
+
63
+ @end
64
+
65
+ Class<RCTComponentViewProtocol> ScannerViewCls(void) {
66
+ return ScannerView.class;
67
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "react-native-fabric-barcode-scanner",
3
+ "version": "1.0.0",
4
+ "description": "High-performance native barcode scanner for React Native with TurboModule + Fabric (New Architecture) support on iOS and Android",
5
+ "main": "src/index.tsx",
6
+ "types": "src/index.tsx",
7
+ "source": "src/index.tsx",
8
+ "react-native": "src/index.tsx",
9
+ "files": [
10
+ "src",
11
+ "ios",
12
+ "android",
13
+ "react-native-fabric-barcode-scanner.podspec",
14
+ "react-native.config.js",
15
+ "!ios/build",
16
+ "!android/build",
17
+ "!android/.cxx",
18
+ "!android/.gradle"
19
+ ],
20
+ "scripts": {
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "keywords": [
24
+ "react-native",
25
+ "barcode",
26
+ "scanner",
27
+ "qr",
28
+ "qr-code",
29
+ "camera",
30
+ "turbo-module",
31
+ "fabric",
32
+ "new-architecture",
33
+ "mlkit",
34
+ "camerax"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Rajubarde12/react-native-fabric-barcode-scanner.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Rajubarde12/react-native-fabric-barcode-scanner/issues"
42
+ },
43
+ "homepage": "https://github.com/Rajubarde12/react-native-fabric-barcode-scanner#readme",
44
+ "author": "Rajubarde12 (https://github.com/Rajubarde12)",
45
+ "license": "MIT",
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "registry": "https://registry.npmjs.org/"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "*",
52
+ "react-native": ">=0.74.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/react": "^19.0.0",
56
+ "typescript": "^5.0.0"
57
+ },
58
+ "codegenConfig": {
59
+ "name": "RNCustomScannerSpec",
60
+ "type": "all",
61
+ "jsSrcsDir": "src",
62
+ "ios": {
63
+ "componentProvider": {
64
+ "ScannerView": "ScannerView"
65
+ }
66
+ },
67
+ "android": {
68
+ "javaPackageName": "com.rncustomscanner"
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,28 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "react-native-fabric-barcode-scanner"
7
+ s.module_name = "RNCustomScanner"
8
+ s.version = package["version"]
9
+ s.summary = package["description"]
10
+ s.homepage = package["homepage"]
11
+ s.license = package["license"]
12
+ s.authors = package["author"]
13
+ s.platforms = { :ios => "13.4" }
14
+ repo_url = package["repository"]["url"].sub(/^git\+/, "")
15
+ s.source = { :git => repo_url, :tag => "v#{s.version}" }
16
+ s.static_framework = true
17
+
18
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
19
+
20
+ s.dependency "React-Core"
21
+
22
+ s.pod_target_xcconfig = {
23
+ "DEFINES_MODULE" => "YES",
24
+ "SWIFT_VERSION" => "5.0"
25
+ }
26
+
27
+ install_modules_dependencies(s)
28
+ end
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ ios: {},
5
+ android: {},
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,21 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ export type CameraPermissionStatus =
5
+ | 'granted'
6
+ | 'not-determined'
7
+ | 'denied'
8
+ | 'blocked'
9
+ | 'restricted';
10
+
11
+ export interface Spec extends TurboModule {
12
+ checkCameraPermission(): Promise<CameraPermissionStatus>;
13
+ requestCameraPermission(): Promise<CameraPermissionStatus>;
14
+ openCameraSettings(): void;
15
+ setTorchEnabled(enabled: boolean): void;
16
+ getConstants(): {
17
+ SUPPORTED_FORMATS: string[];
18
+ };
19
+ }
20
+
21
+ export default TurboModuleRegistry.getEnforcing<Spec>('ScannerModule');
@@ -0,0 +1,28 @@
1
+ import type { ViewProps, HostComponent } from 'react-native';
2
+ import { codegenNativeComponent } from 'react-native';
3
+ import type {
4
+ Int32,
5
+ WithDefault,
6
+ BubblingEventHandler,
7
+ } from 'react-native/Libraries/Types/CodegenTypes';
8
+
9
+ export interface BarcodeScannedEvent {
10
+ value: string;
11
+ type: string; // e.g. 'qr', 'ean13', 'code128', 'pdf417', 'upc-e', etc.
12
+ timestamp: Int32;
13
+ }
14
+
15
+ export interface NativeProps extends ViewProps {
16
+ isActive?: WithDefault<boolean, true>;
17
+ torchOn?: WithDefault<boolean, false>;
18
+
19
+ // comma-free array prop: list of formats to actively decode
20
+ // e.g. ['qr','ean13','ean8','upc-a','upc-e','code128','code39','pdf417','aztec','datamatrix']
21
+ scanFormats?: string[];
22
+
23
+ onBarcodeScanned?: BubblingEventHandler<BarcodeScannedEvent>;
24
+ }
25
+
26
+ export default codegenNativeComponent<NativeProps>(
27
+ 'ScannerView',
28
+ ) as HostComponent<NativeProps>;
package/src/index.tsx ADDED
@@ -0,0 +1,66 @@
1
+ import ScannerModule from './NativeScannerModule';
2
+ import type { CameraPermissionStatus } from './NativeScannerModule';
3
+ import ScannerViewNative from './ScannerViewNativeComponent';
4
+ import type { BarcodeScannedEvent } from './ScannerViewNativeComponent';
5
+
6
+ export const ScannerView = ScannerViewNative;
7
+ export type { BarcodeScannedEvent, CameraPermissionStatus };
8
+
9
+ export interface CameraPermissionState {
10
+ status: CameraPermissionStatus;
11
+ canRequest: boolean;
12
+ canOpenSettings: boolean;
13
+ }
14
+
15
+ export function isCameraPermissionGranted(
16
+ status: CameraPermissionStatus,
17
+ ): boolean {
18
+ return status === 'granted';
19
+ }
20
+
21
+ export function canRequestCameraPermission(
22
+ status: CameraPermissionStatus,
23
+ ): boolean {
24
+ return status === 'not-determined' || status === 'denied';
25
+ }
26
+
27
+ export function shouldOpenCameraSettings(
28
+ status: CameraPermissionStatus,
29
+ ): boolean {
30
+ return status === 'blocked' || status === 'restricted';
31
+ }
32
+
33
+ export async function getCameraPermissionState(): Promise<CameraPermissionState> {
34
+ const status = await ScannerModule.checkCameraPermission();
35
+ return {
36
+ status,
37
+ canRequest: canRequestCameraPermission(status),
38
+ canOpenSettings: shouldOpenCameraSettings(status),
39
+ };
40
+ }
41
+
42
+ export async function checkCameraPermission(): Promise<CameraPermissionStatus> {
43
+ return ScannerModule.checkCameraPermission();
44
+ }
45
+
46
+ export async function requestCameraPermission(): Promise<CameraPermissionStatus> {
47
+ const current = await ScannerModule.checkCameraPermission();
48
+
49
+ if (current === 'granted') {
50
+ return 'granted';
51
+ }
52
+
53
+ if (!canRequestCameraPermission(current)) {
54
+ return current;
55
+ }
56
+
57
+ return ScannerModule.requestCameraPermission();
58
+ }
59
+
60
+ export function openCameraSettings(): void {
61
+ ScannerModule.openCameraSettings();
62
+ }
63
+
64
+ export function setTorchEnabled(enabled: boolean): void {
65
+ ScannerModule.setTorchEnabled(enabled);
66
+ }