From ee00dc06795b3044a2d81a5f48133f2ecbe3513a Mon Sep 17 00:00:00 2001 From: Saurabh Chavan Date: Fri, 30 Oct 2026 19:09:42 +0530 Subject: [PATCH] fix: revert the barcode scanner change --- configs/e2e/native_dependencies.json | 5 +- .../nanoflow-actions-native/CHANGELOG.md | 1 - .../nanoflow-actions-native/package.json | 2 +- .../barcode-scanner-native/CHANGELOG.md | 13 - .../barcode-scanner-native/package.json | 7 +- .../src/BarcodeScanner.tsx | 226 +++++++++--------- .../src/BarcodeScanner.xml | 6 - .../src/__tests__/BarcodeScanner.ios.spec.tsx | 138 ----------- .../src/__tests__/BarcodeScanner.spec.tsx | 40 +--- .../BarcodeScanner.spec.tsx.snap | 30 +-- .../barcode-scanner-native/src/package.xml | 2 +- .../src/scanners/types.ts | 27 --- .../src/scanners/useAndroidScannerOutput.ts | 108 --------- .../src/scanners/useIosScannerOutput.ts | 67 ------ .../src/scanners/utils.ts | 24 -- .../typings/BarcodeScannerProps.d.ts | 2 - pnpm-lock.yaml | 85 ++----- 17 files changed, 157 insertions(+), 626 deletions(-) delete mode 100644 packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.ios.spec.tsx delete mode 100644 packages/pluggableWidgets/barcode-scanner-native/src/scanners/types.ts delete mode 100644 packages/pluggableWidgets/barcode-scanner-native/src/scanners/useAndroidScannerOutput.ts delete mode 100644 packages/pluggableWidgets/barcode-scanner-native/src/scanners/useIosScannerOutput.ts delete mode 100644 packages/pluggableWidgets/barcode-scanner-native/src/scanners/utils.ts diff --git a/configs/e2e/native_dependencies.json b/configs/e2e/native_dependencies.json index 9c61f1253..2b68899f3 100644 --- a/configs/e2e/native_dependencies.json +++ b/configs/e2e/native_dependencies.json @@ -9,10 +9,7 @@ "react-native-svg": "15.15.4", "react-native-video": "6.19.2", "@react-native-async-storage/async-storage": "2.2.0", - "react-native-vision-camera": "5.2.3", - "react-native-vision-camera-barcode-scanner": "5.2.3", - "react-native-nitro-modules": "0.37.0", - "react-native-nitro-image": "0.15.2", + "react-native-vision-camera": "4.7.3", "react-native-view-shot": "5.1.0", "react-native-blob-util": "0.24.7", "react-native-file-viewer-turbo": "0.7.4", diff --git a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md index aab74bafb..0e9782346 100644 --- a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md +++ b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md @@ -10,7 +10,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fixed an issue where the "Get straight line distance" action threw an error at runtime. - Fixed an issue where actions using decimal values threw a "Big is not defined" error at runtime. -- Updated react-native-nitro-modules to 0.37.0. ### Added diff --git a/packages/jsActions/nanoflow-actions-native/package.json b/packages/jsActions/nanoflow-actions-native/package.json index 59259a37d..c2f448b1c 100644 --- a/packages/jsActions/nanoflow-actions-native/package.json +++ b/packages/jsActions/nanoflow-actions-native/package.json @@ -29,7 +29,7 @@ "@react-native-async-storage/async-storage": "2.2.0", "js-base64": "~3.7.2", "react-native-nitro-geolocation": "1.4.3", - "react-native-nitro-modules": "0.37.0", + "react-native-nitro-modules": "0.36.1", "mendix-native": "https://github.com/mendix/mendix-native/releases/download/v0.5.2/mendix-native-v0.5.2.tgz", "react-native-permissions": "5.5.1", "react-native-geocoder": "0.5.0" diff --git a/packages/pluggableWidgets/barcode-scanner-native/CHANGELOG.md b/packages/pluggableWidgets/barcode-scanner-native/CHANGELOG.md index 4ce598f0e..dad63beb2 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/CHANGELOG.md +++ b/packages/pluggableWidgets/barcode-scanner-native/CHANGELOG.md @@ -6,19 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] -### BREAKING - -- We updated react-native-vision-camera from v4.7.3 to v5.2.3. On Android, barcodes are now scanned with react-native-vision-camera-barcode-scanner v5.2.3 (Google ML Kit). On iOS, barcodes are still scanned with Apple's built-in scanner, as before. This version requires Make It Native version 11.12.3 or above. Older versions of Make It Native cannot run this version of the widget. -- For Native Template builds, Native Template version 19.1.8 or above is required. - -### Added - -- We added a `scan delay` property that delays scanning by the specified number of seconds, giving the end user time to position the scanner over the code. - -### Fixed - -- We fixed barcodes with patterned backgrounds not being scanned correctly on Android. - ## [4.5.0] - 2026-9-2 - We fixed the barcode mask not visible issue and selection logic of QR code. diff --git a/packages/pluggableWidgets/barcode-scanner-native/package.json b/packages/pluggableWidgets/barcode-scanner-native/package.json index f8c252327..20b5cbf1a 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/package.json +++ b/packages/pluggableWidgets/barcode-scanner-native/package.json @@ -1,7 +1,7 @@ { "name": "barcode-scanner-native", "widgetName": "BarcodeScanner", - "version": "4.6.0", + "version": "4.5.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -22,10 +22,7 @@ "@mendix/piw-native-utils-internal": "*", "@mendix/piw-utils-internal": "*", "react-native-svg": "15.15.4", - "react-native-vision-camera": "5.2.3", - "react-native-vision-camera-barcode-scanner": "5.2.3", - "react-native-nitro-modules": "0.37.0", - "react-native-nitro-image": "0.15.2" + "react-native-vision-camera": "4.7.3" }, "devDependencies": { "@mendix/pluggable-widgets-tools": "*" diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx index a83424eb3..a48767b4b 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx +++ b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx @@ -1,11 +1,9 @@ import { flattenStyles } from "@mendix/piw-native-utils-internal"; import { ValueStatus } from "mendix"; -import { ReactElement, useCallback, useMemo, useRef, useState, useEffect } from "react"; +import { ReactElement, useCallback, useMemo, useRef, useState } from "react"; import { View, LayoutChangeEvent, Platform } from "react-native"; -import { Camera, useCameraDevice, useCameraPermission, type CameraRef } from "react-native-vision-camera"; +import { Camera, useCodeScanner, Code, useCameraDevice, CodeScannerFrame } from "react-native-vision-camera"; import BarcodeMask from "./components/BarcodeMask"; -import { DetectedCode, Size, UseScannerOutput } from "./scanners/types"; -import { useIosScannerOutput } from "./scanners/useIosScannerOutput"; import { BarcodeScannerProps } from "../typings/BarcodeScannerProps"; import { BarcodeScannerStyle, defaultBarcodeScannerStyle } from "./ui/styles"; @@ -13,19 +11,6 @@ import { executeAction } from "@mendix/piw-utils-internal"; export type Props = BarcodeScannerProps; -// ML Kit on Android can misread 1D barcodes (e.g. Code 39) on busy backgrounds for a single frame, -// so a value is only accepted after it is detected on this many consecutive reads. -const REQUIRED_CONSECUTIVE_READS_ANDROID = 3; -const SCAN_LOCK_DURATION_MS = 2000; - -// iOS uses Apple's scanner from vision-camera core, Android uses Google ML Kit. The Android scanner package isn't -// linked on iOS and fails when loaded, so it is only required on Android. Keep this require directly behind the -// Platform.OS check: the widget build replaces Platform.OS per platform and drops the require from the iOS bundle. -const useScannerOutput: UseScannerOutput = - Platform.OS === "android" - ? require("./scanners/useAndroidScannerOutput").useAndroidScannerOutput - : useIosScannerOutput; - type CodePositionInfo = { isWithinMask: boolean; distanceToMaskCenterSquared: number; @@ -33,19 +18,79 @@ type CodePositionInfo = { overlapPercentage: number; }; +type TransformedCoordinates = { + codeX: number; + codeY: number; + codeWidth: number; + codeHeight: number; +}; + type OverlapInfo = { overlapArea: number; overlapPercentage: number; }; type CandidateCode = { - code: DetectedCode; + code: Code; isWithinMask: boolean; distanceToMaskCenterSquared: number; overlapArea: number; overlapPercentage: number; }; +/** + * Transforms barcode coordinates from camera sensor space to screen view space. + * Handles platform-specific coordinate systems (iOS sensor landscape vs Android ML Kit rotated) + * and applies appropriate scaling based on device orientation. + */ +function transformCodeCoordinates( + codeFrame: { x: number; y: number; width: number; height: number }, + scanFrame: CodeScannerFrame, + viewWidth: number, + viewHeight: number +): TransformedCoordinates { + const { width: frameWidth, height: frameHeight } = scanFrame; + const isPortrait = viewHeight > viewWidth; + + let codeX: number; + let codeY: number; + let codeWidth: number; + let codeHeight: number; + + if (isPortrait && Platform.OS === "ios") { + // iOS: code.frame coordinates are in the sensor's native landscape space, + // so we need a 90° rotation to map to portrait view coordinates. + const scaleX = viewWidth / frameHeight; + const scaleY = viewHeight / frameWidth; + + codeX = (frameHeight - codeFrame.y - codeFrame.height) * scaleX; + codeY = codeFrame.x * scaleY; + codeWidth = codeFrame.height * scaleX; + codeHeight = codeFrame.width * scaleY; + } else if (isPortrait && Platform.OS === "android") { + // Android: ML Kit already rotates code.frame coordinates to match device orientation, + // but CodeScannerFrame still reports sensor landscape dimensions (e.g. 1920x1080). + // Scale using the shorter dimension for X and longer for Y. + const scaleX = viewWidth / Math.min(frameWidth, frameHeight); + const scaleY = viewHeight / Math.max(frameWidth, frameHeight); + + codeX = codeFrame.x * scaleX; + codeY = codeFrame.y * scaleY; + codeWidth = codeFrame.width * scaleX; + codeHeight = codeFrame.height * scaleY; + } else { + const scaleX = viewWidth / frameWidth; + const scaleY = viewHeight / frameHeight; + + codeX = codeFrame.x * scaleX; + codeY = codeFrame.y * scaleY; + codeWidth = codeFrame.width * scaleX; + codeHeight = codeFrame.height * scaleY; + } + + return { codeX, codeY, codeWidth, codeHeight }; +} + /** * Calculates the overlap between a barcode and the mask region. * Returns both the absolute overlap area and the percentage of the barcode that overlaps with the mask. @@ -93,59 +138,19 @@ function compareCodesByPriority(a: CandidateCode, b: CandidateCode): number { export function BarcodeScanner(props: Props): ReactElement { const device = useCameraDevice("back"); - const [cameraViewDimensions, setCameraViewDimensions] = useState(null); - const cameraRef = useRef(null); - - const { hasPermission, canRequestPermission, requestPermission } = useCameraPermission(); - - // VisionCamera v5 no longer prompts implicitly when the session starts; it throws without permission - useEffect(() => { - if (!hasPermission && canRequestPermission) { - requestPermission(); - } - }, [canRequestPermission, requestPermission]); const styles = useMemo(() => flattenStyles(defaultBarcodeScannerStyle, props.style), [props.style]); // Ref to track the lock state const isLockedRef = useRef(false); - // Timer that releases the lock, kept so it can be cleared when the widget unmounts - const lockTimeoutRef = useRef | null>(null); - - // Time from which detected codes may be accepted, null until the camera is started - const scanStartTimeRef = useRef(null); - // Last detected value and how many consecutive reads returned it - const lastReadValueRef = useRef(null); - const consecutiveReadsRef = useRef(0); - - // Resets the consecutive read counter (Android only) - const resetConsecutiveReads = useCallback(() => { - lastReadValueRef.current = null; - consecutiveReadsRef.current = 0; - }, []); - - // Starts a new scan: codes are accepted again after the configured scan delay, - // and the consecutive read count starts from zero - const resetScanState = useCallback(() => { - scanStartTimeRef.current = Date.now() + props.scanDelay * 1000; - resetConsecutiveReads(); - }, [props.scanDelay, resetConsecutiveReads]); - - // Clear a pending lock timer when the widget unmounts - useEffect(() => { - return () => { - if (lockTimeoutRef.current) { - clearTimeout(lockTimeoutRef.current); - } - }; - }, []); + const [cameraViewDimensions, setCameraViewDimensions] = useState<{ width: number; height: number } | null>(null); const maskWidth = styles.mask.width || 280; const maskHeight = styles.mask.height || 260; const getCodePositionInfo = useCallback( - (code: DetectedCode): CodePositionInfo => { + (code: Code, scanFrame: CodeScannerFrame): CodePositionInfo => { if (!props.showMask) { return { isWithinMask: true, @@ -155,8 +160,7 @@ export function BarcodeScanner(props: Props): ReactElement { }; } - const bounds = cameraViewDimensions ? code.getViewBounds() : undefined; - if (!cameraViewDimensions || !bounds) { + if (!cameraViewDimensions || !code.frame) { return { isWithinMask: false, distanceToMaskCenterSquared: Number.MAX_SAFE_INTEGER, @@ -173,7 +177,13 @@ export function BarcodeScanner(props: Props): ReactElement { const maskCenterX = maskX + maskWidth / 2; const maskCenterY = maskY + maskHeight / 2; - const { x: codeX, y: codeY, width: codeWidth, height: codeHeight } = bounds; + // Transform Qr barcode coordinates from camera sensor space to view space + const { codeX, codeY, codeWidth, codeHeight } = transformCodeCoordinates( + code.frame, + scanFrame, + viewWidth, + viewHeight + ); const codeCenterX = codeX + codeWidth / 2; const codeCenterY = codeY + codeHeight / 2; @@ -208,56 +218,38 @@ export function BarcodeScanner(props: Props): ReactElement { [props.showMask, cameraViewDimensions, maskWidth, maskHeight] ); - const onCodesScanned = useCallback( - (codes: DetectedCode[]) => { - // Block if still in cooldown or the previous detection is still being handled - if (isLockedRef.current || props.onDetect?.isExecuting) { - return; - } - - // Block until the camera is started and the scan delay has passed - if (scanStartTimeRef.current === null || Date.now() < scanStartTimeRef.current) { + const onCodeScanned = useCallback( + (codes: Code[], frame: CodeScannerFrame) => { + // Block if still in cooldown + if (isLockedRef.current) { return; } if (props.barcode.status !== ValueStatus.Available || codes.length === 0) { - resetConsecutiveReads(); return; } const candidates = codes - .map(code => ({ - code, - ...getCodePositionInfo(code) - })) + .map(code => { + const positionInfo = getCodePositionInfo(code, frame); + return { + code, + ...positionInfo + }; + }) .filter(item => item.isWithinMask); if (candidates.length === 0) { - resetConsecutiveReads(); return; } - const value = candidates.sort(compareCodesByPriority)[0].code.value; + const selectedCode = candidates.sort(compareCodesByPriority)[0].code; - if (!value) { - resetConsecutiveReads(); + if (!selectedCode.value) { return; } - // Android only: accept the value after it was read on several consecutive reads, - // a different value restarts the count - if (Platform.OS === "android") { - if (value === lastReadValueRef.current) { - consecutiveReadsRef.current += 1; - } else { - lastReadValueRef.current = value; - consecutiveReadsRef.current = 1; - } - - if (consecutiveReadsRef.current < REQUIRED_CONSECUTIVE_READS_ANDROID) { - return; - } - } + const { value } = selectedCode; if (value !== props.barcode.value) { props.barcode.setValue(value); @@ -265,24 +257,33 @@ export function BarcodeScanner(props: Props): ReactElement { executeAction(props.onDetect); - // Reset consecutive reads counter after accepting a detection - resetConsecutiveReads(); - // Lock further scans for 2 seconds isLockedRef.current = true; - lockTimeoutRef.current = setTimeout(() => { + setTimeout(() => { isLockedRef.current = false; - }, SCAN_LOCK_DURATION_MS); + }, 2000); }, - [props.barcode, props.onDetect, getCodePositionInfo, resetConsecutiveReads] + [props.barcode, props.onDetect, getCodePositionInfo] ); - const scannerOutput = useScannerOutput({ onCodesScanned, cameraRef, cameraViewDimensions }); - - // The camera is ready, start the scan delay from now - const handleCameraStarted = useCallback(() => { - resetScanState(); - }, [resetScanState]); + const codeScanner = useCodeScanner({ + codeTypes: [ + "qr", + "aztec", + "codabar", + "code-39", + "code-93", + "code-128", + "data-matrix", + "ean-13", + "ean-8", + "upc-a", + "upc-e", + "pdf-417", + "itf" + ], + onCodeScanned + }); const handleCameraLayout = useCallback((event: LayoutChangeEvent) => { const { width, height } = event.nativeEvent.layout; @@ -290,17 +291,16 @@ export function BarcodeScanner(props: Props): ReactElement { }, []); return ( - - {device && hasPermission && ( + + {device && ( <> {props.showMask && ( diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml index 4850f262d..334efe727 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml +++ b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml @@ -25,12 +25,6 @@ - - - Scan delay - Number of seconds to wait after the camera starts before barcodes are scanned, so the user has time to position the barcode. Set to 0 to start scanning immediately. - - On detect diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.ios.spec.tsx b/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.ios.spec.tsx deleted file mode 100644 index e0ed64422..000000000 --- a/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.ios.spec.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { actionValue, EditableValueBuilder } from "@mendix/piw-utils-internal"; -import { fireEvent, render } from "@testing-library/react-native"; -import { Platform } from "react-native"; -import type { Props } from "../BarcodeScanner"; - -// The scanner implementation is chosen when the widget module loads, so switch to iOS before requiring it -Object.defineProperty(Platform, "OS", { configurable: true, value: "ios" }); -const { BarcodeScanner } = require("../BarcodeScanner") as typeof import("../BarcodeScanner"); - -type MockScannedObject = { value?: string; viewBounds?: { x: number; y: number; width: number; height: number } }; - -let mockOnObjectsScanned: ((objects: MockScannedObject[]) => void) | undefined; - -jest.mock("react-native-vision-camera", () => { - const { useImperativeHandle } = jest.requireActual("react"); - const { View } = jest.requireActual("react-native"); - return { - Camera: ({ children, onStarted, ref, ...props }: any) => { - useImperativeHandle(ref, () => ({ - convertScannedObjectCoordinatesToViewCoordinates: (object: MockScannedObject) => { - if (!object.viewBounds) { - throw new Error("Failed to transform the Scanned Object's coordinates"); - } - return { boundingBox: object.viewBounds }; - } - })); - // Call onStarted to simulate camera being ready - if (onStarted) { - setTimeout(() => onStarted(), 0); - } - return ( - - {children} - - ); - }, - useCameraDevice: () => "mock-device", - useCameraPermission: () => ({ - hasPermission: true, - canRequestPermission: false, - requestPermission: jest.fn() - }), - useObjectOutput: (options: any) => { - mockOnObjectsScanned = options.onObjectsScanned; - return "mockObjectOutput"; - }, - useVideoOutput: () => "mockResolutionOutput", - CommonResolutions: { FHD_16_9: { width: 1080, height: 1920 } }, - isScannedCode: (object: object) => "value" in object - }; -}); - -// The ML Kit scanner isn't linked on iOS, loading it there crashes the app -jest.mock("react-native-vision-camera-barcode-scanner", () => { - throw new Error("react-native-vision-camera-barcode-scanner must not be loaded on iOS"); -}); - -jest.mock("../components/BarcodeMask", () => "BarcodeMask"); - -describe("BarcodeScanner (iOS)", () => { - let defaultProps: Props; - - beforeEach(() => { - jest.useFakeTimers(); - defaultProps = { - showAnimatedLine: false, - showMask: false, - name: "barcode-scanner-test", - style: [], - barcode: new EditableValueBuilder().build(), - scanDelay: 0 - }; - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - it("renders with Apple's object output and a 1080p 30 fps camera format", () => { - const component = render(); - const camera = component.getByTestId("camera"); - expect(camera.props.outputs).toEqual(["mockObjectOutput", "mockResolutionOutput"]); - expect(camera.props.constraints).toEqual([{ resolutionBias: "mockResolutionOutput" }, { fps: 30 }]); - }); - - it("sets a value and executes the onDetect action on the first read", () => { - const onDetectAction = actionValue(); - render(); - jest.runOnlyPendingTimers(); - - mockOnObjectsScanned?.([{ value: "value" }]); - - expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("value"); - expect(onDetectAction.execute).toHaveBeenCalledTimes(1); - - // Further scans are locked for 2 seconds - mockOnObjectsScanned?.([{ value: "value1" }]); - expect(onDetectAction.execute).toHaveBeenCalledTimes(1); - - jest.advanceTimersByTime(2000); - mockOnObjectsScanned?.([{ value: "value1" }]); - - expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("value1"); - expect(onDetectAction.execute).toHaveBeenCalledTimes(2); - }); - - it("ignores scanned objects that are not codes or have no value", () => { - const onDetectAction = actionValue(); - render(); - jest.runOnlyPendingTimers(); - - mockOnObjectsScanned?.([{} as MockScannedObject, { value: undefined }, { value: "" }]); - - expect(defaultProps.barcode.setValue).not.toHaveBeenCalled(); - expect(onDetectAction.execute).not.toHaveBeenCalled(); - }); - - it("only accepts codes inside the mask, using view coordinates from the camera", () => { - const onDetectAction = actionValue(); - const component = render(); - jest.runOnlyPendingTimers(); - // Default mask is 280x260, centered: x 60..340, y 270..530 - fireEvent(component.getByTestId("camera"), "layout", { nativeEvent: { layout: { width: 400, height: 800 } } }); - - mockOnObjectsScanned?.([ - { value: "outside", viewBounds: { x: 0, y: 0, width: 50, height: 50 } }, - { value: "no-preview-yet" } - ]); - expect(onDetectAction.execute).not.toHaveBeenCalled(); - - mockOnObjectsScanned?.([ - { value: "outside", viewBounds: { x: 0, y: 0, width: 50, height: 50 } }, - { value: "inside", viewBounds: { x: 150, y: 350, width: 100, height: 100 } } - ]); - expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("inside"); - expect(onDetectAction.execute).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.spec.tsx b/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.spec.tsx index a4c1d2655..256765ee9 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.spec.tsx +++ b/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/BarcodeScanner.spec.tsx @@ -5,28 +5,14 @@ import { View } from "react-native"; import { BarcodeScanner, Props } from "../BarcodeScanner"; import { defaultBarcodeScannerStyle } from "../ui/styles"; -let mockOnBarcodeScanned: ((barcodes: Array<{ rawValue: string }>) => void) | undefined; +let mockOnCodeScanned: ((codes: Array<{ value: string }>) => void) | undefined; jest.mock("react-native-vision-camera", () => ({ - Camera: ({ children, onStarted, ...props }: any) => { - // Call onStarted to simulate camera being ready - if (onStarted) { - setTimeout(() => onStarted(), 0); - } - return {children}; - }, + Camera: ({ children, ...props }: any) => {children}, useCameraDevice: () => "mock-device", - useCameraPermission: () => ({ - hasPermission: true, - canRequestPermission: false, - requestPermission: jest.fn() - }) -})); - -jest.mock("react-native-vision-camera-barcode-scanner", () => ({ - useBarcodeScannerOutput: (options: any) => { - mockOnBarcodeScanned = options.onBarcodeScanned; - return "mockScannerOutput"; + useCodeScanner: (options: any) => { + mockOnCodeScanned = options.onCodeScanned; + return "mockCodeScanner"; } })); @@ -42,8 +28,7 @@ describe("BarcodeScanner", () => { showMask: false, name: "barcode-scanner-test", style: [], - barcode: new EditableValueBuilder().build(), - scanDelay: 0 + barcode: new EditableValueBuilder().build() }; }); @@ -70,22 +55,15 @@ describe("BarcodeScanner", () => { const onDetectAction = actionValue(); render(); - // Wait for camera start and advance time past scan delay - jest.runOnlyPendingTimers(); - - // Simulate scanning (3 consecutive reads required on Android) - mockOnBarcodeScanned?.([{ rawValue: "value" }]); - mockOnBarcodeScanned?.([{ rawValue: "value" }]); - mockOnBarcodeScanned?.([{ rawValue: "value" }]); + // Simulate scanning + mockOnCodeScanned?.([{ value: "value" }]); jest.advanceTimersByTime(2000); expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("value"); expect(onDetectAction.execute).toHaveBeenCalledTimes(1); // Another scan - mockOnBarcodeScanned?.([{ rawValue: "value1" }]); - mockOnBarcodeScanned?.([{ rawValue: "value1" }]); - mockOnBarcodeScanned?.([{ rawValue: "value1" }]); + mockOnCodeScanned?.([{ value: "value1" }]); jest.advanceTimersByTime(2000); expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("value1"); diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/__snapshots__/BarcodeScanner.spec.tsx.snap b/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/__snapshots__/BarcodeScanner.spec.tsx.snap index 441ae54c1..6fcf797a8 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/__snapshots__/BarcodeScanner.spec.tsx.snap +++ b/packages/pluggableWidgets/barcode-scanner-native/src/__tests__/__snapshots__/BarcodeScanner.spec.tsx.snap @@ -9,18 +9,13 @@ exports[`BarcodeScanner renders 1`] = ` "minHeight": 100, } } - testID="barcode-scanner-test" > `; @@ -41,18 +37,13 @@ exports[`BarcodeScanner renders with mask 1`] = ` "minHeight": 100, } } - testID="barcode-scanner-test" > - + diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/types.ts b/packages/pluggableWidgets/barcode-scanner-native/src/scanners/types.ts deleted file mode 100644 index d60aa4bcf..000000000 --- a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { RefObject } from "react"; -import type { CameraOutput, CameraRef, Constraint } from "react-native-vision-camera"; - -export type Size = { width: number; height: number }; - -// Rectangle in camera view space (dp) -export type ViewBounds = { x: number; y: number; width: number; height: number }; - -export type DetectedCode = { - value: string; - // Resolved lazily, only when the mask needs the code position. Undefined when it can't be determined yet. - getViewBounds: () => ViewBounds | undefined; -}; - -export type ScannerOutputOptions = { - onCodesScanned: (codes: DetectedCode[]) => void; - cameraRef: RefObject; - cameraViewDimensions: Size | null; -}; - -export type ScannerOutput = { - // Outputs to attach to the Camera, the first one is the scanner - outputs: CameraOutput[]; - constraints: Constraint[]; -}; - -export type UseScannerOutput = (options: ScannerOutputOptions) => ScannerOutput; diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/useAndroidScannerOutput.ts b/packages/pluggableWidgets/barcode-scanner-native/src/scanners/useAndroidScannerOutput.ts deleted file mode 100644 index bfd82843b..000000000 --- a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/useAndroidScannerOutput.ts +++ /dev/null @@ -1,108 +0,0 @@ -// Android only: react-native-vision-camera-barcode-scanner (Google ML Kit) creates its native object when it is -// loaded and isn't linked on iOS, so this file must only be required behind a Platform.OS === "android" check. -import { useCallback, useEffect, useMemo, useRef } from "react"; -import type { CameraOutput } from "react-native-vision-camera"; -import { - useBarcodeScannerOutput, - type Barcode, - type TargetBarcodeFormat -} from "react-native-vision-camera-barcode-scanner"; - -import { ScannerOutput, ScannerOutputOptions, Size, ViewBounds } from "./types"; -import { buildDetectedCodes } from "./utils"; - -// Kept as a module constant because the scanner output is recreated whenever this array changes. -const BARCODE_FORMATS: TargetBarcodeFormat[] = [ - "qr-code", - "aztec", - "codabar", - "code-39", - "code-93", - "code-128", - "data-matrix", - "ean-13", - "ean-8", - "upc-a", - "upc-e", - "pdf-417", - "itf" -]; - -/** - * Transforms barcode coordinates from scanner buffer space to screen view space. - * ML Kit already rotates coordinates to match device orientation. - * Uses uniform cover scaling (like CSS object-fit: cover) to match VisionCamera's preview rendering. - */ -function toViewBounds(boundingBox: Barcode["boundingBox"], bufferResolution: Size, viewSize: Size): ViewBounds { - const frameWidth = bufferResolution.width; - const frameHeight = bufferResolution.height; - const { width: viewWidth, height: viewHeight } = viewSize; - const isPortrait = viewHeight > viewWidth; - - const codeFrameWidth = boundingBox.right - boundingBox.left; - const codeFrameHeight = boundingBox.bottom - boundingBox.top; - - // Buffer resolution reports sensor landscape dimensions (e.g. 1920x1080) even in portrait. - // In portrait, we need to use the swapped dimensions for aspect ratio calculation. - const effectiveFrameWidth = isPortrait ? Math.min(frameWidth, frameHeight) : frameWidth; - const effectiveFrameHeight = isPortrait ? Math.max(frameWidth, frameHeight) : frameHeight; - - // Use uniform cover scaling: scale factor is the maximum of width/height ratios - // This ensures the frame fills the view completely, with one dimension cropped - const scale = Math.max(viewWidth / effectiveFrameWidth, viewHeight / effectiveFrameHeight); - - // Calculate how much the scaled frame extends beyond the view (crop region) - const scaledFrameWidth = effectiveFrameWidth * scale; - const scaledFrameHeight = effectiveFrameHeight * scale; - const cropOffsetX = (scaledFrameWidth - viewWidth) / 2; - const cropOffsetY = (scaledFrameHeight - viewHeight) / 2; - - const result = { - x: boundingBox.left * scale - cropOffsetX, - y: boundingBox.top * scale - cropOffsetY, - width: codeFrameWidth * scale, - height: codeFrameHeight * scale - }; - - return result; -} - -export function useAndroidScannerOutput({ onCodesScanned, cameraViewDimensions }: ScannerOutputOptions): ScannerOutput { - // Read when resolving a code position to get the buffer resolution the scanner is actually using - const scannerOutputRef = useRef(null); - - const onBarcodeScanned = useCallback( - (barcodes: Barcode[]) => { - const codes = buildDetectedCodes( - barcodes, - barcode => barcode.rawValue, - barcode => () => { - const bufferResolution = scannerOutputRef.current?.currentResolution; - if (!bufferResolution || !cameraViewDimensions) { - return undefined; - } - return toViewBounds(barcode.boundingBox, bufferResolution, cameraViewDimensions); - } - ); - onCodesScanned(codes); - }, - [onCodesScanned, cameraViewDimensions] - ); - - const onScanError = useCallback((error: Error) => { - console.warn("Barcode scanner error:", error); - }, []); - - const scannerOutput = useBarcodeScannerOutput({ - barcodeFormats: BARCODE_FORMATS, - // Full resolution so dense/patterned QR codes can be decoded - outputResolution: "full", - onBarcodeScanned, - onError: onScanError - }); - useEffect(() => { - scannerOutputRef.current = scannerOutput; - }, [scannerOutput]); - - return useMemo(() => ({ outputs: [scannerOutput], constraints: [] }), [scannerOutput]); -} diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/useIosScannerOutput.ts b/packages/pluggableWidgets/barcode-scanner-native/src/scanners/useIosScannerOutput.ts deleted file mode 100644 index 0918b04ba..000000000 --- a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/useIosScannerOutput.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { - CommonResolutions, - isScannedCode, - useObjectOutput, - useVideoOutput, - type ScannedObject, - type ScannedObjectType -} from "react-native-vision-camera"; - -import { ScannerOutput, ScannerOutputOptions } from "./types"; -import { buildDetectedCodes } from "./utils"; - -// Same code types as v4 on iOS (Apple AVFoundation): UPC-A is reported as EAN-13, and v4's "itf" was interleaved 2 of 5. -// Kept as a module constant because the object output is recreated whenever this array changes. -const OBJECT_TYPES: ScannedObjectType[] = [ - "qr", - "aztec", - "codabar", - "code-39", - "code-93", - "code-128", - "data-matrix", - "ean-13", - "ean-8", - "upc-e", - "pdf-417", - "interleaved-2-of-5" -]; - -export function useIosScannerOutput({ onCodesScanned, cameraRef }: ScannerOutputOptions): ScannerOutput { - const onObjectsScanned = useCallback( - (objects: ScannedObject[]) => { - const scannedCodes = objects.filter(isScannedCode); - const codes = buildDetectedCodes( - scannedCodes, - object => object.value, - object => () => { - try { - // Converts from normalized camera space to view space, handling orientation and cropping natively - return cameraRef.current?.convertScannedObjectCoordinatesToViewCoordinates(object).boundingBox; - } catch { - // The preview isn't ready yet - return undefined; - } - } - ); - onCodesScanned(codes); - }, - [onCodesScanned, cameraRef] - ); - - const objectOutput = useObjectOutput({ types: OBJECT_TYPES, onObjectsScanned }); - - // Without a resolution target VisionCamera picks the first matching camera format, while v4 kept Apple's - // default 1080p 30 fps setup that dense/patterned QR codes need. Only a created output can be used as - // resolution bias, so a video output is added that never records. - const resolutionOutput = useVideoOutput({ targetResolution: CommonResolutions.FHD_16_9, enableAudio: false }); - - return useMemo( - () => ({ - outputs: [objectOutput, resolutionOutput], - constraints: [{ resolutionBias: resolutionOutput }, { fps: 30 }] - }), - [objectOutput, resolutionOutput] - ); -} diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/utils.ts b/packages/pluggableWidgets/barcode-scanner-native/src/scanners/utils.ts deleted file mode 100644 index eff451e36..000000000 --- a/packages/pluggableWidgets/barcode-scanner-native/src/scanners/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DetectedCode, ViewBounds } from "./types"; - -/** - * Generic helper to build DetectedCode array from platform-specific scanner results. - * Filters out items without values and applies the provided view bounds getter. - */ -export function buildDetectedCodes( - items: T[], - getValue: (item: T) => string | null | undefined, - getViewBounds: (item: T) => () => ViewBounds | undefined -): DetectedCode[] { - const codes: DetectedCode[] = []; - for (const item of items) { - const value = getValue(item); - if (!value) { - continue; - } - codes.push({ - value, - getViewBounds: getViewBounds(item) - }); - } - return codes; -} diff --git a/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts b/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts index a961fea4d..d303b6d4b 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts +++ b/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts @@ -12,7 +12,6 @@ export interface BarcodeScannerProps