diff --git a/packages/pluggableWidgets/barcode-scanner-native/package.json b/packages/pluggableWidgets/barcode-scanner-native/package.json index 4d6c1eeb0..58b7981f8 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/package.json +++ b/packages/pluggableWidgets/barcode-scanner-native/package.json @@ -21,6 +21,8 @@ "dependencies": { "@mendix/piw-native-utils-internal": "*", "@mendix/piw-utils-internal": "*", + "@react-navigation/native": "^6.1.18", + "react-native-vector-icons": "10.2.0", "react-native-barcode-mask": "^1.2.4", "react-native-vision-camera": "4.7.3" }, diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx index 0a80a26b0..b54ff5bea 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx +++ b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.tsx @@ -1,9 +1,11 @@ import { flattenStyles } from "@mendix/piw-native-utils-internal"; import { ValueStatus } from "mendix"; -import { ReactElement, useCallback, useMemo, useRef } from "react"; -import { View } from "react-native"; -import { Camera, useCodeScanner, Code, useCameraDevice } from "react-native-vision-camera"; +import { useIsFocused } from "@react-navigation/native"; +import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { AppState, AppStateStatus, LayoutChangeEvent, StyleSheet, TouchableOpacity, View } from "react-native"; +import { Camera, useCodeScanner, Code, useCameraDevice, CodeScannerFrame } from "react-native-vision-camera"; import BarcodeMask from "react-native-barcode-mask"; +import Icon from "react-native-vector-icons/MaterialIcons"; import { BarcodeScannerProps } from "../typings/BarcodeScannerProps"; import { BarcodeScannerStyle, defaultBarcodeScannerStyle } from "./ui/styles"; @@ -13,24 +15,66 @@ export type Props = BarcodeScannerProps; export function BarcodeScanner(props: Props): ReactElement { const device = useCameraDevice("back"); + const isFocused = useIsFocused(); + const [appState, setAppState] = useState(AppState.currentState); + const [viewSize, setViewSize] = useState<{ width: number; height: number } | null>(null); + const [torchEnabled, setTorchEnabled] = useState(false); const styles = useMemo(() => flattenStyles(defaultBarcodeScannerStyle, props.style), [props.style]); // Ref to track the lock state const isLockedRef = useRef(false); + const maskSize = useMemo(() => { + const fallback = styles.mask.width ?? styles.mask.height ?? 200; + if (!viewSize) { + return fallback; + } + return Math.min(fallback, viewSize.width, viewSize.height); + }, [styles.mask.width, styles.mask.height, viewSize]); + const onCodeScanned = useCallback( - (codes: Code[]) => { + (codes: Code[], frame: CodeScannerFrame) => { // Block if still in cooldown if (isLockedRef.current) { return; } - if (props.barcode.status !== ValueStatus.Available || codes.length === 0 || !codes[0].value) { + let visibleCodes = codes; + if (props.showMask && viewSize && frame?.width && frame?.height) { + const roiViewX = Math.max(0, (viewSize.width - maskSize) / 2); + const roiViewY = Math.max(0, (viewSize.height - maskSize) / 2); + const scale = Math.max(viewSize.width / frame.width, viewSize.height / frame.height); + const offsetX = (frame.width * scale - viewSize.width) / 2; + const offsetY = (frame.height * scale - viewSize.height) / 2; + + visibleCodes = codes.filter(code => { + if (!code.corners || code.corners.length === 0) { + return false; + } + return code.corners.every(corner => { + const viewX = corner.x * scale - offsetX; + const viewY = corner.y * scale - offsetY; + return ( + viewX >= roiViewX && + viewX <= roiViewX + maskSize && + viewY >= roiViewY && + viewY <= roiViewY + maskSize + ); + }); + }); + } + + if ( + props.barcode.status !== ValueStatus.Available || + visibleCodes.length === 0 || + visibleCodes.length > 1 || + !visibleCodes[0].value + ) { return; } - const { value } = codes[0]; + const { value } = visibleCodes[0]; if (value !== props.barcode.value) { props.barcode.setValue(value); } @@ -43,7 +87,7 @@ export function BarcodeScanner(props: Props): ReactElement { isLockedRef.current = false; }, 2000); }, - [props.barcode, props.onDetect] + [maskSize, props.barcode, props.onDetect, props.showMask, viewSize] ); const codeScanner = useCodeScanner({ @@ -63,27 +107,62 @@ export function BarcodeScanner(props: Props): ReactElement { onCodeScanned }); + useEffect(() => { + const subscription = AppState.addEventListener("change", nextState => setAppState(nextState)); + return () => subscription.remove(); + }, []); + + const isActive = isFocused && appState === "active"; + const flashTop = useMemo(() => { + if (!viewSize) { + return 16; + } + const centerY = viewSize.height / 2; + const preferred = centerY + maskSize / 2 + 16; + return Math.min(preferred, viewSize.height - 60); + }, [maskSize, viewSize]); + + const onLayout = useCallback((event: LayoutChangeEvent) => { + const { width, height } = event.nativeEvent.layout; + setViewSize({ width, height }); + }, []); + return ( - + {device && ( - + + + {props.showFlashToggle && ( + setTorchEnabled(enabled => !enabled)} + accessibilityLabel="Toggle flash" + testID={`${props.name}-flash-toggle`} + > + + + )} {props.showMask && ( - + + + )} - + )} ); diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml index b8f87cf56..11cc159e6 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml +++ b/packages/pluggableWidgets/barcode-scanner-native/src/BarcodeScanner.xml @@ -1,4 +1,4 @@ - + Barcode scanner Scan barcode and QR code values. @@ -11,7 +11,7 @@ Barcode The attribute that will receive the scanned barcode value. - + @@ -25,14 +25,20 @@ + + + Show flash toggle + Allow users to toggle the camera flash while scanning. + + On detect - + - + diff --git a/packages/pluggableWidgets/barcode-scanner-native/src/ui/styles.tsx b/packages/pluggableWidgets/barcode-scanner-native/src/ui/styles.tsx index 6ad1c7b94..3f46a80f5 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/src/ui/styles.tsx +++ b/packages/pluggableWidgets/barcode-scanner-native/src/ui/styles.tsx @@ -19,6 +19,27 @@ export const defaultBarcodeScannerStyle: BarcodeScannerStyle = { }, mask: { color: "#62B1F6", + width: 200, + height: 200, backgroundColor: "rgba(0, 0, 0, 0.6)" + }, + cameraWrapper: { + flex: 1, + position: "relative" + }, + camera: { + flex: 1, + justifyContent: "center", + alignItems: "center" + }, + flashToggle: { + position: "absolute", + alignSelf: "center", + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: "rgba(0, 0, 0, 0.45)", + justifyContent: "center", + alignItems: "center" } }; diff --git a/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts b/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts index b7bc06952..4644d1a67 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts +++ b/packages/pluggableWidgets/barcode-scanner-native/typings/BarcodeScannerProps.d.ts @@ -12,6 +12,7 @@ export interface BarcodeScannerProps