[fix](client): scan QR codes with MLKit's barcode output, not the iOS-only object output

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 `<Camera>` 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.
This commit is contained in:
2026-10-19 15:18:31 +01:00
parent b5c1710b99
commit b32df04ab9
3 changed files with 36 additions and 22 deletions
+3
View File
@@ -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": ["[email protected]", "", { "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": ["[email protected]", "", { "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": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
+1
View File
@@ -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",
+32 -22
View File
@@ -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]}
/>
) : (
<View style={styles.permission}>