Introduction:
Barcode scanners are essential tools in various applications, from inventory management to mobile shopping. In this blog, we'll explore how to create a barcode scanner app using React Native and the powerful react-native-vision-camera
library.
Scanner.js
// Import necessary modules and components
import { useEffect, useState } from 'react';
import {
Alert,
Image,
Linking,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
Camera,
useCameraDevice,
useCameraPermission,
useCodeScanner,
} from 'react-native-vision-camera';
// Import utility functions from 'utils'
import {
formatWifiData,
getCountryOfOriginFromBarcode,
openExternalLink,
} from './utils';
// Define the ScannerScreen component
export function ScannerScreen({ navigation }) {
// State variables
const [torchOn, setTorchOn] = useState(false);
const [enableOnCodeScanned, setEnableOnCodeScanned] = useState(true);
// Camera permission hooks
const {
hasPermission: cameraHasPermission,
requestPermission: requestCameraPermission,
} = useCameraPermission();
// Get the camera device (back camera)
const device = useCameraDevice('back');
// Handle camera permission on component mount
useEffect(() => {
handleCameraPermission();
}, []);
// Use the code scanner hook to configure barcode scanning
const codeScanner = useCodeScanner({
codeTypes: ['qr', 'ean-13'],
onCodeScanned: (codes) => {
// Check if code scanning is enabled
if (enableOnCodeScanned) {
let value = codes[0]?.value;
let type = codes[0]?.type;
console.log(codes[0]);
// Handle QR code
if (type === 'qr') {
openExternalLink(value).catch((error) => {
showAlert('Detail', formatWifiData(value), false);
});
} else {
// Handle other barcode types
const countryOfOrigin = getCountryOfOriginFromBarcode(value);
console.log(`Country of Origin for ${value}: ${countryOfOrigin}`);
showAlert(value, countryOfOrigin);
}
// Disable code scanning to prevent rapid scans
setEnableOnCodeScanned(false);
}
},
});
// Handle camera permission
const handleCameraPermission = async () => {
const granted = await requestCameraPermission();
if (!granted) {
alert(
'Camera permission is required to use the camera. Please grant permission in your device settings.'
);
// Optionally, open device settings using Linking API
Linking.openSettings();
}
};
// Show alert with customizable content
const showAlert = (
value = '',
countryOfOrigin = '',
showMoreBtn = true
) => {
Alert.alert(
value,
countryOfOrigin,
showMoreBtn
? [
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{
text: 'More',
onPress: () => {
setTorchOn(false);
setEnableOnCodeScanned(true);
openExternalLink(
'https://www.barcodelookup.com/' + value
);
},
},
]
: [
{
text: 'Cancel',
onPress: () => setEnableOnCodeScanned(true),
style: 'cancel',
},
],
{ cancelable: false }
);
};
// Round button component with image
const RoundButtonWithImage = () => {
return (
<TouchableOpacity
onPress={() => setTorchOn((prev) => !prev)}
style={styles.buttonContainer}>
<View style={styles.button}>
<Image
source={
torchOn
? require('./assets/flashlight_on.png')
: require('./assets/torch_off.png')
}
style={styles.buttonImage}
/>
</View>
</TouchableOpacity>
);
};
// Render content based on camera device availability
if (device == null)
return (
<View
style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
>
<Text style={{ margin: 10 }}>Camera Not Found</Text>
</View>
);
// Return the main component structure
return (
<SafeAreaView style={{ flex: 1 }}>
<RoundButtonWithImage />
<Camera
codeScanner={codeScanner}
style={StyleSheet.absoluteFill}
device={device}
isActive={true}
torch={torchOn ? 'on' : 'off'}
onTouchEnd={() => setEnableOnCodeScanned(true)}
/>
</SafeAreaView>
);
}
// Styles for the component
const styles = StyleSheet.create({
buttonContainer: {
alignItems: 'center',
position: 'absolute',
zIndex: 1,
right: 20,
top: 20,
},
button: {
backgroundColor: '#FFF', // Button background color
borderRadius: 50, // Make it round (half of the width and height)
width: 50,
height: 50,
justifyContent: 'center',
alignItems: 'center',
},
buttonImage: {
width: 25, // Adjust the width and height of the image as needed
height: 25,
},
});
utils.js
// Import Linking from react-native
const { Linking } = require('react-native');
// Function to open an external link
export const openExternalLink = url => {
return Linking.openURL(url)
.then(() => {
console.log('Link opened successfully');
})
.catch(err => {
console.error('Error opening external link:', err);
throw err; // Rethrow the error for the calling code to handle
});
};
// Function to format WiFi data
export const formatWifiData = wifiData => {
// Split the WiFi data into parts and filter out empty parts
const dataParts = wifiData.split(';').filter(part => part.trim() !== '');
// Format each part as key-value pairs and join them with newline
const formattedData = dataParts.map(part => {
const [key, value] = part.split(':');
return `${key}: ${value}`;
});
return formattedData.join('\n');
};
// Function to get the country of origin from a barcode
export const getCountryOfOriginFromBarcode = barcode => {
// Extract the first three digits of the barcode as the prefix
const prefix = parseInt(barcode.substring(0, 3), 10);
// Define ranges for country codes and their corresponding countries
const countryRanges = {
'000-019': 'United States and Canada',
'020-029': 'Restricted distribution',
'030-039': 'United States drugs (National Drug Code)',
'040-049':
'Used to issue restricted circulation numbers within a geographic region',
'050-059': 'Reserved for future use',
'060-099': 'United States and Canada',
'100-139': 'United States',
'200-299': 'Restricted distribution',
'300-379': 'France and Monaco',
380: 'Bulgaria',
383: 'Slovenia',
385: 'Croatia',
387: 'Bosnia and Herzegovina',
389: 'Montenegro',
'400-440': 'Germany',
'450-459': 'Japan',
'460-469': 'Russia',
470: 'Kyrgyzstan',
471: 'Taiwan',
474: 'Estonia',
475: 'Latvia',
476: 'Azerbaijan',
477: 'Lithuania',
478: 'Uzbekistan',
479: 'Sri Lanka',
480: 'Philippines',
481: 'Belarus',
482: 'Ukraine',
484: 'Moldova',
485: 'Armenia',
486: 'Georgia',
487: 'Kazakhstan',
488: 'Tajikistan',
489: 'Hong Kong',
'490-499': 'Japan',
'500-509': 'United Kingdom',
'520-521': 'Greece',
528: 'Lebanon',
529: 'Cyprus',
530: 'Albania',
531: 'Macedonia',
535: 'Malta',
539: 'Ireland',
'540-549': 'Belgium and Luxembourg',
560: 'Portugal',
569: 'Iceland',
'570-579': 'Denmark, Faroe Islands, and Greenland',
590: 'Poland',
594: 'Romania',
599: 'Hungary',
'600-601': 'South Africa',
603: 'Ghana',
604: 'Senegal',
608: 'Bahrain',
609: 'Mauritius',
611: 'Morocco',
613: 'Algeria',
615: 'Nigeria',
616: 'Kenya',
618: 'Côte d’Ivoire',
619: 'Tunisia',
621: 'Syria',
622: 'Egypt',
624: 'Libya',
625: 'Jordan',
626: 'Iran',
627: 'Kuwait',
628: 'Saudi Arabia',
629: 'United Arab Emirates',
'640-649': 'Finland',
'690-695': 'China',
'700-709': 'Norway',
729: 'Israel',
'730-739': 'Sweden',
740: 'Guatemala',
741: 'El Salvador',
742: 'Honduras',
743: 'Nicaragua',
744: 'Costa Rica',
750: 'Mexico',
'754-755': 'Canada',
759: 'Venezuela',
'760-769': 'Switzerland and Liechtenstein',
'770-771': 'Colombia',
773: 'Uruguay',
775: 'Peru',
777: 'Bolivia',
779: 'Argentina',
780: 'Chile',
784: 'Paraguay',
785: 'Peru',
786: 'Ecuador',
'789-790': 'Brazil',
'800-839': 'Italy, San Marino, and Vatican City',
'840-849': 'Spain and Andorra',
850: 'Cuba',
858: 'Slovakia',
859: 'Czech Republic',
860: 'Serbia',
865: 'Mongolia',
867: 'North Korea',
'868-869': 'Turkey',
'870-879': 'Netherlands',
880: 'South Korea',
884: 'Cambodia',
885: 'Thailand',
888: 'Singapore',
890: 'India',
893: 'Vietnam',
896: 'Pakistan',
899: 'Indonesia',
'900-919': 'Austria',
'930-939': 'Australia',
'940-949': 'New Zealand',
955: 'Malaysia',
958: 'Macau',
'960-969': 'GS1 Global Office: GTIN-8 allocations',
977: 'Serial publications (ISSN)',
'978-979': 'Bookland (ISBN)',
980: 'Refund receipts',
'981-984': 'GS1 coupon identification for common currency areas',
'990-999': 'Coupon identification',
};
// Iterate through the country ranges to find the matching range
for (const range in countryRanges) {
const [start, end] = range.split('-').map(Number);
// Check if the prefix falls within the current range
if (prefix >= start && prefix <= end) {
return countryRanges[range];
}
}
// If no matching range is found, return a default message
return 'Country not specified';
};
Conclusion:
Congratulations! You've successfully built a barcode scanner app with React Native and the react-native-vision-camera
library. Feel free to customize and enhance the app based on your specific requirements. Happy coding!
Top comments (2)
Nice tutorial! The Vision Camera approach is great for getting started with barcode scanning in React Native.
One heads-up for developers: while this works well for basic QR codes and EAN/UPC barcodes, Vision Camera can struggle with denser formats like Code 128 or PDF417, especially in challenging conditions (low light, blurry images, damaged codes).
For those building production apps that need more robust scanning capabilities, you might want to consider a commercial solution, like Scanbot SDK (disclosure: I'm part of their team). These come with support during integration and ready-to-use scanning interfaces and features like multi-scanning or AR overlays.
Here's a quick step-by-step integration guide.
Hello. Thanks for your detailed explanation. But I have got a issue while trying with 'react-native-vision-camera' module.
Please help me.
Could not determine the dependencies of task ':react-native-vision-camera:compileDebugAidl'.