Replace expo-camera which uses non-FOSS libs with react-native-vision-camera (#1405)

This commit is contained in:
Leendert de Borst
2025-11-27 23:44:15 +01:00
committed by Leendert de Borst
parent c459a48927
commit 6a4fbb9193
5 changed files with 110 additions and 69 deletions

View File

@@ -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

View File

@@ -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<string>());
@@ -65,35 +66,51 @@ export default function QRScannerScreen() : React.ReactNode {
* Request camera permission.
*/
const requestCameraPermission = async () : Promise<void> => {
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 (
<ThemedContainer>
<View style={styles.loadingContainer}>
@@ -193,13 +224,11 @@ export default function QRScannerScreen() : React.ReactNode {
return (
<ThemedContainer style={styles.container}>
<View style={styles.cameraContainer}>
<CameraView
<Camera
style={styles.camera}
facing="back"
barcodeScannerSettings={{
barcodeTypes: ['qr'],
}}
onBarcodeScanned={handleBarcodeScanned}
device={device}
isActive={true}
codeScanner={codeScanner}
>
<View style={styles.cameraOverlay}>
<Ionicons name="qr-code-outline" size={100} color={colors.white} />
@@ -207,7 +236,7 @@ export default function QRScannerScreen() : React.ReactNode {
{t('settings.qrScanner.scanningMessage')}
</ThemedText>
</View>
</CameraView>
</Camera>
</View>
</ThemedContainer>
);

View File

@@ -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

View File

@@ -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"

View File

@@ -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": {