DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on

Building a QR Code/Barcode Scanner App with React Native and Vision Camera

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.

GitHub Repo

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,
  },
});

Enter fullscreen mode Exit fullscreen mode

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';
};

Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Image description

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 (1)

Collapse
 
gold79 profile image
Kevin Zhang

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'.

Could not resolve all task dependencies for configuration ':react-native-vision-camera:debugCompileClasspath'.
Could not find any matches for com.facebook.react:react-android:+ as no versions of com.facebook.react:react-android are available.
Required by:
project :react-native-vision-camera