diff --git a/README.md b/README.md index 7f88bb3..bfd36a1 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@
-![React Native](https://img.shields.io/badge/React%20Native-v0.81+-blue.svg) +![React Native](https://img.shields.io/badge/React%20Native-v0.80+-blue.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) ![Platform](https://img.shields.io/badge/platform-Android%20%7C%20iOS-lightgrey.svg) +![Version](https://img.shields.io/badge/version-2.0.0-brightgreen.svg) **A powerful, easy-to-use QR code & Barcode Scanner for React Native with New Architecture support** @@ -36,6 +37,7 @@ The `@pushpendersingh/react-native-scanner` package also includes a flashlight f - đŸŽ¯ **Easy Integration** - Simple API with event-based scanning - 💡 **Flash Support** - Toggle flashlight on/off - 🔄 **Lifecycle Management** - Automatic camera resource handling +- 🔒 **Thread-Safe** - Modern concurrency patterns (Swift Actors & Kotlin synchronization) - 🎨 **Customizable** - Flexible styling options --- @@ -516,18 +518,24 @@ This library supports a wide range of barcode formats across different categorie ## đŸ› ī¸ Technical Details ### Android + - **CameraX 1.5.0** - Modern camera API with lifecycle awareness - **ML Kit Barcode Scanning 17.3.0** - Google's ML-powered barcode detection -- **Kotlin** - Native implementation +- **Kotlin** - Native implementation with thread-safe synchronization +- **Thread Safety** - Uses `@Volatile` and `@Synchronized` for concurrent access protection ### iOS + - **AVFoundation** - Native camera framework - **Vision Framework** - Apple's barcode detection -- **Swift 5.0** - Native implementation +- **Swift 5.0+** - Native implementation with modern concurrency +- **Thread Safety** - Uses Swift Actors for isolated state management and thread-safe operations ### React Native + - **New Architecture** - Turbo Modules + Fabric support -- **React Native 0.81+** - Minimum version requirement +- **React Native 0.80+** - Minimum version requirement +- **Codegen** - Automatic native interface generation --- diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/CameraManager.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/CameraManager.kt index 048f175..c3012e2 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/CameraManager.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/CameraManager.kt @@ -22,21 +22,38 @@ import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock class CameraManager(private val reactContext: ReactApplicationContext) { private val TAG = "CameraManager" + @Volatile private var cameraProvider: ProcessCameraProvider? = null - private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() + @Volatile + private var cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() private var scanner: BarcodeScanner? = null + @Volatile private var cameraControl: CameraControl? = null private var imageAnalysis: ImageAnalysis? = null private var preview: Preview? = null private var previewView: PreviewView? = null + // AtomicBoolean for lock-free scanning flag + private val isScanning = AtomicBoolean(false) + // AtomicReference for thread-safe callback + private val scanCallbackRef = AtomicReference<((WritableMap) -> Unit)?>(null) + + // Lock for synchronizing camera binding operations + private val cameraBindLock = ReentrantLock() + // Flag to prevent concurrent binding @Volatile - private var isScanning: Boolean = false - private var scanCallback: ((WritableMap) -> Unit)? = null + private var isBinding = false + // Lock for executor lifecycle management + private val executorLock = ReentrantLock() companion object { private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) @@ -50,25 +67,69 @@ class CameraManager(private val reactContext: ReactApplicationContext) { fun bindPreviewView(view: PreviewView) { this.previewView = view - if (isScanning) { - // If already scanning, rebind with preview - bindCameraUseCases() + // Only rebind if scanning AND not already in binding process + if (isScanning.get() && !isBinding) { + // Schedule rebind on main executor to avoid race with initializeCamera callback + ContextCompat.getMainExecutor(reactContext).execute { + bindCameraUseCases() + } + } + } + + /** + * Thread-safe executor lifecycle management + * Ensures the camera executor is available and not shutdown. + * Creates a new executor if the current one has been shutdown. + * Uses dedicated lock to prevent race with releaseCamera() + */ + private fun ensureExecutor() { + executorLock.withLock { + if (cameraExecutor.isShutdown) { + cameraExecutor = Executors.newSingleThreadExecutor() + Log.d(TAG, "â™ģī¸ Recreated camera executor") + } + } + } + + /** + * Safe executor access with validation + * Returns the executor only if it's not shutdown + */ + private fun getExecutorSafely(): ExecutorService? { + return executorLock.withLock { + if (!cameraExecutor.isShutdown) cameraExecutor else null } } - @Synchronized + /** + * Lock-free scanning with atomic CAS operation + * Eliminates race condition between check and set + */ fun startScanning(callback: (WritableMap) -> Unit) { if (!hasCameraPermission()) { throw SecurityException("Camera permission not granted") } - if (isScanning) { - Log.w(TAG, "Scanning already in progress") + // Atomic compare-and-set: only first caller proceeds + if (!isScanning.compareAndSet(false, true)) { + Log.w(TAG, "Scanning already in progress, updating callback") + scanCallbackRef.set(callback) return } - scanCallback = callback - initializeCamera() + // At this point, we're guaranteed to be the only thread starting scanning + // Set callback immediately after winning the CAS race + scanCallbackRef.set(callback) + + try { + ensureExecutor() + initializeCamera() + } catch (e: Exception) { + // Reset flag on error + isScanning.set(false) + scanCallbackRef.set(null) + throw e + } } private fun initializeCamera() { @@ -105,20 +166,47 @@ class CameraManager(private val reactContext: ReactApplicationContext) { bindCameraUseCases() } catch (exc: Exception) { Log.e(TAG, "Error initializing camera: ${exc.message}", exc) - isScanning = false + // Reset state on error + isScanning.set(false) + scanCallbackRef.set(null) } }, ContextCompat.getMainExecutor(reactContext)) } + /** + * Thread-safe camera binding with lock + * Prevents concurrent binding operations that could cause IllegalStateException + */ private fun bindCameraUseCases() { - Log.d(TAG, "Binding camera use cases...") - val currentActivity = reactContext.currentActivity as? AppCompatActivity - if (currentActivity == null) { - Log.e(TAG, "❌ Current activity is not available") - return + // Use lock to serialize all binding operations + cameraBindLock.withLock { + // Check and set binding flag atomically within lock + if (isBinding) { + Log.w(TAG, "âš ī¸ Camera binding already in progress, skipping") + return + } + isBinding = true } - + try { + Log.d(TAG, "Binding camera use cases...") + val currentActivity = reactContext.currentActivity as? AppCompatActivity + if (currentActivity == null || currentActivity.isDestroyed || currentActivity.isFinishing) { + Log.e(TAG, "❌ Current activity is not available") + isScanning.set(false) + scanCallbackRef.set(null) + return + } + + // Get executor safely - may be null if shutdown in progress + val executor = getExecutorSafely() + if (executor == null) { + Log.e(TAG, "❌ Executor is shutdown, cannot bind camera") + isScanning.set(false) + scanCallbackRef.set(null) + return + } + // Unbind any existing use cases first cameraProvider?.unbindAll() Log.d(TAG, "Unbound previous camera use cases") @@ -134,12 +222,14 @@ class CameraManager(private val reactContext: ReactApplicationContext) { } } + // Use the safely-obtained executor reference imageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { analysis -> - analysis.setAnalyzer(cameraExecutor) { imageProxy -> - if (!isScanning) { + analysis.setAnalyzer(executor) { imageProxy -> + // Check atomic boolean + if (!isScanning.get()) { imageProxy.close() return@setAnalyzer } @@ -164,11 +254,17 @@ class CameraManager(private val reactContext: ReactApplicationContext) { ) cameraControl = camera?.cameraControl - isScanning = true - Log.d(TAG, "Camera successfully bound and scanning started (preview: ${preview != null})") + Log.d(TAG, "✅ Camera successfully bound and scanning started (preview: ${preview != null})") } catch (exc: Exception) { Log.e(TAG, "Error binding camera use cases: ${exc.message}", exc) - isScanning = false + // Reset state on binding error + isScanning.set(false) + scanCallbackRef.set(null) + } finally { + // Always reset binding flag in finally block + cameraBindLock.withLock { + isBinding = false + } } } @@ -189,7 +285,8 @@ class CameraManager(private val reactContext: ReactApplicationContext) { for (barcode in barcodes) { if (!barcode.rawValue.isNullOrEmpty()) { val result = createBarcodeResult(barcode) - scanCallback?.invoke(result) + val callback = scanCallbackRef.get() + callback?.invoke(result) break // Process only the first barcode } } @@ -251,33 +348,46 @@ class CameraManager(private val reactContext: ReactApplicationContext) { } } - @Synchronized + /** + * Uses atomic operation to stop scanning + * Thread-safe: Can be called from any thread, camera operations executed on main thread + */ fun stopScanning() { - if (!isScanning) { + // Atomic CAS: only proceed if actually scanning + if (!isScanning.compareAndSet(true, false)) { Log.w(TAG, "Scanning is not in progress") return } try { Log.d(TAG, "Stopping scanning...") - isScanning = false - scanCallback = null + scanCallbackRef.set(null) - // Clear the analyzer to stop processing frames - imageAnalysis?.clearAnalyzer() - - // Unbind all use cases from the camera - cameraProvider?.unbindAll() - - // Clear references but don't shut down executor or scanner - // so they can be reused if scanning starts again - cameraControl = null - imageAnalysis = null - preview = null - - Log.d(TAG, "✅ Scanning stopped successfully") + // Execute camera operations on main thread to avoid IllegalStateException + ContextCompat.getMainExecutor(reactContext).execute { + try { + // Clear the analyzer to stop processing frames + imageAnalysis?.clearAnalyzer() + + // Unbind all use cases from the camera + cameraProvider?.unbindAll() + + // Clear references but DON'T shutdown executor or scanner + // They will be reused if scanning starts again + cameraControl = null + imageAnalysis = null + preview = null + + Log.d(TAG, "✅ Scanning stopped successfully (executor kept alive for reuse)") + } catch (e: Exception) { + Log.e(TAG, "Error stopping scanning on main thread: ${e.message}", e) + } + } } catch (e: Exception) { Log.e(TAG, "Error stopping scanning: ${e.message}", e) + // Ensure flag is still set to false even on error + isScanning.set(false) + throw e } } @@ -305,29 +415,63 @@ class CameraManager(private val reactContext: ReactApplicationContext) { try { Log.d(TAG, "Releasing camera resources...") - // Stop scanning first - if (isScanning) { - stopScanning() + // Stop scanning first using atomic operation + if (isScanning.compareAndSet(true, false)) { + scanCallbackRef.set(null) + } + + // Wait for binding to complete WITHOUT holding the lock + // This prevents deadlock when startScanning() is called during release + var attempts = 0 + while (isBinding && attempts < 100) { // Max wait: 5 seconds + Log.d(TAG, "âŗ Waiting for camera binding to complete before release...") + Thread.sleep(50) + attempts++ } - // Unbind all camera use cases - cameraProvider?.unbindAll() - + if (isBinding) { + Log.w(TAG, "âš ī¸ Binding still in progress after 5s, forcing release") + } + + // Now safely unbind with lock (binding should be complete) + cameraBindLock.withLock { + // Double-check binding state and unbind + if (isBinding) { + Log.w(TAG, "âš ī¸ Binding flag still set, unbinding anyway") + } + cameraProvider?.unbindAll() + } + // Clear all references cameraProvider = null cameraControl = null imageAnalysis = null preview = null previewView = null - scanCallback = null - + scanCallbackRef.set(null) + // Close the barcode scanner scanner?.close() scanner = null - - // Note: We don't shutdown cameraExecutor here because it's a singleton - // and we want to reuse it if scanning starts again - + + // Use dedicated executor lock for shutdown + // This prevents race with ensureExecutor() and getExecutorSafely() + executorLock.withLock { + if (!cameraExecutor.isShutdown) { + cameraExecutor.shutdown() + try { + if (!cameraExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + cameraExecutor.shutdownNow() + Log.w(TAG, "âš ī¸ Executor did not terminate gracefully, forced shutdown") + } + } catch (e: InterruptedException) { + cameraExecutor.shutdownNow() + Thread.currentThread().interrupt() + Log.w(TAG, "âš ī¸ Executor shutdown interrupted") + } + } + } + Log.d(TAG, "✅ Camera resources released successfully") } catch (e: Exception) { Log.e(TAG, "❌ Error releasing camera: ${e.message}", e) diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerModule.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerModule.kt index 0b40240..0e664b1 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerModule.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerModule.kt @@ -8,10 +8,10 @@ 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.modules.core.DeviceEventManagerModule import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionListener +import com.pushpendersingh.reactnativescanner.NativeReactNativeScannerSpec @ReactModule(name = ReactNativeScannerModule.NAME) class ReactNativeScannerModule(reactContext: ReactApplicationContext) : @@ -53,10 +53,7 @@ class ReactNativeScannerModule(reactContext: ReactApplicationContext) : } cameraManager.startScanning { result -> - // Send barcode result as event to JavaScript - reactApplicationContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit("onBarcodeScanned", result) + emitOnBarcodeScanned(result) } promise.resolve(null) } catch (e: Exception) { @@ -158,16 +155,6 @@ class ReactNativeScannerModule(reactContext: ReactApplicationContext) : } } - @ReactMethod - override fun addListener(eventName: String) { - // Required for event emitters - } - - @ReactMethod - override fun removeListeners(count: Double) { - // Required for event emitters - } - override fun invalidate() { super.invalidate() permissionPromise = null diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt index d1f653a..e02798c 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt @@ -12,6 +12,7 @@ class ReactNativeScannerView(context: Context) : FrameLayout(context) { private val previewView: PreviewView private var cameraManager: CameraManager? = null + private var layoutCallback: Choreographer.FrameCallback? = null init { // Create PreviewView @@ -30,13 +31,21 @@ class ReactNativeScannerView(context: Context) : FrameLayout(context) { } private fun setupLayoutHack() { - Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback { + layoutCallback = object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { manuallyLayoutChildren() viewTreeObserver.dispatchOnGlobalLayout() Choreographer.getInstance().postFrameCallback(this) } - }) + } + layoutCallback?.let { Choreographer.getInstance().postFrameCallback(it) } + } + + private fun removeLayoutCallback() { + layoutCallback?.let { + Choreographer.getInstance().removeFrameCallback(it) + layoutCallback = null + } } private fun manuallyLayoutChildren() { @@ -59,9 +68,17 @@ class ReactNativeScannerView(context: Context) : FrameLayout(context) { // Bind the preview view to the camera manager manager.bindPreviewView(previewView) } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + // Restart layout callback when view is reattached + setupLayoutHack() + } override fun onDetachedFromWindow() { super.onDetachedFromWindow() + // Remove Choreographer callback to prevent memory leak + removeLayoutCallback() // Clean up camera when view is detached cameraManager?.releaseCamera() } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3360670..8340652 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2340,7 +2340,7 @@ PODS: - React-perflogger (= 0.81.1) - React-utils (= 0.81.1) - SocketRocket - - ReactNativeScanner (1.6.0): + - ReactNativeScanner (1.9.0): - boost - DoubleConversion - fast_float @@ -2704,11 +2704,11 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 3eb9096cb139eb433965693bbe541d96eb3d3ec9 ReactCodegen: 4d203eddf6f977caa324640a20f92e70408d648b ReactCommon: ce5d4226dfaf9d5dacbef57b4528819e39d3a120 - ReactNativeScanner: 40e66f0f9bcf41dc30f6c4c6a655f492da7a7a4b + ReactNativeScanner: a12dcaf9a2b833163139c0372e17d819e60b7b99 RNPermissions: 380b0ddaff0bba3d4d0bbe4ed402044bc695752b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 11c9686a21e2cd82a094a723649d9f4507200fb0 PODFILE CHECKSUM: 39409e5f2910df3585d821342022cbe2b38bffab -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/CameraManager.swift b/ios/CameraManager.swift index 70bc75e..3f655d1 100644 --- a/ios/CameraManager.swift +++ b/ios/CameraManager.swift @@ -5,24 +5,97 @@ // Created by Pushpender Singh // -import Foundation @preconcurrency import AVFoundation +import Foundation @preconcurrency import Vision -@objc public class CameraManager: NSObject, @unchecked Sendable { - +// Actor that manages camera session state and operations +// Ensures all camera operations are thread-safe using Swift's actor isolation +actor CameraSessionActor { private var captureSession: AVCaptureSession? private var videoDevice: AVCaptureDevice? private var videoOutput: AVCaptureVideoDataOutput? - private let sessionQueue = DispatchQueue(label: "com.pushpendersingh.scanner.sessionQueue") private var isScanning = false + + func getSession() -> AVCaptureSession? { + return captureSession + } + + func setSession(_ session: AVCaptureSession?) { + captureSession = session + } + + func getVideoDevice() -> AVCaptureDevice? { + return videoDevice + } + + func setVideoDevice(_ device: AVCaptureDevice?) { + videoDevice = device + } + + func getVideoOutput() -> AVCaptureVideoDataOutput? { + return videoOutput + } + + func setVideoOutput(_ output: AVCaptureVideoDataOutput?) { + videoOutput = output + } + + func getScanningState() -> Bool { + return isScanning + } + + func setScanningState(_ scanning: Bool) { + isScanning = scanning + } + + // Eliminates race condition between check and set + func startScanningIfNotActive() -> Bool { + guard !isScanning else { return false } + isScanning = true + return true + } + + // Safe stop operation + func stopScanningIfActive() -> Bool { + guard isScanning else { return false } + isScanning = false + return true + } +} + +// Actor that manages scan callbacks thread-safely +actor CallbackActor { private var scanCallback: (([String: Any]) -> Void)? + func setCallback(_ callback: (([String: Any]) -> Void)?) { + scanCallback = callback + } + + func getCallback() -> (([String: Any]) -> Void)? { + return scanCallback + } + + func invokeCallback(with result: [String: Any]) { + if let callback = scanCallback { + Task { @MainActor in + callback(result) + } + } + } +} + +@objc public class CameraManager: NSObject { + + private let sessionActor = CameraSessionActor() + private let callbackActor = CallbackActor() + private let sessionQueue = DispatchQueue(label: "com.pushpendersingh.scanner.sessionQueue") + // Session interruption handling private var sessionInterruptionObserver: NSObjectProtocol? private var sessionInterruptionEndedObserver: NSObjectProtocol? @objc public var onSessionReady: ((AVCaptureSession) -> Void)? - + // Barcode types to detect private let supportedBarcodeTypes: [VNBarcodeSymbology] = [ .qr, @@ -36,55 +109,108 @@ import Foundation .ean8, .itf14, .pdf417, - .upce + .upce, ] - + @objc public func hasCameraPermission() -> Bool { let status = AVCaptureDevice.authorizationStatus(for: .video) return status == .authorized } - + @objc public func requestCameraPermission(completion: @escaping (Bool) -> Void) { AVCaptureDevice.requestAccess(for: .video) { granted in completion(granted) } } - - @objc public func currentSession() -> AVCaptureSession? { - // CHANGE: Synchronize the read on sessionQueue to avoid cross-thread access warnings. - // Reason: The session is produced/mutated on sessionQueue; reading it from main without - // synchronization can trigger structural concurrency diagnostics. - return sessionQueue.sync { captureSession } + + // Async version - don't block main thread + // Use this from Swift async contexts + public func getCurrentSession() async -> AVCaptureSession? { + return await sessionActor.getSession() } + // Callback version for Objective-C bridge + // Non-blocking alternative for synchronous contexts + @objc public func getCurrentSession(completion: @escaping (AVCaptureSession?) -> Void) { + Task { + let session = await sessionActor.getSession() + await MainActor.run { + completion(session) + } + } + } + @objc(startScanningWithCallback:error:) public func startScanning(callback: @escaping ([String: Any]) -> Void) throws { - guard hasCameraPermission() else { - throw NSError(domain: "CameraManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Camera permission not granted"]) + // Check permission status immediately, but asynchronously request if needed. + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + // Permission already granted. Proceed with scanning setup. + sessionQueue.async { [weak self] in + guard let self = self else { return } + Task { + await self.configureAndStartScanning(callback: callback) + } + } + case .notDetermined: + // Request permission. The result is handled asynchronously. + AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in + guard let self = self else { return } + self.sessionQueue.async { + Task { + if granted { + await self.configureAndStartScanning(callback: callback) + } else { + // Handle denial. + 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"] + Task { + await callbackActor.invokeCallback(with: errorInfo) + } + @unknown default: + // Handle any future unknown cases. + let errorInfo = ["error": "Unknown camera permission status"] + Task { + await callbackActor.invokeCallback(with: errorInfo) + } + } + } + + // Private helper with atomic check-and-set + // Eliminates race condition in session initialization + 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") + await callbackActor.setCallback(callback) + return } - // Execute on sessionQueue for thread safety - sessionQueue.async { [weak self] in - guard let self = self else { return } - - // Check if session is already running - if self.isScanning, let session = self.captureSession, session.isRunning { - print("âš ī¸ Session already running, updating callback only") - self.scanCallback = callback - return - } - - // Set flag on the same queue where it's checked - self.isScanning = true - self.scanCallback = callback - - self.setupCaptureSession() + // At this point, we're guaranteed to be the only thread starting scanning + await callbackActor.setCallback(callback) + + do { + try await setupCaptureSession() + } catch { + // Reset state on error + await sessionActor.setScanningState(false) + await callbackActor.setCallback(nil) + print("❌ Failed to setup camera session: \(error.localizedDescription)") } } - - private func setupCaptureSession() { + + private func setupCaptureSession() async throws { // Reuse existing session if available - if let existingSession = captureSession { + let existingSession = await sessionActor.getSession() + + if let existingSession = existingSession { // If session exists but not running, just restart it if !existingSession.isRunning { print("â™ģī¸ Restarting existing session") @@ -94,27 +220,29 @@ import Foundation print("âš ī¸ Session already running") } if let onSessionReady = self.onSessionReady { - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard self != nil else { return } onSessionReady(existingSession) } } return } - + // Create new session only if none exists let newSession = AVCaptureSession() - + newSession.beginConfiguration() newSession.sessionPreset = .high - + // Setup video device - guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { + guard let videoDevice = AVCaptureDevice.default( + .builtInWideAngleCamera, for: .video, position: .back) + else { print("❌ Failed to get video device") - isScanning = false - return + throw CameraError.deviceNotAvailable } - self.videoDevice = videoDevice - + await sessionActor.setVideoDevice(videoDevice) + // Setup video input do { let videoInput = try AVCaptureDeviceInput(device: videoDevice) @@ -122,118 +250,157 @@ import Foundation newSession.addInput(videoInput) } else { print("❌ Cannot add video input") - isScanning = false - return + throw CameraError.cannotAddInput } } catch { print("❌ Error creating video input: \(error.localizedDescription)") - isScanning = false - return + throw error } - + // Setup video output let videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(self, queue: sessionQueue) videoOutput.alwaysDiscardsLateVideoFrames = true - + if newSession.canAddOutput(videoOutput) { newSession.addOutput(videoOutput) - self.videoOutput = videoOutput + await sessionActor.setVideoOutput(videoOutput) } else { print("❌ Cannot add video output") - isScanning = false - return + throw CameraError.cannotAddOutput } - + newSession.commitConfiguration() - - // Assign to property AFTER configuration - self.captureSession = newSession - + + // Assign to actor AFTER configuration + await sessionActor.setSession(newSession) + // Start session (already on sessionQueue) newSession.startRunning() print("✅ Camera session started - Running: \(newSession.isRunning)") - print("✅ Video output delegate set: \(self.videoOutput?.sampleBufferDelegate != nil)") - // Notify UI after the session is running to avoid startRunning during configuration + let output = await sessionActor.getVideoOutput() + print("✅ Video output delegate set: \(output?.sampleBufferDelegate != nil)") + + // Notify UI after the session is running if let onSessionReady = self.onSessionReady { - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard self != nil else { return } onSessionReady(newSession) } } } + // Camera error types for better error handling + enum CameraError: Error { + case deviceNotAvailable + case cannotAddInput + case cannotAddOutput + case sessionNotRunning + case torchUnavailable + + var localizedDescription: String { + switch self { + case .deviceNotAvailable: + return "Camera device not available" + case .cannotAddInput: + return "Cannot add video input to session" + case .cannotAddOutput: + return "Cannot add video output to session" + case .sessionNotRunning: + return "Camera session is not running" + case .torchUnavailable: + return "Torch/flashlight is not available on this device" + } + } + } + @objc public func stopScanning() { sessionQueue.async { [weak self] in guard let self = self else { return } - - // Set flag first, then stop session - self.isScanning = false - self.scanCallback = nil - - if let session = self.captureSession, session.isRunning { - session.stopRunning() - print("✅ Scanning stopped") - } else { - print("âš ī¸ No active session to stop") + + Task { + // ✅ Use atomic operation to stop scanning + guard await self.sessionActor.stopScanningIfActive() else { + print("âš ī¸ Scanning is not active") + return + } + + await self.callbackActor.setCallback(nil) + + let session = await self.sessionActor.getSession() + if let session = session, session.isRunning { + session.stopRunning() + print("✅ Scanning stopped") + } else { + print("âš ī¸ No active session to stop") + } } } } - + @objc public func enableFlashlight() { // Execute on sessionQueue for thread safety sessionQueue.async { [weak self] in guard let self = self else { return } - - // Check if session is running - guard let session = self.captureSession, session.isRunning else { - print("âš ī¸ Cannot enable flashlight - camera not running") - return - } - - guard let device = self.videoDevice, device.hasTorch else { - print("âš ī¸ Torch not available") - return - } - - do { - try device.lockForConfiguration() - device.torchMode = .on - device.unlockForConfiguration() - print("✅ Flashlight enabled") - } catch { - print("❌ Failed to enable torch: \(error.localizedDescription)") + + Task { + // Check if session is running + let session = await self.sessionActor.getSession() + guard let session = session, session.isRunning else { + print("âš ī¸ Cannot enable flashlight - camera not running") + return + } + + let device = await self.sessionActor.getVideoDevice() + guard let device = device, device.hasTorch else { + print("âš ī¸ Torch not available") + return + } + + do { + try device.lockForConfiguration() + device.torchMode = .on + device.unlockForConfiguration() + print("✅ Flashlight enabled") + } catch { + print("❌ Failed to enable torch: \(error.localizedDescription)") + } } } } - + @objc public func disableFlashlight() { // Execute on sessionQueue for thread safety sessionQueue.async { [weak self] in guard let self = self else { return } - - // Check if session is running - guard let session = self.captureSession, session.isRunning else { - print("âš ī¸ Cannot disable flashlight - camera not running") - return - } - - guard let device = self.videoDevice, device.hasTorch else { - print("âš ī¸ Torch not available") - return - } - - do { - try device.lockForConfiguration() - device.torchMode = .off - device.unlockForConfiguration() - print("✅ Flashlight disabled") - } catch { - print("❌ Failed to disable torch: \(error.localizedDescription)") + + Task { + // Check if session is running + let session = await self.sessionActor.getSession() + guard let session = session, session.isRunning else { + print("âš ī¸ Cannot disable flashlight - camera not running") + return + } + + let device = await self.sessionActor.getVideoDevice() + guard let device = device, device.hasTorch else { + print("âš ī¸ Torch not available") + return + } + + do { + try device.lockForConfiguration() + device.torchMode = .off + device.unlockForConfiguration() + print("✅ Flashlight disabled") + } catch { + print("❌ Failed to disable torch: \(error.localizedDescription)") + } } } } - + // Setup interruption observers in init public override init() { super.init() @@ -242,7 +409,7 @@ import Foundation self.setupInterruptionObservers() } } - + @MainActor private func setupInterruptionObservers() { // Handle session interruption (e.g., incoming call, alarm) @@ -252,18 +419,20 @@ import Foundation queue: nil ) { [weak self] notification in guard let self = self else { return } - - if let reasonValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int, - let reason = AVCaptureSession.InterruptionReason(rawValue: reasonValue) { + + if let reasonValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] + as? Int, + let reason = AVCaptureSession.InterruptionReason(rawValue: reasonValue) + { print("âš ī¸ Session interrupted: \(reason)") - - // Optionally pause scanning during interruption - sessionQueue.async { - self.isScanning = false + + // Pause scanning during interruption + Task { + await self.sessionActor.setScanningState(false) } } } - + // Handle session interruption ended sessionInterruptionEndedObserver = NotificationCenter.default.addObserver( forName: .AVCaptureSessionInterruptionEnded, @@ -272,18 +441,25 @@ import Foundation ) { [weak self] _ in guard let self = self else { return } print("✅ Session interruption ended") - - // Resume scanning only if we had a callback (means scanning was active) - self.sessionQueue.async { - if self.scanCallback != nil, let session = self.captureSession, !session.isRunning { + + // Resume scanning only if we were actually scanning before interruption + Task { + // Check scanning state first to prevent restarting released session + let isScanning = await self.sessionActor.getScanningState() + let callback = await self.callbackActor.getCallback() + let session = await self.sessionActor.getSession() + + // Only resume if still supposed to be scanning + if isScanning, callback != nil, let session = session, !session.isRunning { session.startRunning() - self.isScanning = true print("✅ Scanning resumed after interruption") + } else if !isScanning { + print("â„šī¸ Not resuming - scanning was stopped during interruption") } } } } - + @objc public func releaseCamera() { // Remove observers on main thread to keep lifecycle consistent DispatchQueue.main.async { [weak self] in @@ -297,45 +473,52 @@ import Foundation self.sessionInterruptionEndedObserver = nil } } - + sessionQueue.async { [weak self] in guard let self = self else { return } - - // Stop scanning first - self.isScanning = false - self.scanCallback = nil - - // Stop session before modifying it - if let session = self.captureSession { - if session.isRunning { - session.stopRunning() - } - - // Use beginConfiguration when removing inputs/outputs - session.beginConfiguration() - - for input in session.inputs { - session.removeInput(input) - } - for output in session.outputs { - session.removeOutput(output) + + Task { + // Stop scanning first + await self.sessionActor.setScanningState(false) + await self.callbackActor.setCallback(nil) + + // Stop session before modifying it + let session = await self.sessionActor.getSession() + if let session = session { + if session.isRunning { + session.stopRunning() + } + + // Use beginConfiguration when removing inputs/outputs + session.beginConfiguration() + + for input in session.inputs { + session.removeInput(input) + } + for output in session.outputs { + session.removeOutput(output) + } + + session.commitConfiguration() } - - session.commitConfiguration() + + await self.sessionActor.setSession(nil) + await self.sessionActor.setVideoDevice(nil) + await self.sessionActor.setVideoOutput(nil) + + print("✅ Camera resources released") } - - self.captureSession = nil - self.videoDevice = nil - self.videoOutput = nil - - print("✅ Camera resources released") } - + + // Clear the onSessionReady callback to prevent retain cycles + onSessionReady = nil + + // Re-setup observers for next use Task { @MainActor in self.setupInterruptionObservers() } } - + // Deinit to cleanup observers deinit { if let observer = sessionInterruptionObserver { @@ -348,77 +531,73 @@ import Foundation } } -// MARK: - AVCaptureVideoDataOutputSampleBufferDelegate +// AVCaptureVideoDataOutputSampleBufferDelegate extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { - - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { - // Early exit if not scanning - guard isScanning else { return } - - guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { - print("âš ī¸ Failed to get pixel buffer") - return - } - - let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) - - let barcodeRequest = VNDetectBarcodesRequest { [weak self] request, error in - guard let self = self else { return } - - // Double-check isScanning inside async callback - guard self.isScanning else { - print("âš ī¸ Barcode detected but scanning stopped, ignoring") - return - } - - if let error = error { - print("Barcode detection error: \(error.localizedDescription)") - return - } - - guard let results = request.results as? [VNBarcodeObservation], - let firstBarcode = results.first, - let payloadString = firstBarcode.payloadStringValue else { - return - } - - print("✅ Barcode detected: \(payloadString) - Type: \(firstBarcode.symbology.rawValue)") - - // Create result dictionary - let result = self.createBarcodeResult(barcode: firstBarcode, payloadString: payloadString) - - // Capture callback safely - guard let callback = self.scanCallback else { - print("âš ī¸ No callback available") + + public func captureOutput( + _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { + // Check scanning state using async context + Task { + let isCurrentlyScanning = await sessionActor.getScanningState() + guard isCurrentlyScanning else { return } + + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + print("âš ī¸ Failed to get pixel buffer") return } - - // Call the callback on main thread - DispatchQueue.main.async { - if self.isScanning { - callback(result) + + let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) + + let barcodeRequest = VNDetectBarcodesRequest { [weak self] request, error in + guard let self = self else { return } + + Task { + // Double-check isScanning inside async callback + let stillScanning = await self.sessionActor.getScanningState() + guard stillScanning else { return } + + if let error = error { + print("Barcode detection error: \(error.localizedDescription)") + return + } + + guard let results = request.results as? [VNBarcodeObservation], + let firstBarcode = results.first, + let payloadString = firstBarcode.payloadStringValue + 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") - } else { - print("âš ī¸ Scanning stopped before callback, ignoring") } } - } - - barcodeRequest.symbologies = supportedBarcodeTypes - - do { - try imageRequestHandler.perform([barcodeRequest]) - } catch { - print("Failed to perform barcode detection: \(error.localizedDescription)") + + barcodeRequest.symbologies = supportedBarcodeTypes + + do { + try imageRequestHandler.perform([barcodeRequest]) + } catch { + print("Failed to perform barcode detection: \(error.localizedDescription)") + } } } - - private func createBarcodeResult(barcode: VNBarcodeObservation, payloadString: String) -> [String: Any] { + + private func createBarcodeResult(barcode: VNBarcodeObservation, payloadString: String) + -> [String: Any] + { var result: [String: Any] = [ "data": payloadString, - "type": getBarcodeTypeName(barcode.symbology) + "type": getBarcodeTypeName(barcode.symbology), ] - + // Add bounds if available let boundingBox = barcode.boundingBox let bounds: [String: Any] = [ @@ -428,15 +607,15 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { "topLeft": ["x": boundingBox.minX, "y": boundingBox.maxY], "bottomLeft": ["x": boundingBox.minX, "y": boundingBox.minY], "bottomRight": ["x": boundingBox.maxX, "y": boundingBox.minY], - "topRight": ["x": boundingBox.maxX, "y": boundingBox.maxY] - ] + "topRight": ["x": boundingBox.maxX, "y": boundingBox.maxY], + ], ] - + result["bounds"] = bounds - + return result } - + private func getBarcodeTypeName(_ symbology: VNBarcodeSymbology) -> String { switch symbology { case .qr: @@ -468,4 +647,3 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { } } } - diff --git a/ios/CameraView.swift b/ios/CameraView.swift index 91f8cc6..23f6b80 100644 --- a/ios/CameraView.swift +++ b/ios/CameraView.swift @@ -44,7 +44,8 @@ public class CameraView: UIView { // keeping UI work on main and avoiding concurrency violations. manager.onSessionReady = { [weak self] session in guard let self = self else { return } - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } if let previewLayer = self.previewLayer { previewLayer.session = session previewLayer.connection?.videoOrientation = .portrait @@ -55,11 +56,11 @@ public class CameraView: UIView { } } - // CHANGE: If a session already exists, bind it immediately on the main thread. - // Reason: Ensures the preview shows even if the session was created before the view. - if let existingSession = manager.currentSession() { - DispatchQueue.main.async { [weak self] in - guard let self = self, let previewLayer = self.previewLayer else { return } + // Use non-blocking callback version + // Reason: Avoids blocking main thread with semaphore + manager.getCurrentSession { [weak self] existingSession in + guard let self = self, let previewLayer = self.previewLayer else { return } + if let existingSession = existingSession { previewLayer.session = existingSession previewLayer.connection?.videoOrientation = .portrait print("✅ Preview layer bound to existing session") @@ -79,6 +80,12 @@ public class CameraView: UIView { previewLayer?.session = nil previewLayer?.removeFromSuperlayer() previewLayer = nil + + // Clear the onSessionReady callback to prevent potential retain cycles + cameraManager?.onSessionReady = nil + cameraManager = nil + + print("đŸ—‘ī¸ CameraView deallocated") } } diff --git a/ios/CameraViewManager.mm b/ios/CameraViewManager.mm index 1598df5..de0f8ab 100644 --- a/ios/CameraViewManager.mm +++ b/ios/CameraViewManager.mm @@ -5,18 +5,17 @@ // Created by Pushpender Singh // -#import -#import -#import #import "ReactNativeScanner.h" +#import +#import // Import the Swift bridging header #if __has_include() - // For dynamic frameworks or when installed as a framework - #import +// For dynamic frameworks or when installed as a framework +#import #else - // For static libraries or when included directly in the project - #import "ReactNativeScanner-Swift.h" +// For static libraries or when included directly in the project +#import "ReactNativeScanner-Swift.h" #endif @interface ReactNativeScannerViewManager : RCTViewManager @@ -26,26 +25,20 @@ @implementation ReactNativeScannerViewManager RCT_EXPORT_MODULE(ReactNativeScannerView) -- (UIView *)view -{ - CameraView *cameraView = [[CameraView alloc] init]; - - // Bind immediately on the current thread (main queue) - // Since requiresMainQueueSetup returns YES, we're already on main queue - ReactNativeScanner *scannerModule = [self.bridge moduleForClass:[ReactNativeScanner class]]; - if (scannerModule && scannerModule.cameraManager) { - [cameraView setCameraManager:scannerModule.cameraManager]; - NSLog(@"✅ Camera manager bound to view"); - } else { - NSLog(@"âš ī¸ Scanner module or camera manager not available"); - } - - return cameraView; +- (UIView *)view { + CameraView *cameraView = [[CameraView alloc] init]; + + ReactNativeScanner *scannerModule = + [self.bridge moduleForClass:[ReactNativeScanner class]]; + if (scannerModule && scannerModule.cameraManager) { + [cameraView setCameraManager:scannerModule.cameraManager]; + } + + return cameraView; } -+ (BOOL)requiresMainQueueSetup -{ - return YES; ++ (BOOL)requiresMainQueueSetup { + return YES; } @end diff --git a/ios/ReactNativeScanner-Bridging-Header.h b/ios/ReactNativeScanner-Bridging-Header.h index d3e9f4c..c628714 100644 --- a/ios/ReactNativeScanner-Bridging-Header.h +++ b/ios/ReactNativeScanner-Bridging-Header.h @@ -5,8 +5,6 @@ // Created by Pushpender Singh // -#import -#import #import #import #import diff --git a/ios/ReactNativeScanner.h b/ios/ReactNativeScanner.h index 602780b..cb8ea2c 100644 --- a/ios/ReactNativeScanner.h +++ b/ios/ReactNativeScanner.h @@ -1,10 +1,12 @@ -#import +#import #import @class CameraManager; -@interface ReactNativeScanner : RCTEventEmitter +@interface ReactNativeScanner : NativeReactNativeScannerSpecBase @property (nonatomic, strong, readonly) CameraManager *cameraManager; @end + + diff --git a/ios/ReactNativeScanner.mm b/ios/ReactNativeScanner.mm index e1dffac..61036f7 100644 --- a/ios/ReactNativeScanner.mm +++ b/ios/ReactNativeScanner.mm @@ -7,13 +7,8 @@ #import #endif -@interface ReactNativeScanner () -@property(nonatomic, strong) CameraManager *cameraManager; -@end - @implementation ReactNativeScanner - -RCT_EXPORT_MODULE() +@synthesize cameraManager = _cameraManager; - (instancetype)init { if (self = [super init]) { @@ -27,10 +22,6 @@ - (CameraManager *)cameraManager { return _cameraManager; } -- (NSArray *)supportedEvents { - return @[ @"onBarcodeScanned" ]; -} - - (void)startScanning:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { if (![_cameraManager hasCameraPermission]) { @@ -42,7 +33,7 @@ - (void)startScanning:(RCTPromiseResolveBlock)resolve @try { [_cameraManager startScanningWithCallback:^(NSDictionary *result) { - [self sendEventWithName:@"onBarcodeScanned" body:result]; + [self emitOnBarcodeScanned:result]; } error:&error]; if (error) { @@ -108,32 +99,26 @@ - (void)requestCameraPermission:(RCTPromiseResolveBlock)resolve }]; } -- (void)addListener:(NSString *)eventName { - // Required for RCTEventEmitter - // Keep track of listeners to avoid "no listeners" warning - [super addListener:eventName]; -} - -- (void)removeListeners:(double)count { - // Required for RCTEventEmitter - [super removeListeners:count]; -} - - (void)invalidate { - // Capture strong reference to camera manager first to ensure it stays alive during cleanup + // Capture strong reference to camera manager first to ensure it stays alive + // during cleanup CameraManager *cameraManager = _cameraManager; - + if (cameraManager) { [cameraManager releaseCamera]; } - - [super invalidate]; } - (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared( - params); + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + ++ (NSString *)moduleName +{ + return @"ReactNativeScanner"; } @end + diff --git a/src/CameraView.tsx b/src/CameraView.tsx index 0141a8d..6618d4b 100644 --- a/src/CameraView.tsx +++ b/src/CameraView.tsx @@ -1,18 +1,14 @@ import React from 'react'; -import { requireNativeComponent } from 'react-native'; import type { ViewProps } from 'react-native'; +import ReactNativeScannerViewNativeComponent from './ReactNativeScannerViewNativeComponent'; export interface CameraViewProps extends ViewProps { style?: ViewProps['style']; } -const NativeCameraView = requireNativeComponent( - 'ReactNativeScannerView' -); - export const CameraView = React.forwardRef( (props, ref) => { - return ; + return ; } ); diff --git a/src/NativeReactNativeScanner.ts b/src/NativeReactNativeScanner.ts index 1202d57..cff7c8e 100644 --- a/src/NativeReactNativeScanner.ts +++ b/src/NativeReactNativeScanner.ts @@ -1,6 +1,21 @@ -import type { TurboModule } from 'react-native'; +import type { CodegenTypes, TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; +export type BarcodeScannedEvent = { + data: string; + type: string; + bounds?: { + width: number; + height: number; + origin: { + topLeft: { x: number; y: number }; + bottomLeft: { x: number; y: number }; + bottomRight: { x: number; y: number }; + topRight: { x: number; y: number }; + }; + }; +}; + export interface Spec extends TurboModule { // Start scanning - results will be emitted via events startScanning(): Promise; @@ -23,9 +38,7 @@ export interface Spec extends TurboModule { // Request camera permission requestCameraPermission(): Promise; - // Add event emitter support - addListener(eventName: string): void; - removeListeners(count: number): void; + readonly onBarcodeScanned: CodegenTypes.EventEmitter; } export default TurboModuleRegistry.getEnforcing('ReactNativeScanner'); diff --git a/src/ReactNativeScannerViewNativeComponent.ts b/src/ReactNativeScannerViewNativeComponent.ts index 16f542a..bc6a001 100644 --- a/src/ReactNativeScannerViewNativeComponent.ts +++ b/src/ReactNativeScannerViewNativeComponent.ts @@ -1,5 +1,5 @@ import type { ViewProps, HostComponent } from 'react-native'; -import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import { codegenNativeComponent } from 'react-native'; export interface NativeProps extends ViewProps { // Add any custom props here if needed in the future diff --git a/src/index.tsx b/src/index.tsx index 711f0b4..8cdc38e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,29 +1,5 @@ -import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import NativeReactNativeScanner from './NativeReactNativeScanner'; -// Augment global type for TurboModule detection -declare global { - // eslint-disable-next-line no-var - var __turboModuleProxy: any | undefined; -} - -const LINKING_ERROR = - `The package '@pushpendersingh/react-native-scanner' doesn't seem to be linked. Make sure: \n\n` + - Platform.select({ ios: "- Run 'pod install'\n", default: '' }) + - '- Rebuild the app'; - -const isTurboModuleEnabled = global.__turboModuleProxy != null; - -const ReactNativeScannerModule = isTurboModuleEnabled - ? NativeReactNativeScanner - : NativeModules.ReactNativeScanner; - -if (!ReactNativeScannerModule) { - throw new Error(LINKING_ERROR); -} - -const eventEmitter = new NativeEventEmitter(ReactNativeScannerModule); - // Types export type BarcodeType = | 'QR_CODE' @@ -62,88 +38,51 @@ export type BarcodeScannerCallback = (result: BarcodeResult) => void; export class BarcodeScanner { private static listener: any = null; - /** - * Start scanning for barcodes - * @param callback Function to call when a barcode is detected - * @returns Promise that resolves when scanning starts - */ static async startScanning(callback: BarcodeScannerCallback): Promise { - // Remove any existing listener first + // Remove existing listener if (this.listener) { this.listener.remove(); this.listener = null; } - // Add new listener BEFORE starting scanning - this.listener = eventEmitter.addListener( - 'onBarcodeScanned', - (result: any) => { - console.log('📱 Event received in JS:', result); - callback(result as BarcodeResult); - } - ); - - // Small delay to ensure listener is fully registered - await new Promise((resolve) => setTimeout(resolve, 10)); + this.listener = NativeReactNativeScanner.onBarcodeScanned((event) => { + callback(event as BarcodeResult); + }); // Start scanning - return ReactNativeScannerModule.startScanning(); + return NativeReactNativeScanner.startScanning(); } - /** - * Stop scanning for barcodes - * @returns Promise that resolves when scanning stops - */ static async stopScanning(): Promise { if (this.listener) { this.listener.remove(); this.listener = null; } - return ReactNativeScannerModule.stopScanning(); + return NativeReactNativeScanner.stopScanning(); } - /** - * Enable device flashlight/torch - * @returns Promise that resolves when flashlight is enabled - */ static async enableFlashlight(): Promise { - return ReactNativeScannerModule.enableFlashlight(); + return NativeReactNativeScanner.enableFlashlight(); } - /** - * Disable device flashlight/torch - * @returns Promise that resolves when flashlight is disabled - */ static async disableFlashlight(): Promise { - return ReactNativeScannerModule.disableFlashlight(); + return NativeReactNativeScanner.disableFlashlight(); } - /** - * Release camera resources - * @returns Promise that resolves when camera is released - */ static async releaseCamera(): Promise { if (this.listener) { this.listener.remove(); this.listener = null; } - return ReactNativeScannerModule.releaseCamera(); + return NativeReactNativeScanner.releaseCamera(); } - /** - * Check if camera permission is granted - * @returns Promise that resolves with permission status - */ static async hasCameraPermission(): Promise { - return ReactNativeScannerModule.hasCameraPermission(); + return NativeReactNativeScanner.hasCameraPermission(); } - /** - * Request camera permission - * @returns Promise that resolves with whether permission was granted - */ static async requestCameraPermission(): Promise { - return ReactNativeScannerModule.requestCameraPermission(); + return NativeReactNativeScanner.requestCameraPermission(); } } diff --git a/yarn.lock b/yarn.lock index 1bdd3c7..1c31458 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,19 +5,28 @@ __metadata: version: 6 cacheKey: 8 -"@ark/schema@npm:0.49.0": - version: 0.49.0 - resolution: "@ark/schema@npm:0.49.0" +"@ark/regex@npm:0.0.0": + version: 0.0.0 + resolution: "@ark/regex@npm:0.0.0" dependencies: - "@ark/util": 0.49.0 - checksum: 9901123581afa0eef63305fc47a1a725ff17c8958a80694464b0d08d3c398be26160763fed91864b8f8fb9553f3bf57d7075e436b6f7902220074f86310ee9d8 + "@ark/util": 0.50.0 + checksum: b89d50a610393a4025a0e2cb4444c16c4f2fb16708ee6e4afe36160ee3503c3a7a5df8a7477bbf4b75099509329fc62f388f64819002d2f93642b2188618b5e5 languageName: node linkType: hard -"@ark/util@npm:0.49.0": - version: 0.49.0 - resolution: "@ark/util@npm:0.49.0" - checksum: 01ae677327cd585d9bbdc9373d5d5d70e10a14be151976c7d86f27cc7289d6e4d51e3da3993c69aed1657f3aa4abe409834e6338a7a7391a30209fa34c066c14 +"@ark/schema@npm:0.50.0": + version: 0.50.0 + resolution: "@ark/schema@npm:0.50.0" + dependencies: + "@ark/util": 0.50.0 + checksum: 6a080104865ec4a0be91d6bffab95f69923f4a85b6087f67cf04555b30b65544084eeebbfa4cf9759ec27b964b0fc4dc7e19603b055b472a096463f13c084343 + languageName: node + linkType: hard + +"@ark/util@npm:0.50.0": + version: 0.50.0 + resolution: "@ark/util@npm:0.50.0" + checksum: 50aa1d506bbf70ef502f0f424370ab831fcb891f5a71fdec51c46d06c504eab751ccfa2920bbeae7c97d22bcfc71c755a29121489984eacf052040c61c696fc9 languageName: node linkType: hard @@ -1775,19 +1784,12 @@ __metadata: languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.3.1": - version: 0.3.1 - resolution: "@eslint/config-helpers@npm:0.3.1" - checksum: b95c239264078a430761afb344402d517134289a7d8b69a6ff1378ebe5eec9da6ad22b5e6d193b9e02899aeda30817ac47178d5927247092cc6d73a52f8d07c9 - languageName: node - linkType: hard - -"@eslint/core@npm:^0.15.2": - version: 0.15.2 - resolution: "@eslint/core@npm:0.15.2" +"@eslint/config-helpers@npm:^0.4.0": + version: 0.4.0 + resolution: "@eslint/config-helpers@npm:0.4.0" dependencies: - "@types/json-schema": ^7.0.15 - checksum: 535fc4e657760851826ceae325a72dde664b99189bd975715de3526db655c66d7a35b72dbb1c7641ab9201ed4e2130f79c5be51f96c820b5407c3766dcf94f23 + "@eslint/core": ^0.16.0 + checksum: f17af9d6de60e0d8be5131451ef489f32984f92aff00cb1c5c8f1790baf07ea7ad803e0f21f1519eded4ce247871ffe593b7e51ddc094b5337d22f29dd720ba5 languageName: node linkType: hard @@ -1817,10 +1819,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.36.0, @eslint/js@npm:^9.35.0": - version: 9.36.0 - resolution: "@eslint/js@npm:9.36.0" - checksum: 17ff28272337357783b55e76417e61306e528dced99bb49d49e06298023b4071cb30f4aeb0bf30a337817d3eb3132784db6b8edd3a90118c5217833136712713 +"@eslint/js@npm:9.37.0, @eslint/js@npm:^9.35.0": + version: 9.37.0 + resolution: "@eslint/js@npm:9.37.0" + checksum: 916f2ff7f70eadaa3a1c3f7d6d375fccfb676723484e1c54c5d63ff8a462746090097b73d21f4cb876ff2276d04af3f1c4c9e9a93729a9305213ca3aaa75008c languageName: node linkType: hard @@ -1831,22 +1833,22 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.3.5": - version: 0.3.5 - resolution: "@eslint/plugin-kit@npm:0.3.5" +"@eslint/plugin-kit@npm:^0.4.0": + version: 0.4.0 + resolution: "@eslint/plugin-kit@npm:0.4.0" dependencies: - "@eslint/core": ^0.15.2 + "@eslint/core": ^0.16.0 levn: ^0.4.1 - checksum: 1808d7e2538335b8e4536ef372840e93468ecc6f4a5bf72ad665795290b6a8a72f51ef4ffd8bcfc601b133a5d5f67b59ab256d945f8c825c5c307aad29efaf86 + checksum: bb82be19c99eea256f7ec8e0996d28bd4b95b796bd1b27659b92e83278ef813485ada55995314887e7812cca02b0a9672d63f547c2a110eb5a7f0022c8e0f23d languageName: node linkType: hard "@evilmartians/lefthook@npm:^1.12.3": - version: 1.13.5 - resolution: "@evilmartians/lefthook@npm:1.13.5" + version: 1.13.6 + resolution: "@evilmartians/lefthook@npm:1.13.6" bin: lefthook: bin/index.js - checksum: 139f12fec89c1517c4fe26bb9c4abc5ee1fd75f39314bbcfe749f9f93a6fa071b851d77b8650a6c64b0da24821e5a024108ecd995627c0cee35f6bb1bc875952 + checksum: 6cceca3e874015678f50818ae14a74d959816cfaba6638f8852d007332404d6819b15c71538985a3650a1ef057aa6975c17fadfe43ece7a0da1aeb9faaf02946 conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=ia32) languageName: node linkType: hard @@ -1905,53 +1907,53 @@ __metadata: languageName: node linkType: hard -"@inquirer/ansi@npm:^1.0.0": - version: 1.0.0 - resolution: "@inquirer/ansi@npm:1.0.0" - checksum: 153b619c1178ece3e28a66ab41b7827b9ee64c84180f779bcc1c38c8c3e87979130bba109dd7e648ccdd3786da75c4a3a0945e816dc6afec9219f54ac7fbbb69 +"@inquirer/ansi@npm:^1.0.0, @inquirer/ansi@npm:^1.0.1": + version: 1.0.1 + resolution: "@inquirer/ansi@npm:1.0.1" + checksum: 0dda65720736f3e730715f3778e0e90f039ebd1382c277495a4d1cdbd2b2863095aa7291cd8ea7d3c0618bdee04a375db6e10a7bae5fb904df0b632a1c7774f9 languageName: node linkType: hard -"@inquirer/checkbox@npm:^4.2.4": - version: 4.2.4 - resolution: "@inquirer/checkbox@npm:4.2.4" +"@inquirer/checkbox@npm:^4.3.0": + version: 4.3.0 + resolution: "@inquirer/checkbox@npm:4.3.0" dependencies: - "@inquirer/ansi": ^1.0.0 - "@inquirer/core": ^10.2.2 - "@inquirer/figures": ^1.0.13 - "@inquirer/type": ^3.0.8 + "@inquirer/ansi": ^1.0.1 + "@inquirer/core": ^10.3.0 + "@inquirer/figures": ^1.0.14 + "@inquirer/type": ^3.0.9 yoctocolors-cjs: ^2.1.2 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 4e55f2795016c63f1ee0d9c48a83d067f485f23fb2fe404eeb47c239031b12f5c3c20ec5e26f99c5c103bb47ce341cef918038268bee9e70d512425614d56840 + checksum: d4957d0ce205c5c0bc70eb9491ca4ebe983cce0abaf552cc8ad521179db94841fb25603121d0af1b31757bb8381377a410c21cde2a48754af18f694b31477c14 languageName: node linkType: hard -"@inquirer/confirm@npm:^5.1.18": - version: 5.1.18 - resolution: "@inquirer/confirm@npm:5.1.18" +"@inquirer/confirm@npm:^5.1.19": + version: 5.1.19 + resolution: "@inquirer/confirm@npm:5.1.19" dependencies: - "@inquirer/core": ^10.2.2 - "@inquirer/type": ^3.0.8 + "@inquirer/core": ^10.3.0 + "@inquirer/type": ^3.0.9 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 59a27eedf9b8e5ff1ca5eb738121caf56c94d9ec80f0ff02021300a7894c608e9c32e06b79ba21714f6977a277c84025e62b141c50c580be2a30697f52ef4941 + checksum: d65e0addf80c146d71a74057d77048bd78a4a80d74a9e0d774b759ff1adf38a33cde6c06a6d6ef802bb61ef9158770315dec3931f89b3624c0e63c595c0473c1 languageName: node linkType: hard -"@inquirer/core@npm:^10.2.2": - version: 10.2.2 - resolution: "@inquirer/core@npm:10.2.2" +"@inquirer/core@npm:^10.2.2, @inquirer/core@npm:^10.3.0": + version: 10.3.0 + resolution: "@inquirer/core@npm:10.3.0" dependencies: - "@inquirer/ansi": ^1.0.0 - "@inquirer/figures": ^1.0.13 - "@inquirer/type": ^3.0.8 + "@inquirer/ansi": ^1.0.1 + "@inquirer/figures": ^1.0.14 + "@inquirer/type": ^3.0.9 cli-width: ^4.1.0 mute-stream: ^2.0.0 signal-exit: ^4.1.0 @@ -1962,39 +1964,39 @@ __metadata: peerDependenciesMeta: "@types/node": optional: true - checksum: 79d528ecb5f485a0f63bd5e48273a2ffba9457240e2a1971da8ea97a97c8398b932260691b8d5e5134f306ba8b08ce7a4800dfa7bd8cc9143a86760714215927 + checksum: 42607c2e8388bf6505f5ce1716d47750f9386085f3080733b7f27bfe59d576d480ec622d7468fcf1bd9b854ff117311421d9eae0c083873c67324023635e103a languageName: node linkType: hard -"@inquirer/editor@npm:^4.2.20": - version: 4.2.20 - resolution: "@inquirer/editor@npm:4.2.20" +"@inquirer/editor@npm:^4.2.21": + version: 4.2.21 + resolution: "@inquirer/editor@npm:4.2.21" dependencies: - "@inquirer/core": ^10.2.2 + "@inquirer/core": ^10.3.0 "@inquirer/external-editor": ^1.0.2 - "@inquirer/type": ^3.0.8 + "@inquirer/type": ^3.0.9 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: d87ddb4d2668a8860ea48dee4f26323d568a50be8dfa645d29fcd97d9457a20bc5a80b7f79c725af253e363dee740b28f77a1f9748eb6833fc002b00a6f5d67a + checksum: 8467c192f9c1573853718c15c020146268cf4b076d99a14e014a61d124c46157c57780d770caeeba94f309259504f3602248591842d11b9465ad12fd82185276 languageName: node linkType: hard -"@inquirer/expand@npm:^4.0.20": - version: 4.0.20 - resolution: "@inquirer/expand@npm:4.0.20" +"@inquirer/expand@npm:^4.0.21": + version: 4.0.21 + resolution: "@inquirer/expand@npm:4.0.21" dependencies: - "@inquirer/core": ^10.2.2 - "@inquirer/type": ^3.0.8 + "@inquirer/core": ^10.3.0 + "@inquirer/type": ^3.0.9 yoctocolors-cjs: ^2.1.2 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 975d2159dc38ae4a4fd9e7fe1f731bcb01f20a80f49d79a43232cbf9310d868cbc20c19c25fb9d3970d1415be772fd1a793065b4d939e60045b13abecb45d057 + checksum: eb1900c443895377c03652c3e2b6ca29c572fe6ee2682e264572957b9b4a596d3d55c9ea271934846fb05d5cc5195cca0dffde1386e41358ac5c308698320e93 languageName: node linkType: hard @@ -2013,142 +2015,142 @@ __metadata: languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.13": - version: 1.0.13 - resolution: "@inquirer/figures@npm:1.0.13" - checksum: 1042cbefad8c69b004396ce6be2d0b135c303317d870ddd0cee75bac429fc7c7f577bac9e3c1ec1cd3668a709f49a591edb2f714193778e7d7b140a622f2a1ef +"@inquirer/figures@npm:^1.0.14": + version: 1.0.14 + resolution: "@inquirer/figures@npm:1.0.14" + checksum: 37eec986f119eabb6c231c8c1481c6a48ab2347e9f57b2d6442161f7b83936678221fccb7ead60582026c2ae20d457467d0727c485ff53aee2cf965077b0f51b languageName: node linkType: hard -"@inquirer/input@npm:^4.2.4": - version: 4.2.4 - resolution: "@inquirer/input@npm:4.2.4" +"@inquirer/input@npm:^4.2.5": + version: 4.2.5 + resolution: "@inquirer/input@npm:4.2.5" dependencies: - "@inquirer/core": ^10.2.2 - "@inquirer/type": ^3.0.8 + "@inquirer/core": ^10.3.0 + "@inquirer/type": ^3.0.9 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 725b9e3152e15bdcc5c7043cd8a15cc5573cb78ea7a273aca802f0a673ddd3f543e4cb2a94445e88f7ca66facef6faaa6040f959bf70c28f53c0fad25a8cecda + checksum: 9d192556aefc8f8fbc70626f9a90cd2806032ec9e7d323b46afff0a0813f5c0f766ff9a5d2f8bdc39863688f8fdd081ce23b782c19aebf61ff8692c9135528b6 languageName: node linkType: hard -"@inquirer/number@npm:^3.0.20": - version: 3.0.20 - resolution: "@inquirer/number@npm:3.0.20" +"@inquirer/number@npm:^3.0.21": + version: 3.0.21 + resolution: "@inquirer/number@npm:3.0.21" dependencies: - "@inquirer/core": ^10.2.2 - "@inquirer/type": ^3.0.8 + "@inquirer/core": ^10.3.0 + "@inquirer/type": ^3.0.9 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 9f5d3c7484cd8d0e798ac6ab4e00b71dcc46450b5d9e39f40def15dd76363aa0fac17158583cad1b64af90968b72bfa4cc6ac3338f19651023f2cb686db9c394 + checksum: 445ba93639ecfc3755efa7ee9cf7cf972919abc1cd022ada27e7d73b93e01680ffcf56a8ca6fe090775358c8d2aec259890aa33b6c0e1c3aeba7306f25ba633a languageName: node linkType: hard -"@inquirer/password@npm:^4.0.20": - version: 4.0.20 - resolution: "@inquirer/password@npm:4.0.20" +"@inquirer/password@npm:^4.0.21": + version: 4.0.21 + resolution: "@inquirer/password@npm:4.0.21" dependencies: - "@inquirer/ansi": ^1.0.0 - "@inquirer/core": ^10.2.2 - "@inquirer/type": ^3.0.8 + "@inquirer/ansi": ^1.0.1 + "@inquirer/core": ^10.3.0 + "@inquirer/type": ^3.0.9 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 9b3c46498cf09e3d3da1e2539b23468ab4020904dc40efb8a8f50b1c5bf7af7e7355c18ee313dac00d750b4b83d8b0a2c72b10cfcef475f742a5a738fe4300f2 + checksum: 07fb1527ea2d44a81b79d9263f59713e66977e21fbf44efedb6bf08d27d617900ef481c49c91b0a749caf1d282f2b5e19fe6b7474acc98db3edd174eb5d45416 languageName: node linkType: hard "@inquirer/prompts@npm:^7.8.6": - version: 7.8.6 - resolution: "@inquirer/prompts@npm:7.8.6" - dependencies: - "@inquirer/checkbox": ^4.2.4 - "@inquirer/confirm": ^5.1.18 - "@inquirer/editor": ^4.2.20 - "@inquirer/expand": ^4.0.20 - "@inquirer/input": ^4.2.4 - "@inquirer/number": ^3.0.20 - "@inquirer/password": ^4.0.20 - "@inquirer/rawlist": ^4.1.8 - "@inquirer/search": ^3.1.3 - "@inquirer/select": ^4.3.4 + version: 7.9.0 + resolution: "@inquirer/prompts@npm:7.9.0" + dependencies: + "@inquirer/checkbox": ^4.3.0 + "@inquirer/confirm": ^5.1.19 + "@inquirer/editor": ^4.2.21 + "@inquirer/expand": ^4.0.21 + "@inquirer/input": ^4.2.5 + "@inquirer/number": ^3.0.21 + "@inquirer/password": ^4.0.21 + "@inquirer/rawlist": ^4.1.9 + "@inquirer/search": ^3.2.0 + "@inquirer/select": ^4.4.0 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: a6e554516743de1f5781503c4a402df6d24af7d526e89ac5cd17a30d5761bfcc16f283f49382aa57c67e7b88c0e63e91e9aa5367a1dbe4de6373c873fae60202 + checksum: 1dd6a87bcf77d1a8b728c781a7d34c0dd4028d7ec96e4e41e173a260d3ef9a76cba5eb8715d8674d75b18681d3f7eac9bd9f3ff1d82d8e786fb5222893498ea3 languageName: node linkType: hard -"@inquirer/rawlist@npm:^4.1.8": - version: 4.1.8 - resolution: "@inquirer/rawlist@npm:4.1.8" +"@inquirer/rawlist@npm:^4.1.9": + version: 4.1.9 + resolution: "@inquirer/rawlist@npm:4.1.9" dependencies: - "@inquirer/core": ^10.2.2 - "@inquirer/type": ^3.0.8 + "@inquirer/core": ^10.3.0 + "@inquirer/type": ^3.0.9 yoctocolors-cjs: ^2.1.2 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: ece338071d8070d7be123b2cac0bffabe3a1b1c197bf18d518e4eeed910aaa9359c287b3bdfc6943f0552839c312e36ef87aebae62ed31b10d59d7442f8b3064 + checksum: ec23e087bfa9497b36d51b53e8da18c837e4a0c5c091bce7d1a6b52d9664035d7e22c3753993dd3c7c9ebfd5e9b71f1738873f2c25422668733ddb28d74bf26b languageName: node linkType: hard -"@inquirer/search@npm:^3.1.3": - version: 3.1.3 - resolution: "@inquirer/search@npm:3.1.3" +"@inquirer/search@npm:^3.2.0": + version: 3.2.0 + resolution: "@inquirer/search@npm:3.2.0" dependencies: - "@inquirer/core": ^10.2.2 - "@inquirer/figures": ^1.0.13 - "@inquirer/type": ^3.0.8 + "@inquirer/core": ^10.3.0 + "@inquirer/figures": ^1.0.14 + "@inquirer/type": ^3.0.9 yoctocolors-cjs: ^2.1.2 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 7b908673b28b2651ecba33974b99b684d253a911d1e6123f386e648911060add083981889de81d2ed89c0cfc504047624b1ba15921da92a95a28cfd73decec6b + checksum: 5f8f86368513d29d9119a04cc65bbe075d0d93ec55af27a555f02bf740c7a65497e36df80da722cbee020ab348c9038856f0e070e9a22615ff9d5c3155c3296a languageName: node linkType: hard -"@inquirer/select@npm:^4.3.4": - version: 4.3.4 - resolution: "@inquirer/select@npm:4.3.4" +"@inquirer/select@npm:^4.4.0": + version: 4.4.0 + resolution: "@inquirer/select@npm:4.4.0" dependencies: - "@inquirer/ansi": ^1.0.0 - "@inquirer/core": ^10.2.2 - "@inquirer/figures": ^1.0.13 - "@inquirer/type": ^3.0.8 + "@inquirer/ansi": ^1.0.1 + "@inquirer/core": ^10.3.0 + "@inquirer/figures": ^1.0.14 + "@inquirer/type": ^3.0.9 yoctocolors-cjs: ^2.1.2 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 59da7ce52c324879e7c7b239ae4ddbdc9519a6593ded099eb1efa9cb45854d62e6565cab29d4df38fdc64bf7fc1ba7d0192b686a4039f9501739a189dcee303c + checksum: cb0441f5ec981011f3e451fa062897e8991ca3b0cadca99432d2556a1f90e9c0251b277233056db63b717b5f77caaf32b2c5db7a1881fcc78d5c357d1e912616 languageName: node linkType: hard -"@inquirer/type@npm:^3.0.8": - version: 3.0.8 - resolution: "@inquirer/type@npm:3.0.8" +"@inquirer/type@npm:^3.0.8, @inquirer/type@npm:^3.0.9": + version: 3.0.9 + resolution: "@inquirer/type@npm:3.0.9" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 361fa75c98f274462aaa3f2baf40ee43f284daaa64e3689a92863ed4ff63236ca3d40c6e715b3ff80c45feb6ab679792a6162e2d4521daff3929c490b0dddfcf + checksum: 960ba4737405f70bac17e7cdc4696c60064b06c8dd13a4b3d0783763ba1714bdadbd598b88d537ab9415b7d5d61e011ac042cfbd1438b2a35298e2868724b853 languageName: node linkType: hard @@ -3560,11 +3562,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 24.6.0 - resolution: "@types/node@npm:24.6.0" + version: 24.8.0 + resolution: "@types/node@npm:24.8.0" dependencies: - undici-types: ~7.13.0 - checksum: 5550f2c93824f9dde6b4277f2774a833f5d276e3ebd5cf2f071d76553dff6fb896965fccc0ebed79374720034f24936841a7c544188f4d3d9ae52dc00b7aa63b + undici-types: ~7.14.0 + checksum: 34a0bff50f96c704310fdcab6e2f04e67745a4ead63faadaeea578b306c59b192d19160b42fa69f59976206e6696a45363c395298d8b3a1eb0caa50750ecd5e2 languageName: node linkType: hard @@ -3583,11 +3585,11 @@ __metadata: linkType: hard "@types/react@npm:^19.1.0": - version: 19.1.16 - resolution: "@types/react@npm:19.1.16" + version: 19.2.2 + resolution: "@types/react@npm:19.2.2" dependencies: csstype: ^3.0.2 - checksum: 2c58c6be656b111774da23b98d9f797ab5befb1e0a7900c698f04701de66977c36d5a9c989b10b2da1d575ca9a45a6f3bf75292a0ad24a0c91457549cd65f9e6 + checksum: 7eb2d316dd5a6c02acb416524b50bae932c38d055d26e0f561ca23c009c686d16a2b22fcbb941eecbe2ecb167f119e29b9d0142d9d056dd381352c43413b60da languageName: node linkType: hard @@ -4027,12 +4029,13 @@ __metadata: linkType: hard "arktype@npm:^2.1.15": - version: 2.1.22 - resolution: "arktype@npm:2.1.22" + version: 2.1.23 + resolution: "arktype@npm:2.1.23" dependencies: - "@ark/schema": 0.49.0 - "@ark/util": 0.49.0 - checksum: 46947539b550912f709908bcb973114607a8d61124f7f4ea1090bcaab85ca5c49d68afd6928bf05ce80fe403b6906e7d31d58ed346b408bb8519b9ffdf08e0cb + "@ark/regex": 0.0.0 + "@ark/schema": 0.50.0 + "@ark/util": 0.50.0 + checksum: 5874ef1c0140aff0a99cd88537e11851b4d0a1a49ee7b097eb766c941f9de6bbd04427bbda8023c69171db0ee02c7d99f08e59114b63fa83a93c4130964fb616 languageName: node linkType: hard @@ -4360,12 +4363,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.3": - version: 2.8.9 - resolution: "baseline-browser-mapping@npm:2.8.9" +"baseline-browser-mapping@npm:^2.8.9": + version: 2.8.16 + resolution: "baseline-browser-mapping@npm:2.8.16" bin: baseline-browser-mapping: dist/cli.js - checksum: ad426d6e239ffaad388e126c52263c0bf4248b9482b56db219f7c866a3f5a6cef39a5c08b7ace1bdf3f87f26ac96b0e9f9bfc199fc148d838ea68e43e3104d92 + checksum: 929b3f2b5f4beb3b9e02b660a4a8b24f62e43cc7efdc38a726ef061137f9529d3372e1c70298eacde191ac70f5defd21ae4a29ff9e470c4c2b7fd7be68320241 languageName: node linkType: hard @@ -4442,18 +4445,18 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.20.4, browserslist@npm:^4.24.0, browserslist@npm:^4.25.3": - version: 4.26.2 - resolution: "browserslist@npm:4.26.2" +"browserslist@npm:^4.20.4, browserslist@npm:^4.24.0, browserslist@npm:^4.26.3": + version: 4.26.3 + resolution: "browserslist@npm:4.26.3" dependencies: - baseline-browser-mapping: ^2.8.3 - caniuse-lite: ^1.0.30001741 - electron-to-chromium: ^1.5.218 + baseline-browser-mapping: ^2.8.9 + caniuse-lite: ^1.0.30001746 + electron-to-chromium: ^1.5.227 node-releases: ^2.0.21 update-browserslist-db: ^1.1.3 bin: browserslist: cli.js - checksum: ebd96e8895cdfc72be074281eb377332b69ceb944ec0c063739d8eeb8e513b168ac1e27d26ce5cc260e69a340a44c6bb5e9408565449d7a16739e5844453d4c7 + checksum: aa5bbcda9db1eeb9952b4c2f11f9a5a2247da7bcce7fa14d3cc215e67246a93394eda2f86378a41c3f73e6e1a1561bf0e7eade93c5392cb6d37bc66f70d0c53f languageName: node linkType: hard @@ -4597,10 +4600,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001741": - version: 1.0.30001746 - resolution: "caniuse-lite@npm:1.0.30001746" - checksum: 3d2a310b49bc414d87184a73ca7c3b1aea0a1547cc9e3bfd8a6ee45129d269917a94b8e773e7b2821cfc3b9e1256759a34e4b7242c56e3f70f575213baa6c880 +"caniuse-lite@npm:^1.0.30001746": + version: 1.0.30001751 + resolution: "caniuse-lite@npm:1.0.30001751" + checksum: d11e25c44e40c21e7b7492a25c9fd60f4c04e94aa265573f7c487666f5e1b5ca3ed09d09560336f959237063616255cb294d415511bb6cf0486eb2cb6a3a4318 languageName: node linkType: hard @@ -4694,9 +4697,9 @@ __metadata: linkType: hard "ci-info@npm:^4.3.0": - version: 4.3.0 - resolution: "ci-info@npm:4.3.0" - checksum: 77a851ec826e1fbcd993e0e3ef402e6a5e499c733c475af056b7808dea9c9ede53e560ed433020489a8efea2d824fd68ca203446c9988a0bac8475210b0d4491 + version: 4.3.1 + resolution: "ci-info@npm:4.3.1" + checksum: 66c159d92648e8a07acab0a3a0681bff6ccc39aa44916263208c4d97bbbeedbbc886d7611fd30c21df1aa624ce3c6fcdfde982e74689e3e014e064e1d0805f94 languageName: node linkType: hard @@ -4799,9 +4802,9 @@ __metadata: linkType: hard "collect-v8-coverage@npm:^1.0.0": - version: 1.0.2 - resolution: "collect-v8-coverage@npm:1.0.2" - checksum: c10f41c39ab84629d16f9f6137bc8a63d332244383fc368caf2d2052b5e04c20cd1fd70f66fcf4e2422b84c8226598b776d39d5f2d2a51867cc1ed5d1982b4da + version: 1.0.3 + resolution: "collect-v8-coverage@npm:1.0.3" + checksum: ed1d1ebc9c05e7263fffa3ad6440031db6a1fdd9f574435aa689effcdfe9f2b93aba8ec600f9c7b99124cd6ff5d9415c17961d84ae829a72251a4fe668a49b63 languageName: node linkType: hard @@ -5170,24 +5173,24 @@ __metadata: linkType: hard "core-js-compat@npm:^3.43.0": - version: 3.45.1 - resolution: "core-js-compat@npm:3.45.1" + version: 3.46.0 + resolution: "core-js-compat@npm:3.46.0" dependencies: - browserslist: ^4.25.3 - checksum: 817286f6b7deb90278fd1f46131664fda36b74983e2fc4859a36ae85ef9361868b307964eea0e364251763e415eab7589e9abe2a4ec4d1672c2870f03c52b1ac + browserslist: ^4.26.3 + checksum: 16d381c51e34d38ecc65d429d5a5c1dbd198f70b5a0a6256a3a41dcb8523e07f0a8682f6349298a55ff6e9d039e131d67b07fe863047a28672ae5f10373c57cf languageName: node linkType: hard "cosmiconfig-typescript-loader@npm:^6.1.0": - version: 6.1.0 - resolution: "cosmiconfig-typescript-loader@npm:6.1.0" + version: 6.2.0 + resolution: "cosmiconfig-typescript-loader@npm:6.2.0" dependencies: - jiti: ^2.4.1 + jiti: ^2.6.1 peerDependencies: "@types/node": "*" cosmiconfig: ">=9" typescript: ">=5" - checksum: 45114854faaa97178abd2ccad511363faa57c03321c7e39ad16619c63842b3f6147dd20118f9f07c9530a242a39c3107c791708bb0b987dad374e71f23f9468b + checksum: 2680bb585de1185aa23ba678cb0426cba1be8fa0a9d286f71c2ce5bd63f23e5b8f726161673a16babb2aa0e7d033fda8774268a025fb63f548d1c75977292212 languageName: node linkType: hard @@ -5569,10 +5572,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.218": - version: 1.5.227 - resolution: "electron-to-chromium@npm:1.5.227" - checksum: 6a798b53216b300f20dbad56c0ef3777da59cb37a761c169fbf8b1dcbeca74ccf137f83a5b3b959ae82cf3cd454aae1d4fda8c7dabd99a72bef645ac18fa6a45 +"electron-to-chromium@npm:^1.5.227": + version: 1.5.237 + resolution: "electron-to-chromium@npm:1.5.237" + checksum: 5905e2808dc6243ced0a83537afbafedec20c063feb6403a678b612a7855d79bc6ecb7d094bdab71f54173cf2ae5d1d8070b0c31572025001c94de62af84f5f8 languageName: node linkType: hard @@ -5637,11 +5640,11 @@ __metadata: linkType: hard "envinfo@npm:^7.13.0": - version: 7.15.0 - resolution: "envinfo@npm:7.15.0" + version: 7.18.0 + resolution: "envinfo@npm:7.18.0" bin: envinfo: dist/cli.js - checksum: 38595c11134ecb66a40289980d8ca82e89fdcd68849dd72560c1bbc3cfc55c867573b4150967707ff9ff2e5cad6f1d0cb6cc56c333a6eccdcd3533452141c0a8 + checksum: 1a4d0094d9aede737e3a1ea59756d6dff440ba2a7e87509cae3a8f69ef8748cba952e9a6c0f66e54bc12a90902d32a082b0934dfc156ca4252e18fe5a0c4d11d languageName: node linkType: hard @@ -6063,17 +6066,17 @@ __metadata: linkType: hard "eslint@npm:^9.35.0": - version: 9.36.0 - resolution: "eslint@npm:9.36.0" + version: 9.37.0 + resolution: "eslint@npm:9.37.0" dependencies: "@eslint-community/eslint-utils": ^4.8.0 "@eslint-community/regexpp": ^4.12.1 "@eslint/config-array": ^0.21.0 - "@eslint/config-helpers": ^0.3.1 - "@eslint/core": ^0.15.2 + "@eslint/config-helpers": ^0.4.0 + "@eslint/core": ^0.16.0 "@eslint/eslintrc": ^3.3.1 - "@eslint/js": 9.36.0 - "@eslint/plugin-kit": ^0.3.5 + "@eslint/js": 9.37.0 + "@eslint/plugin-kit": ^0.4.0 "@humanfs/node": ^0.16.6 "@humanwhocodes/module-importer": ^1.0.1 "@humanwhocodes/retry": ^0.4.2 @@ -6108,7 +6111,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 08a02a1d474cf7ea63ef9328e638751c939a1c08b99f7812f0f44a96e3b8346ab3bbca3af57da8b3e74cbc6619e41645fd3dcb3adda94d1cb826f02664e2d44c + checksum: 78e813174acef58d361d557a4d083d2d03f20cd70dd96f59973414305acaedf72bad52271c789174a19ee0407f8bece017ce42a05c89014b93e457d033285aeb languageName: node linkType: hard @@ -6265,9 +6268,9 @@ __metadata: linkType: hard "exponential-backoff@npm:^3.1.1": - version: 3.1.2 - resolution: "exponential-backoff@npm:3.1.2" - checksum: 7e191e3dd6edd8c56c88f2c8037c98fbb8034fe48778be53ed8cb30ccef371a061a4e999a469aab939b92f8f12698f3b426d52f4f76b7a20da5f9f98c3cbc862 + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 471fdb70fd3d2c08a74a026973bdd4105b7832911f610ca67bbb74e39279411c1eed2f2a110c9d41c2edd89459ba58fdaba1c174beed73e7a42d773882dcff82 languageName: node linkType: hard @@ -6581,9 +6584,9 @@ __metadata: linkType: hard "generator-function@npm:^2.0.0": - version: 2.0.0 - resolution: "generator-function@npm:2.0.0" - checksum: 12b5ca9c9cb21196aa4d8a53de3104956a917e806e00b0370d442fcd1142e3ae2f9401211204b951352bf22daf0373dd055d080cf6b9392d8c1281fe497c5de9 + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 3bf87f7b0230de5d74529677e6c3ceb3b7b5d9618b5a22d92b45ce3876defbaf5a77791b25a61b0fa7d13f95675b5ff67a7769f3b9af33f096e34653519e873d languageName: node linkType: hard @@ -7432,15 +7435,15 @@ __metadata: linkType: hard "is-generator-function@npm:^1.0.10": - version: 1.1.1 - resolution: "is-generator-function@npm:1.1.1" + version: 1.1.2 + resolution: "is-generator-function@npm:1.1.2" dependencies: - call-bound: ^1.0.3 + call-bound: ^1.0.4 generator-function: ^2.0.0 - get-proto: ^1.0.0 + get-proto: ^1.0.1 has-tostringtag: ^1.0.2 safe-regex-test: ^1.1.0 - checksum: 194a7a1654ec8ae1e49df4f6774956e686fba7ba7dbe545de5ba9cd06bb93e2c4c0bfe6924f5211b800d3fa16cb151cd272d1da01d0392d07317a6b4900b923a + checksum: 0b81c613752a5e534939e5b3835ff722446837a5b94c3a3934af5ded36a651d9aa31c3f11f8a3453884b9658bf26dbfb7eb855e744d920b07f084bd890a43414 languageName: node linkType: hard @@ -8309,12 +8312,12 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^2.4.1, jiti@npm:^2.5.1": - version: 2.6.0 - resolution: "jiti@npm:2.6.0" +"jiti@npm:^2.5.1, jiti@npm:^2.6.1": + version: 2.6.1 + resolution: "jiti@npm:2.6.1" bin: jiti: lib/jiti-cli.mjs - checksum: 2bd869527bfbb23b5210344881b4f2f5fd86b7c9c703001036544762411af73fe0f95097ba025a738874085143939664173360aafea7d7cbc4ca3bbc325774a9 + checksum: 9394e29c5e40d1ca8267923160d8d86706173c9ff30c901097883434b0c4866de2c060427b6a9a5843bb3e42fa3a3c8b5b2228531d3dd4f4f10c5c6af355bb86 languageName: node linkType: hard @@ -8838,69 +8841,69 @@ __metadata: languageName: node linkType: hard -"metro-babel-transformer@npm:0.83.2": - version: 0.83.2 - resolution: "metro-babel-transformer@npm:0.83.2" +"metro-babel-transformer@npm:0.83.3": + version: 0.83.3 + resolution: "metro-babel-transformer@npm:0.83.3" dependencies: "@babel/core": ^7.25.2 flow-enums-runtime: ^0.0.6 hermes-parser: 0.32.0 nullthrows: ^1.1.1 - checksum: 8ca98216c3fc32757cbb445d2e42042617b5a2399d3d409759b168fbd3d52aadf8bb2b8471e4b204ddf5c654b7b146397edb7693f48a0582e7e4e169cf3bbfbb + checksum: dd178409d1718dae12dfffb6572ebc5bb78f1e0d7e93dce829c945957f8a686cb1b4c466c69585d7b982b3937fbea28d5c53a80691f2fc66717a0bcc800bc5b8 languageName: node linkType: hard -"metro-cache-key@npm:0.83.2": - version: 0.83.2 - resolution: "metro-cache-key@npm:0.83.2" +"metro-cache-key@npm:0.83.3": + version: 0.83.3 + resolution: "metro-cache-key@npm:0.83.3" dependencies: flow-enums-runtime: ^0.0.6 - checksum: ad60492b1db35b7d4eb1f9ed6f8aa79a051dcb1be3183fcd5b0a810e7c4ba5dba5e9f02e131ccd271d6db2efaa9893ef0e316ef26ebb3ab49cb074fada4de1b5 + checksum: a6f9d2bf8b810f57d330d6f8f1ebf029e1224f426c5895f73d9bc1007482684048bfc7513a855626ee7f3ae72ca46e1b08cf983aefbfa84321bb7c0cef4ba4ae languageName: node linkType: hard -"metro-cache@npm:0.83.2": - version: 0.83.2 - resolution: "metro-cache@npm:0.83.2" +"metro-cache@npm:0.83.3": + version: 0.83.3 + resolution: "metro-cache@npm:0.83.3" dependencies: exponential-backoff: ^3.1.1 flow-enums-runtime: ^0.0.6 https-proxy-agent: ^7.0.5 - metro-core: 0.83.2 - checksum: 29e914de2c3da88f94a5cb2708cb87ea1a1d7dba73a0f0f45d974e36e635132190a00330803cc8226e784700322576e68b96c52a03d10725d3a7afbf3a5845df + metro-core: 0.83.3 + checksum: 95606275411d85de071fd95171a9548406cd1154320850a554bf00207804f7844ed252f9750a802d6612ade839c579b23bd87927ae173f43c368e8f5d900149d languageName: node linkType: hard -"metro-config@npm:0.83.2, metro-config@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-config@npm:0.83.2" +"metro-config@npm:0.83.3, metro-config@npm:^0.83.1": + version: 0.83.3 + resolution: "metro-config@npm:0.83.3" dependencies: connect: ^3.6.5 flow-enums-runtime: ^0.0.6 jest-validate: ^29.7.0 - metro: 0.83.2 - metro-cache: 0.83.2 - metro-core: 0.83.2 - metro-runtime: 0.83.2 + metro: 0.83.3 + metro-cache: 0.83.3 + metro-core: 0.83.3 + metro-runtime: 0.83.3 yaml: ^2.6.1 - checksum: d8b8ddd0ce77cf6c1173288af1b38676918d6465b8542061a6be6ff61022d0363ae0479a58fc343baac812b38b4876e22d0a50a97d1207ea44cffa7bbc893aa0 + checksum: a14b77668a9712abbcebe5bf6a0081f0fd46caf8d37405174f261765abcd44d7a99910533fcc05edde3de10f9b22820cc9910c7dee2b01e761692a0a322f2608 languageName: node linkType: hard -"metro-core@npm:0.83.2, metro-core@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-core@npm:0.83.2" +"metro-core@npm:0.83.3, metro-core@npm:^0.83.1": + version: 0.83.3 + resolution: "metro-core@npm:0.83.3" dependencies: flow-enums-runtime: ^0.0.6 lodash.throttle: ^4.1.1 - metro-resolver: 0.83.2 - checksum: 58ce33dcfe0b5803aadd1681b37bf51b481582437738afed701b124da77bf476e082124da8c2b60161f15290043ecc8086c51fdc44f241fcc3bb9d7887fffd0e + metro-resolver: 0.83.3 + checksum: d06871313310cd718094ecbae805bcacea3f325340f6dff3c5044b62457c4690dd729cdb938349bdd3c41efa6f28032ae07696467ef006d5509fec9045c1966f languageName: node linkType: hard -"metro-file-map@npm:0.83.2": - version: 0.83.2 - resolution: "metro-file-map@npm:0.83.2" +"metro-file-map@npm:0.83.3": + version: 0.83.3 + resolution: "metro-file-map@npm:0.83.3" dependencies: debug: ^4.4.0 fb-watchman: ^2.0.0 @@ -8911,76 +8914,76 @@ __metadata: micromatch: ^4.0.4 nullthrows: ^1.1.1 walker: ^1.0.7 - checksum: 16ea37fa9c252686aafd1bc5fc5d4791273ff1be606303582035d52865b2ff16f1f13fc0a867c5b2385479563f748e0ee96b6fb83d16e739e413e60c0e22a079 + checksum: 0dea599206e93b6e8628be2aa98452d4dae16e805b810759ec8b50cebcd83f2d053f7e5865196d464f3793f86b3b5003830c6713f91bf62fa406a4af7c93a776 languageName: node linkType: hard -"metro-minify-terser@npm:0.83.2": - version: 0.83.2 - resolution: "metro-minify-terser@npm:0.83.2" +"metro-minify-terser@npm:0.83.3": + version: 0.83.3 + resolution: "metro-minify-terser@npm:0.83.3" dependencies: flow-enums-runtime: ^0.0.6 terser: ^5.15.0 - checksum: ee164bdd3ddf797e1b0f9fd71960b662b40fc3abead77521b1e1435291d38cc151442348362d6afee0596d52fcff48cc6a055a04a7928905e9557968e05293ac + checksum: 1de88b70b7c903147807baa46497491a87600594fd0868b6538bbb9d7785242cabfbe8bccf36cc2285d0e17be72445b512d00c496952a159572545f3e6bcb199 languageName: node linkType: hard -"metro-resolver@npm:0.83.2": - version: 0.83.2 - resolution: "metro-resolver@npm:0.83.2" +"metro-resolver@npm:0.83.3": + version: 0.83.3 + resolution: "metro-resolver@npm:0.83.3" dependencies: flow-enums-runtime: ^0.0.6 - checksum: f3b97ac389c7cbf624db1558a07e48d3e8be5f581c010a3a1d26f8a5ef95ab9ba14bb959d4102da4e637eb66643f178499348e60d06f6cce7fa3068ecb5fd3d6 + checksum: de2ae5ced6239b004a97712f98934c6e830870d11614e2dba48250930214581f0746df8a4f0f1cb71060fe21c2cf919d3359106ad4f375c2500ba08e10922896 languageName: node linkType: hard -"metro-runtime@npm:0.83.2, metro-runtime@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-runtime@npm:0.83.2" +"metro-runtime@npm:0.83.3, metro-runtime@npm:^0.83.1": + version: 0.83.3 + resolution: "metro-runtime@npm:0.83.3" dependencies: "@babel/runtime": ^7.25.0 flow-enums-runtime: ^0.0.6 - checksum: 1868bffbb7dc8a9c69a2d480d7d8e1019548f68522f9368f5513aa9325c39ed9dfaae052cfe0209cb03bc70a908e08d72eb852e1cff56bc6f32a73c8dc92a5ff + checksum: dcbdc5502020d1e20cee1a3a8019323ab2f3ca2aa2d6ddb2b7a2b8547835a20b84fe4afc23c397f788584e108c70411db93df2f61322b44a4f0f119275052d03 languageName: node linkType: hard -"metro-source-map@npm:0.83.2, metro-source-map@npm:^0.83.1": - version: 0.83.2 - resolution: "metro-source-map@npm:0.83.2" +"metro-source-map@npm:0.83.3, metro-source-map@npm:^0.83.1": + version: 0.83.3 + resolution: "metro-source-map@npm:0.83.3" dependencies: "@babel/traverse": ^7.25.3 "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3" "@babel/types": ^7.25.2 flow-enums-runtime: ^0.0.6 invariant: ^2.2.4 - metro-symbolicate: 0.83.2 + metro-symbolicate: 0.83.3 nullthrows: ^1.1.1 - ob1: 0.83.2 + ob1: 0.83.3 source-map: ^0.5.6 vlq: ^1.0.0 - checksum: 50dc6eebc0a6d36c8a93acc57cc0311cbf0485a0b1fdb81c265c8950afefcf16b7cfb56e2dbb211a04bd0fa59b5a0369cd2e7499ea489ce6f98719aa88b2d097 + checksum: 5bf3b7a1561bc1f0ad6ab3b7b550d4b4581da31964a7f218727a3201576912076c909a2e50fba4dd3c649d79312324dec683a37228f4559811c37b69ecca8831 languageName: node linkType: hard -"metro-symbolicate@npm:0.83.2": - version: 0.83.2 - resolution: "metro-symbolicate@npm:0.83.2" +"metro-symbolicate@npm:0.83.3": + version: 0.83.3 + resolution: "metro-symbolicate@npm:0.83.3" dependencies: flow-enums-runtime: ^0.0.6 invariant: ^2.2.4 - metro-source-map: 0.83.2 + metro-source-map: 0.83.3 nullthrows: ^1.1.1 source-map: ^0.5.6 vlq: ^1.0.0 bin: metro-symbolicate: src/index.js - checksum: fdf5a0d35dfad39d9cda8beda85f09f26e4ae662cbd05623492574299dde3660561502f54396cce3b25818a9079219d1fdbd217c5000619b8d14d6357739a59c + checksum: 943cc2456d56ae2ed8369495c18966d91feff636b37909b5225ffb8ce2a50eba8fbedf116f3bea3059d431ebc621c9c9af8a8bfd181b0cd1fece051507e10ffd languageName: node linkType: hard -"metro-transform-plugins@npm:0.83.2": - version: 0.83.2 - resolution: "metro-transform-plugins@npm:0.83.2" +"metro-transform-plugins@npm:0.83.3": + version: 0.83.3 + resolution: "metro-transform-plugins@npm:0.83.3" dependencies: "@babel/core": ^7.25.2 "@babel/generator": ^7.25.0 @@ -8988,34 +8991,34 @@ __metadata: "@babel/traverse": ^7.25.3 flow-enums-runtime: ^0.0.6 nullthrows: ^1.1.1 - checksum: 455cf6811172351ed61ae498f2fed20a1830b23a47d591066bcd1bf52f9b0cc7d0daf8c97ffedc0e0b1e5a7d2da65d16fac869a3c09d0e84ac4ffa5df0777ccb + checksum: 6f92b9dfa53bdb63e79038bbd4d68791379ab26cf874679e64563618c578eeed3a828795debf8076ffd518431dff53191990784fb619046bcc03fff114b0cb21 languageName: node linkType: hard -"metro-transform-worker@npm:0.83.2": - version: 0.83.2 - resolution: "metro-transform-worker@npm:0.83.2" +"metro-transform-worker@npm:0.83.3": + version: 0.83.3 + resolution: "metro-transform-worker@npm:0.83.3" dependencies: "@babel/core": ^7.25.2 "@babel/generator": ^7.25.0 "@babel/parser": ^7.25.3 "@babel/types": ^7.25.2 flow-enums-runtime: ^0.0.6 - metro: 0.83.2 - metro-babel-transformer: 0.83.2 - metro-cache: 0.83.2 - metro-cache-key: 0.83.2 - metro-minify-terser: 0.83.2 - metro-source-map: 0.83.2 - metro-transform-plugins: 0.83.2 + metro: 0.83.3 + metro-babel-transformer: 0.83.3 + metro-cache: 0.83.3 + metro-cache-key: 0.83.3 + metro-minify-terser: 0.83.3 + metro-source-map: 0.83.3 + metro-transform-plugins: 0.83.3 nullthrows: ^1.1.1 - checksum: 955e4f8f190151e62c75167168d85c4cde2cfb5121e72f9f7459ba371f3ce41d131ec3bb6c2d0097c036f66a38183ecdd383375648c29736c2345c45f6f4d4e9 + checksum: fcb25ebc1ce703d830ef60c9af87325f996af4c3946325ab957b65ca59d12d181fe6c527c9ba1f932cd954d23a400052293117fe56f9a2727dfbc0a118e7bb27 languageName: node linkType: hard -"metro@npm:0.83.2, metro@npm:^0.83.1": - version: 0.83.2 - resolution: "metro@npm:0.83.2" +"metro@npm:0.83.3, metro@npm:^0.83.1": + version: 0.83.3 + resolution: "metro@npm:0.83.3" dependencies: "@babel/code-frame": ^7.24.7 "@babel/core": ^7.25.2 @@ -9038,18 +9041,18 @@ __metadata: jest-worker: ^29.7.0 jsc-safe-url: ^0.2.2 lodash.throttle: ^4.1.1 - metro-babel-transformer: 0.83.2 - metro-cache: 0.83.2 - metro-cache-key: 0.83.2 - metro-config: 0.83.2 - metro-core: 0.83.2 - metro-file-map: 0.83.2 - metro-resolver: 0.83.2 - metro-runtime: 0.83.2 - metro-source-map: 0.83.2 - metro-symbolicate: 0.83.2 - metro-transform-plugins: 0.83.2 - metro-transform-worker: 0.83.2 + metro-babel-transformer: 0.83.3 + metro-cache: 0.83.3 + metro-cache-key: 0.83.3 + metro-config: 0.83.3 + metro-core: 0.83.3 + metro-file-map: 0.83.3 + metro-resolver: 0.83.3 + metro-runtime: 0.83.3 + metro-source-map: 0.83.3 + metro-symbolicate: 0.83.3 + metro-transform-plugins: 0.83.3 + metro-transform-worker: 0.83.3 mime-types: ^2.1.27 nullthrows: ^1.1.1 serialize-error: ^2.1.0 @@ -9059,7 +9062,7 @@ __metadata: yargs: ^17.6.2 bin: metro: src/cli.js - checksum: 0f2ddde7644f58f1f7580e665e4ea627a8329e73a5c595892cae7d91f5568e0c70e6f8d3cec85db35db5171991a42e265e7615091ef7b78b4a49f321be6da785 + checksum: 306d8c06b5a1a45e18df6e41f494bbc8b439700985429284eea7b3c3c82108e3c3795d859a8ab3ed7a85793d64e3160519be9aa84c6418d6ed37bd5ae4500b57 languageName: node linkType: hard @@ -9350,8 +9353,8 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 11.4.2 - resolution: "node-gyp@npm:11.4.2" + version: 11.5.0 + resolution: "node-gyp@npm:11.5.0" dependencies: env-paths: ^2.2.0 exponential-backoff: ^3.1.1 @@ -9365,7 +9368,7 @@ __metadata: which: ^5.0.0 bin: node-gyp: bin/node-gyp.js - checksum: d8041cee7ec60c86fb2961d77c12a2d083a481fb28b08e6d9583153186c0e7766044dc30bdb1f3ac01ddc5763b83caeed3d1ea35787ec4ffd8cc4aeedfc34f2b + checksum: 6cc29b9d454d9a684c8fe299668db618875bb4282e37717ca5b79689cc5ce99cd553c70944bb367979f2eba40ad6a50afaf7b12a6b214172edc7377384efa051 languageName: node linkType: hard @@ -9377,9 +9380,9 @@ __metadata: linkType: hard "node-releases@npm:^2.0.21": - version: 2.0.21 - resolution: "node-releases@npm:2.0.21" - checksum: 191f8245e18272971650eb45151c5891313bca27507a8f634085bd8c98a9cb9492686ef6182176866ceebff049646ef6cd5fb5ca46d5b5ca00ce2c69185d84c4 + version: 2.0.25 + resolution: "node-releases@npm:2.0.25" + checksum: 9a23149cf3f6778e62440b1f26f91927aff06c3606a29996f3d196c7c0f5e31c17c24c324b5ef1f571cebef6b5a8db9adce9c09381ca271bc6422aac91463f75 languageName: node linkType: hard @@ -9459,12 +9462,12 @@ __metadata: languageName: node linkType: hard -"ob1@npm:0.83.2": - version: 0.83.2 - resolution: "ob1@npm:0.83.2" +"ob1@npm:0.83.3": + version: 0.83.3 + resolution: "ob1@npm:0.83.3" dependencies: flow-enums-runtime: ^0.0.6 - checksum: 8eb482589b66cf46600d1231c2ea50a365f47ee5db0274795d1d3f5c43112e255b931a41ce1ef8a220f31b4fb985fb269c6a54bf7e9719f90dac3f4001a89a6c + checksum: 20dfe91d48d0cadd97159cfd53f5abdca435b55d58b1f562e0687485e8f44f8a95e8ab3c835badd13d0d8c01e3d7b14d639a316aa4bf82841ac78b49611d4e5c languageName: node linkType: hard @@ -10764,7 +10767,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.2, semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": +"semver@npm:7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -10782,6 +10785,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + languageName: node + linkType: hard + "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" @@ -11707,22 +11719,22 @@ __metadata: linkType: hard "typescript@npm:^5.9.2": - version: 5.9.2 - resolution: "typescript@npm:5.9.2" + version: 5.9.3 + resolution: "typescript@npm:5.9.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: f619cf6773cfe31409279711afd68cdf0859780006c50bc2a7a0c3227f85dea89a3b97248846326f3a17dad72ea90ec27cf61a8387772c680b2252fd02d8497b + checksum: 0d0ffb84f2cd072c3e164c79a2e5a1a1f4f168e84cb2882ff8967b92afe1def6c2a91f6838fb58b168428f9458c57a2ba06a6737711fdd87a256bbe83e9a217f languageName: node linkType: hard "typescript@patch:typescript@^5.9.2#~builtin": - version: 5.9.2 - resolution: "typescript@patch:typescript@npm%3A5.9.2#~builtin::version=5.9.2&hash=14eedb" + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#~builtin::version=5.9.3&hash=14eedb" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: e42a701947325500008334622321a6ad073f842f5e7d5e7b588a6346b31fdf51d56082b9ce5cef24312ecd3e48d6c0d4d44da7555f65e2feec18cf62ec540385 + checksum: 8bb8d86819ac86a498eada254cad7fb69c5f74778506c700c2a712daeaff21d3a6f51fd0d534fe16903cb010d1b74f89437a3d02d4d0ff5ca2ba9a4660de8497 languageName: node linkType: hard @@ -11754,10 +11766,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.13.0": - version: 7.13.0 - resolution: "undici-types@npm:7.13.0" - checksum: fcb3e1195a36615fce3935eb97c21ebe4dbafe968f831ed00e6f22e8e73c0655b8e3242acc6ba4ff0f3c34e3f3f860f19fbb59c00b261bd4e20b515abbc2de7c +"undici-types@npm:~7.14.0": + version: 7.14.0 + resolution: "undici-types@npm:7.14.0" + checksum: bd28cb36b33a51359f02c27b84bfe8563cdad57bdab0aa6ac605ce64d51aff49fd0aa4cb2d3b043caaa93c3ec42e96b5757df5d2d9bcc06a5f3e71899c765035 languageName: node linkType: hard