From b32df04ab910713961bceb3166126c149d793b03 Mon Sep 17 00:00:00 2001 From: Admin <1+admin@noreply.git.dcntrlized.org> Date: Mon, 19 Oct 2026 15:18:31 +0100 Subject: [PATCH] [fix](client): scan QR codes with MLKit's barcode output, not the iOS-only object output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping "Scan a QR code" crashed the app on Android: com.facebook.react.common.JavascriptException: Error: CameraFactory.createObjectOutput(...): java.lang.Error: CameraObjectOutput is not available on Android! `useObjectOutput` is Apple Vision only — the comment claiming v5 ships a cross-platform code scanner in core was wrong, and the native factory throws as soon as the screen mounts. The Barcode Scanner output is the cross-platform one (MLKit on Android, Vision on iOS); it is attached to the same `` via `outputs`, scans `qr-code` only, and keeps the hand-paste fallback. The scan callback now takes the barcode values directly instead of the object-output shape. --- bun.lock | 3 ++ package.json | 1 + src/screens/ScannerScreen.tsx | 54 +++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/bun.lock b/bun.lock index 79ffc93..0253ab1 100644 --- a/bun.lock +++ b/bun.lock @@ -26,6 +26,7 @@ "react-native-screens": "4.25.2", "react-native-svg": "15.15.5", "react-native-vision-camera": "5.2.3", + "react-native-vision-camera-barcode-scanner": "^5.2.3", "react-native-worklets": "0.10.0", "tailwind-merge": "^3.6.0", "tailwind-variants": "^3.3.1", @@ -1442,6 +1443,8 @@ "react-native-vision-camera": ["react-native-vision-camera@5.2.3", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-image": "*", "react-native-nitro-modules": "*" }, "optionalPeers": ["react-native-nitro-modules"] }, "sha512-OHjgM8BPzxppTqbF1FEltj+fxvisj4xdghM7ypoT1p6cOe28HByhOJP1c7LkBiUiZuotbsdbQwGdJ8nU7ZvPzA=="], + "react-native-vision-camera-barcode-scanner": ["react-native-vision-camera-barcode-scanner@5.2.3", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-image": "*", "react-native-nitro-modules": "*", "react-native-vision-camera": "*" }, "optionalPeers": ["react-native-nitro-modules"] }, "sha512-p8SyswRlr9Ty5gFcQ1qM8gmU8hKbHyRLyu9TKFMD7oUXsZDF93NKGvcr4Qdr16Z+AIj3HYA/fW1NgoeY+L353g=="], + "react-native-worklets": ["react-native-worklets@0.10.0", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.4" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.83 - 0.86" } }, "sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], diff --git a/package.json b/package.json index 21d0aa1..56f37cd 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-native-screens": "4.25.2", "react-native-svg": "15.15.5", "react-native-vision-camera": "5.2.3", + "react-native-vision-camera-barcode-scanner": "5.2.3", "react-native-worklets": "0.10.0", "tailwind-merge": "^3.6.0", "tailwind-variants": "^3.3.1", diff --git a/src/screens/ScannerScreen.tsx b/src/screens/ScannerScreen.tsx index 54b9e48..e3ce1a8 100644 --- a/src/screens/ScannerScreen.tsx +++ b/src/screens/ScannerScreen.tsx @@ -14,8 +14,8 @@ import { Camera, useCameraDevice, useCameraPermission, - useObjectOutput, } from 'react-native-vision-camera'; +import { useBarcodeScannerOutput } from 'react-native-vision-camera-barcode-scanner'; import { ScanLine, X } from 'lucide-react-native'; import { AppText, Button, ErrorBanner, Field, Header, TextInputBox } from '../components/ui'; import { useAlarm } from '../state/AlarmContext'; @@ -158,32 +158,42 @@ export default function ScannerScreen() { [busy, joinAlert, navigation, perms], ); - const onScanned = useCallback( - (objects: readonly { type: string; value?: string }[]) => { - for (const obj of objects) { - const value = typeof obj.value === 'string' ? obj.value : undefined; - if (obj.type !== 'qr' || !value) continue; - const parsed = parseQrPayload(value); - if (parsed.ok) { - if (targetAlertId && parsed.payload.alertId !== targetAlertId) { - setError('That code is for a different alert.'); - continue; - } - complete(parsed.payload.alertId, parsed.payload.subscribeToken); - } else { - setError(qrErrorMessage(parsed.error)); + // Both platforms scan through MLKit here. The core library's + // `useObjectOutput` is Apple-Vision only: on Android its native factory + // throws `CameraObjectOutput is not available on Android!` the moment the + // screen mounts, which crashed the app on "Scan a QR code". The Barcode + // Scanner output is cross-platform (MLKit on Android, Vision on iOS). + const handleScanValue = useCallback( + (value: string) => { + const parsed = parseQrPayload(value); + if (parsed.ok) { + if (targetAlertId && parsed.payload.alertId !== targetAlertId) { + setError('That code is for a different alert.'); + return; } - return; + complete(parsed.payload.alertId, parsed.payload.subscribeToken); + } else { + setError(qrErrorMessage(parsed.error)); } }, [complete, targetAlertId], ); - // v5 ships the scanner in core: `useObjectOutput` is the code-scanner — there - // is no separate vision-camera-code-scanner plugin for 5.x. - const objectOutput = useObjectOutput({ - types: ['qr'], - onObjectsScanned: onScanned, + const barcodeOutput = useBarcodeScannerOutput({ + barcodeFormats: ['qr-code'], + onBarcodeScanned(barcodes) { + for (const barcode of barcodes) { + const value = barcode.rawValue; + if (typeof value === 'string' && value.length > 0) { + handleScanValue(value); + return; + } + } + }, + onError(error) { + setError('The camera could not start scanning. You can paste the code by hand.'); + console.log('barcode scanner error', error.message); + }, }); const submitManual = async () => { @@ -236,7 +246,7 @@ export default function ScannerScreen() { style={StyleSheet.absoluteFill} device={device} isActive={!joinedName} - outputs={[objectOutput]} + outputs={[barcodeOutput]} /> ) : (