From 6a4fbb9193268d8f269f0f4aaa2900704811aa2a Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 27 Nov 2025 23:44:15 +0100 Subject: [PATCH] Replace expo-camera which uses non-FOSS libs with react-native-vision-camera (#1405) --- apps/mobile-app/android/gradle.properties | 3 + .../app/(tabs)/settings/qr-scanner.tsx | 87 ++++++++++++------- apps/mobile-app/ios/Podfile.lock | 25 +++--- apps/mobile-app/package-lock.json | 58 ++++++++----- apps/mobile-app/package.json | 6 +- 5 files changed, 110 insertions(+), 69 deletions(-) diff --git a/apps/mobile-app/android/gradle.properties b/apps/mobile-app/android/gradle.properties index a0a18dd2a..889455a91 100644 --- a/apps/mobile-app/android/gradle.properties +++ b/apps/mobile-app/android/gradle.properties @@ -52,6 +52,9 @@ expo.webp.animated=false # Enable network inspector EX_DEV_CLIENT_NETWORK_INSPECTOR=true +# Enable VisionCamera code scanner +VisionCamera_enableCodeScanner=true + # Use legacy packaging to compress native libraries in the resulting APK. expo.useLegacyPackaging=false diff --git a/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx b/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx index 68b3b2893..9f2f250eb 100644 --- a/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx +++ b/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx @@ -1,8 +1,8 @@ import { Ionicons } from '@expo/vector-icons'; -import { CameraView, useCameraPermissions } from 'expo-camera'; import { Href, router, useLocalSearchParams } from 'expo-router'; import { useEffect, useCallback, useRef } from 'react'; -import { View, Alert, StyleSheet } from 'react-native'; +import { View, Alert, StyleSheet, Linking } from 'react-native'; +import { Camera, useCameraDevice, useCameraPermission, useCodeScanner } from 'react-native-vision-camera'; import { useColors } from '@/hooks/useColorScheme'; import { useTranslation } from '@/hooks/useTranslation'; @@ -54,7 +54,8 @@ function parseQRCode(data: string): ScannedQRCode { export default function QRScannerScreen() : React.ReactNode { const colors = useColors(); const { t } = useTranslation(); - const [permission, requestPermission] = useCameraPermissions(); + const { hasPermission, requestPermission } = useCameraPermission(); + const device = useCameraDevice('back'); const { url } = useLocalSearchParams<{ url?: string }>(); const hasProcessedUrl = useRef(false); const processedUrls = useRef(new Set()); @@ -65,35 +66,51 @@ export default function QRScannerScreen() : React.ReactNode { * Request camera permission. */ const requestCameraPermission = async () : Promise => { - if (!permission) { + if (hasPermission === undefined) { return; // Still loading permission status } - if (!permission.granted && permission.canAskAgain) { - // Request permission - await requestPermission(); - } else if (!permission.granted && !permission.canAskAgain) { - // Permission was permanently denied - Alert.alert( - t('settings.qrScanner.cameraPermissionTitle'), - t('settings.qrScanner.cameraPermissionMessage'), - [{ text: t('common.ok'), /** - * Go back to the settings tab. - */ - onPress: (): void => router.back() }] - ); + if (!hasPermission) { + const permission = await requestPermission(); + + if (!permission) { + // Permission was denied + Alert.alert( + t('settings.qrScanner.cameraPermissionTitle'), + t('settings.qrScanner.cameraPermissionMessage'), + [ + { text: t('common.cancel'), + /** + * Go back to the settings tab. + */ + onPress: (): void => router.back(), + style: 'cancel' + }, + { + text: t('common.openSettings'), + /** + * Open app settings. + */ + onPress: (): void => { + Linking.openSettings(); + router.back(); + } + } + ] + ); + } } }; requestCameraPermission(); - }, [permission, requestPermission, t]); + }, [hasPermission, requestPermission, t]); /* * Handle barcode scanned - parse and navigate to appropriate page. * Only processes AliasVault QR codes, silently ignores others. * Validation is handled by the destination page. */ - const handleBarcodeScanned = useCallback(({ data }: { data: string }) : void => { + const handleBarcodeScanned = useCallback((data: string) : void => { // Prevent processing the same URL multiple times if (processedUrls.current.has(data)) { return; @@ -119,6 +136,20 @@ export default function QRScannerScreen() : React.ReactNode { } }, []); + // Configure code scanner + const codeScanner = useCodeScanner({ + codeTypes: ['qr'], + /** + * Handle QR code scanned. + * @param codes - Scanned codes. + */ + onCodeScanned: (codes) => { + if (codes.length > 0 && codes[0]?.value) { + handleBarcodeScanned(codes[0].value); + } + }, + }); + /** * Reset hasProcessedUrl when URL changes to allow processing new URLs. */ @@ -132,7 +163,7 @@ export default function QRScannerScreen() : React.ReactNode { useEffect(() => { if (url && typeof url === 'string' && !hasProcessedUrl.current) { hasProcessedUrl.current = true; - handleBarcodeScanned({ data: url }); + handleBarcodeScanned(url); } }, [url, handleBarcodeScanned]); @@ -179,8 +210,8 @@ export default function QRScannerScreen() : React.ReactNode { }, }); - // Show permission request screen - if (!permission || !permission.granted) { + // Show permission request screen or loading if device not ready + if (hasPermission === undefined || !hasPermission || !device) { return ( @@ -193,13 +224,11 @@ export default function QRScannerScreen() : React.ReactNode { return ( - @@ -207,7 +236,7 @@ export default function QRScannerScreen() : React.ReactNode { {t('settings.qrScanner.scanningMessage')} - + ); diff --git a/apps/mobile-app/ios/Podfile.lock b/apps/mobile-app/ios/Podfile.lock index 42b9df0ad..965acbac4 100644 --- a/apps/mobile-app/ios/Podfile.lock +++ b/apps/mobile-app/ios/Podfile.lock @@ -272,10 +272,6 @@ PODS: - ExpoModulesCore - ExpoBlur (14.1.5): - ExpoModulesCore - - ExpoCamera (16.1.11): - - ExpoModulesCore - - ZXingObjC/OneD - - ZXingObjC/PDF417 - ExpoClipboard (7.1.5): - ExpoModulesCore - ExpoDocumentPicker (13.1.6): @@ -2448,12 +2444,13 @@ PODS: - SQLite.swift/standard (0.14.1) - SwiftLint (0.59.1) - SWXMLHash (7.0.2) + - VisionCamera (4.7.3): + - VisionCamera/Core (= 4.7.3) + - VisionCamera/React (= 4.7.3) + - VisionCamera/Core (4.7.3) + - VisionCamera/React (4.7.3): + - React-Core - Yoga (0.0.0) - - ZXingObjC/Core (3.6.9) - - ZXingObjC/OneD (3.6.9): - - ZXingObjC/Core - - ZXingObjC/PDF417 (3.6.9): - - ZXingObjC/Core DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) @@ -2468,7 +2465,6 @@ DEPENDENCIES: - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoBlur (from `../node_modules/expo-blur/ios`) - - ExpoCamera (from `../node_modules/expo-camera/ios`) - ExpoClipboard (from `../node_modules/expo-clipboard/ios`) - ExpoDocumentPicker (from `../node_modules/expo-document-picker/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) @@ -2571,6 +2567,7 @@ DEPENDENCIES: - SignalArgon2 (~> 1.3.2) - SQLite.swift (~> 0.14.0) - SwiftLint + - VisionCamera (from `../node_modules/react-native-vision-camera`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -2582,7 +2579,6 @@ SPEC REPOS: - SQLite.swift - SwiftLint - SWXMLHash - - ZXingObjC EXTERNAL SOURCES: boost: @@ -2609,8 +2605,6 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-asset/ios" ExpoBlur: :path: "../node_modules/expo-blur/ios" - ExpoCamera: - :path: "../node_modules/expo-camera/ios" ExpoClipboard: :path: "../node_modules/expo-clipboard/ios" ExpoDocumentPicker: @@ -2804,6 +2798,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-screens" RNSVG: :path: "../node_modules/react-native-svg" + VisionCamera: + :path: "../node_modules/react-native-vision-camera" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -2820,7 +2816,6 @@ SPEC CHECKSUMS: expo-dev-menu-interface: 609c35ae8b97479cdd4c9e23c8cf6adc44beea0e ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6 ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9 - ExpoCamera: e1879906d41184e84b57d7643119f8509414e318 ExpoClipboard: 436f6de6971f14eb75ae160e076d9cb3b19eb795 ExpoDocumentPicker: b263a279685b6640b8c8bc70d71c83067aeaae55 ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63 @@ -2924,8 +2919,8 @@ SPEC CHECKSUMS: SQLite.swift: 2992550ebf3c5b268bf4352603e3df87d2a4ed72 SwiftLint: 3d48e2fb2a3468fdaccf049e5e755df22fb40c2c SWXMLHash: dd733a457e9c4fe93b1538654057aefae4acb382 + VisionCamera: 7187b3dac1ff3071234ead959ce311875748e14f Yoga: dc7c21200195acacb62fa920c588e7c2106de45e - ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 PODFILE CHECKSUM: ac288e273086bafdd610cafff08ccca0d164f7c3 diff --git a/apps/mobile-app/package-lock.json b/apps/mobile-app/package-lock.json index ec42f7b2c..3693cea40 100644 --- a/apps/mobile-app/package-lock.json +++ b/apps/mobile-app/package-lock.json @@ -17,10 +17,8 @@ "@types/jsrsasign": "^10.5.15", "expo": "^53.0.22", "expo-blur": "~14.1.5", - "expo-camera": "^16.1.11", "expo-clipboard": "~7.1.5", "expo-constants": "~17.1.7", - "expo-dev-client": "~5.1.8", "expo-document-picker": "~13.1.6", "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", @@ -56,6 +54,7 @@ "react-native-screens": "~4.15.4", "react-native-svg": "15.11.2", "react-native-toast-message": "^2.2.1", + "react-native-vision-camera": "^4.7.3", "react-native-webview": "13.13.5", "secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0", "yup": "^1.6.1" @@ -77,6 +76,7 @@ "eslint-config-expo": "~9.2.0", "eslint-plugin-jsdoc": "^55.2.0", "eslint-plugin-react-native": "^5.0.0", + "expo-dev-client": "~5.1.8", "globals": "^16.3.0", "jest": "^29.2.1", "jest-expo": "~53.0.0", @@ -8756,26 +8756,6 @@ "react-native": "*" } }, - "node_modules/expo-camera": { - "version": "16.1.11", - "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-16.1.11.tgz", - "integrity": "sha512-etA5ZKoC6nPBnWWqiTmlX//zoFZ6cWQCCIdmpUHTGHAKd4qZNCkhPvBWbi8o32pDe57lix1V4+TPFgEcvPwsaA==", - "license": "MIT", - "dependencies": { - "invariant": "^2.2.4" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*", - "react-native-web": "*" - }, - "peerDependenciesMeta": { - "react-native-web": { - "optional": true - } - } - }, "node_modules/expo-clipboard": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz", @@ -8806,6 +8786,7 @@ "version": "5.1.8", "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.1.8.tgz", "integrity": "sha512-IopYPgBi3JflksO5ieTphbKsbYHy9iIVdT/d69It++y0iBMSm0oBIoDmUijrHKjE3fV6jnrwrm8luU13/mzIQQ==", + "dev": true, "license": "MIT", "dependencies": { "expo-dev-launcher": "5.1.11", @@ -8822,6 +8803,7 @@ "version": "5.1.11", "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.1.11.tgz", "integrity": "sha512-bN0+nv5H038s8Gzf8i16hwCyD3sWDmHp7vb+QbL1i6B3XNnICCKS/H/3VH6H3PRMvCmoLGPlg+ODDqGlf0nu3g==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "8.11.0", @@ -8837,6 +8819,7 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -8853,12 +8836,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/expo-dev-menu": { "version": "6.1.10", "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.1.10.tgz", "integrity": "sha512-LaI0Bw5zzw5XefjYSX6YaMydzk0YBysjqQoxzj6ufDyKgwAfPmFwOLkZ03DOSerc9naezGLNAGgTEN6QTgMmgQ==", + "dev": true, "license": "MIT", "dependencies": { "expo-dev-menu-interface": "1.10.0" @@ -8871,6 +8856,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz", "integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==", + "dev": true, "license": "MIT", "peerDependencies": { "expo": "*" @@ -8922,6 +8908,7 @@ "version": "0.15.0", "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "dev": true, "license": "MIT" }, "node_modules/expo-keep-awake": { @@ -8989,6 +8976,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz", "integrity": "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==", + "dev": true, "license": "MIT", "dependencies": { "@expo/config": "~11.0.12", @@ -9140,6 +9128,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz", "integrity": "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==", + "dev": true, "license": "MIT", "peerDependencies": { "expo": "*" @@ -14963,6 +14952,30 @@ "react-native": "*" } }, + "node_modules/react-native-vision-camera": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-4.7.3.tgz", + "integrity": "sha512-g1/neOyjSqn1kaAa2FxI/qp5KzNvPcF0bnQw6NntfbxH6tm0+8WFZszlgb5OV+iYlB6lFUztCbDtyz5IpL47OA==", + "license": "MIT", + "peerDependencies": { + "@shopify/react-native-skia": "*", + "react": "*", + "react-native": "*", + "react-native-reanimated": "*", + "react-native-worklets-core": "*" + }, + "peerDependenciesMeta": { + "@shopify/react-native-skia": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + }, + "react-native-worklets-core": { + "optional": true + } + } + }, "node_modules/react-native-webview": { "version": "13.13.5", "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.13.5.tgz", @@ -17475,6 +17488,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" diff --git a/apps/mobile-app/package.json b/apps/mobile-app/package.json index 9e3990b5f..ae0569bd8 100644 --- a/apps/mobile-app/package.json +++ b/apps/mobile-app/package.json @@ -38,10 +38,8 @@ "@types/jsrsasign": "^10.5.15", "expo": "^53.0.22", "expo-blur": "~14.1.5", - "expo-camera": "^16.1.11", "expo-clipboard": "~7.1.5", "expo-constants": "~17.1.7", - "expo-dev-client": "~5.1.8", "expo-document-picker": "~13.1.6", "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", @@ -76,7 +74,9 @@ "react-native-safe-area-context": "5.6.1", "react-native-screens": "~4.15.4", "react-native-svg": "15.11.2", + "react-native-svg-transformer": "^1.5.0", "react-native-toast-message": "^2.2.1", + "react-native-vision-camera": "^4.7.3", "react-native-webview": "13.13.5", "secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0", "yup": "^1.6.1" @@ -87,6 +87,7 @@ "@eslint/js": "^9.35.0", "@react-native-community/cli": "^20.0.0", "@stylistic/eslint-plugin": "^5.3.1", + "expo-dev-client": "~5.1.8", "@types/fbemitter": "^2.0.35", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.16", @@ -101,7 +102,6 @@ "globals": "^16.3.0", "jest": "^29.2.1", "jest-expo": "~53.0.0", - "react-native-svg-transformer": "^1.5.0", "typescript": "~5.8.3" }, "engines": {