[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:
@@ -26,6 +26,7 @@
|
|||||||
"react-native-screens": "4.25.2",
|
"react-native-screens": "4.25.2",
|
||||||
"react-native-svg": "15.15.5",
|
"react-native-svg": "15.15.5",
|
||||||
"react-native-vision-camera": "5.2.3",
|
"react-native-vision-camera": "5.2.3",
|
||||||
|
"react-native-vision-camera-barcode-scanner": "^5.2.3",
|
||||||
"react-native-worklets": "0.10.0",
|
"react-native-worklets": "0.10.0",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwind-variants": "^3.3.1",
|
"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": ["[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-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=="],
|
"react-refresh": ["[email protected]", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"react-native-screens": "4.25.2",
|
"react-native-screens": "4.25.2",
|
||||||
"react-native-svg": "15.15.5",
|
"react-native-svg": "15.15.5",
|
||||||
"react-native-vision-camera": "5.2.3",
|
"react-native-vision-camera": "5.2.3",
|
||||||
|
"react-native-vision-camera-barcode-scanner": "5.2.3",
|
||||||
"react-native-worklets": "0.10.0",
|
"react-native-worklets": "0.10.0",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwind-variants": "^3.3.1",
|
"tailwind-variants": "^3.3.1",
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
Camera,
|
Camera,
|
||||||
useCameraDevice,
|
useCameraDevice,
|
||||||
useCameraPermission,
|
useCameraPermission,
|
||||||
useObjectOutput,
|
|
||||||
} from 'react-native-vision-camera';
|
} from 'react-native-vision-camera';
|
||||||
|
import { useBarcodeScannerOutput } from 'react-native-vision-camera-barcode-scanner';
|
||||||
import { ScanLine, X } from 'lucide-react-native';
|
import { ScanLine, X } from 'lucide-react-native';
|
||||||
import { AppText, Button, ErrorBanner, Field, Header, TextInputBox } from '../components/ui';
|
import { AppText, Button, ErrorBanner, Field, Header, TextInputBox } from '../components/ui';
|
||||||
import { useAlarm } from '../state/AlarmContext';
|
import { useAlarm } from '../state/AlarmContext';
|
||||||
@@ -158,32 +158,42 @@ export default function ScannerScreen() {
|
|||||||
[busy, joinAlert, navigation, perms],
|
[busy, joinAlert, navigation, perms],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onScanned = useCallback(
|
// Both platforms scan through MLKit here. The core library's
|
||||||
(objects: readonly { type: string; value?: string }[]) => {
|
// `useObjectOutput` is Apple-Vision only: on Android its native factory
|
||||||
for (const obj of objects) {
|
// throws `CameraObjectOutput is not available on Android!` the moment the
|
||||||
const value = typeof obj.value === 'string' ? obj.value : undefined;
|
// screen mounts, which crashed the app on "Scan a QR code". The Barcode
|
||||||
if (obj.type !== 'qr' || !value) continue;
|
// Scanner output is cross-platform (MLKit on Android, Vision on iOS).
|
||||||
|
const handleScanValue = useCallback(
|
||||||
|
(value: string) => {
|
||||||
const parsed = parseQrPayload(value);
|
const parsed = parseQrPayload(value);
|
||||||
if (parsed.ok) {
|
if (parsed.ok) {
|
||||||
if (targetAlertId && parsed.payload.alertId !== targetAlertId) {
|
if (targetAlertId && parsed.payload.alertId !== targetAlertId) {
|
||||||
setError('That code is for a different alert.');
|
setError('That code is for a different alert.');
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
complete(parsed.payload.alertId, parsed.payload.subscribeToken);
|
complete(parsed.payload.alertId, parsed.payload.subscribeToken);
|
||||||
} else {
|
} else {
|
||||||
setError(qrErrorMessage(parsed.error));
|
setError(qrErrorMessage(parsed.error));
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[complete, targetAlertId],
|
[complete, targetAlertId],
|
||||||
);
|
);
|
||||||
|
|
||||||
// v5 ships the scanner in core: `useObjectOutput` is the code-scanner — there
|
const barcodeOutput = useBarcodeScannerOutput({
|
||||||
// is no separate vision-camera-code-scanner plugin for 5.x.
|
barcodeFormats: ['qr-code'],
|
||||||
const objectOutput = useObjectOutput({
|
onBarcodeScanned(barcodes) {
|
||||||
types: ['qr'],
|
for (const barcode of barcodes) {
|
||||||
onObjectsScanned: onScanned,
|
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 () => {
|
const submitManual = async () => {
|
||||||
@@ -236,7 +246,7 @@ export default function ScannerScreen() {
|
|||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
device={device}
|
device={device}
|
||||||
isActive={!joinedName}
|
isActive={!joinedName}
|
||||||
outputs={[objectOutput]}
|
outputs={[barcodeOutput]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.permission}>
|
<View style={styles.permission}>
|
||||||
|
|||||||
Reference in New Issue
Block a user