One package: expo-camera
If you searched this a year ago you’d have found expo-barcode-scanner. That package is deprecated — its scanning is now built into expo-camera, which is the one library you need. It exposes a CameraView component with an onBarcodeScanned callback. That callback is the entire feature.
Like any camera feature, scanning is native, so the final app needs a development build rather than Expo Go. Everything else is plain React.
Step 1 — Install and permit
npx expo install expo-cameraCamera access needs an explicit permission and a usage string. Add the reason to app.jsonso App Store review doesn’t reject you:
{
"expo": {
"plugins": [
["expo-camera", {
"cameraPermission": "We use the camera to scan QR codes."
}]
]
}
}Step 2 — The full scanner screen
This is a complete, working screen. Note the scanned flag — without it, onBarcodeScanned fires continuously and you get the same code a hundred times:
import { useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
export default function ScannerScreen() {
const [permission, requestPermission] = useCameraPermissions();
const [scanned, setScanned] = useState(false);
const [result, setResult] = useState('');
if (!permission) return <View />;
if (!permission.granted) {
return (
<View style={styles.center}>
<Text style={{ color: 'white', marginBottom: 12 }}>
Camera access is needed to scan.
</Text>
<Button title="Grant permission" onPress={requestPermission} />
</View>
);
}
const onScan = ({ data }) => {
if (scanned) return; // guard: handle each code once
setScanned(true);
setResult(data);
// do something: look up ticket, open URL, add inventory…
};
return (
<View style={styles.center}>
<CameraView
style={StyleSheet.absoluteFill}
barcodeScannerSettings={{ barcodeTypes: ['qr', 'ean13', 'code128'] }}
onBarcodeScanned={scanned ? undefined : onScan}
/>
{scanned && (
<View style={styles.overlay}>
<Text style={{ color: 'white' }}>Scanned: {result}</Text>
<Button title="Scan again" onPress={() => setScanned(false)} />
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' },
overlay: { position: 'absolute', bottom: 60, alignItems: 'center', gap: 10 },
});Setting onBarcodeScanned to undefined once scanned is a belt-and-suspenders way to stop the callback entirely until you reset.
Step 3 — Do something with the result
The data string is whatever the code encodes. From there the app is just normal logic:
- Ticket / check-in: look the code up in your database, mark it used, show valid/invalid.
- Inventory: match the barcode to a product, increment or decrement stock.
- Link: if it’s a URL, open it — but confirm with the user first, never auto-navigate.
One security note: treat scanned data as untrusted input. A QR code can encode anything, so validate before you act — especially before opening a URL.
Step 4 — Build for a real device
npx expo run:ios # or eas build --profile developmentTest with an actual printed or on-screen code. The simulator can’t use a real camera, so device testing here isn’t optional.
The shortcut: describe it, don’t wire it
The scanner itself is 40 lines, but the surrounding app — the ticket database, the check-in list, the valid/used states — is the real work. That’s where an AI builder earns its place. Describe “an event check-in app that scans ticket QR codes and marks them used” in ShipNative and it wires expo-camera, the permission prompt, the scan guard, and a screen to see results — previewed live on your phone.
You bring the camera and a printed code; the boilerplate is already handled. See the loyalty-card build plan for a scanner-plus-database app you can generate today.
Build a scanner app free
Describe your scanning app in one sentence and see it running on your phone in minutes at shipnative.dev. No credit card, full Expo export whenever you want it.