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