Getting Started with React Native CaptureSDK

Requirements

The Socket Mobile CaptureSDK uses Bluetooth Classic for the barcode scanner products and Bluetooth Low Energy (BLE) for the Contactless Reader/Writer products (Socket Mobile D600, S550).

Even though the React Native CaptureSDK allows to develop an app to run on iOS and Android the underlying Capture architecture on these 2 platforms is different.

On Android there is a service embedded in the Socket Mobile Companion app that is required in order to connect the Socket Mobile device to the Android host.

On iOS the communication with the Socket Mobile devices are embedded in the React Native Module therefore adding more configuration to be taken care of in the application itself.

Requirements for iOS platform

For applications that need to work with barcode scanners, make sure the following requirements are met:

  1. Your iOS application needs to be registered in our Apple MFI Approved Application list before submitting your application to the Apple Store. It will not pass the Apple Store review if this is not done.

  2. Your application must have the string com.socketmobile.chs in the Supported External Protocol setting.
    ExternalAccessory

3. Your application must add some security descriptions for the Bluetooth permissions shown here:
BluetoothPrivacy

Note

For more info on enabling your app for Android, check out the documentation pertaining to iOS.

Requirements for Android platform

  1. Add the below lines to your AndroidManifest.xml file, found in your app’s android/app/src/main folder, right under your opening <manifest> tag.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
  1. Add the below lines to your AndroidManifest.xml file.

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<meta-data android:name="com.socketmobile.capture.APP_KEY" android:value="YOUR_APP_KEY"/>
<meta-data android:name="com.socketmobile.capture.DEVELOPER_ID" android:value="YOUR_DEVELOPER_ID"/>
  1. Next, you will need to enable communication to Socket Mobile Companion. Right before your closing </manifest> tag, put the below quieries property.

<queries>
  <package android:name="com.socketmobile.companion"/>
</queries>
  1. Next you will need to update the <application> tag to include android:networkSecurityConfig="@xml/network_security_config". It should look something like the below.

<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:networkSecurityConfig="@xml/network_security_config">
  1. Next, add a file called network_security_config.xml to your xml directory, which can be found in your app’s android/app/src/main/res folder. This file should look like this.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="false" />
  <domain-config cleartextTrafficPermitted="true">
      <domain includeSubdomains="false">localhost</domain>
      <domain includeSubdomains="false">127.0.0.1</domain>
  </domain-config>
</network-security-config>
  1. The final step is to update your app’s build.gradle file, located at android/build.gradle, to include the below property.

allprojects {
  repositories {
      maven {
        url "https://bin.socketmobile.com/repo/releases"
      }
  }
}

Note

For more info on enabling your app for Android, check out the documentation pertaining to Android.

Requirements for both iOS and Android platforms

  1. Your application will need a SocketMobile AppKey. Follow the link to create an AppKey. AppKeys can be generated online and at no additional cost beyond the nominal one time registration fee. The AppKey is validated by the SDK library on the device, no internet connection is required. Note: You don’t need to create your own AppKey to compile and run the sample apps.

  2. The scanner needs to be paired with your devices in Application Mode. This can be done using Socket Mobile Companion app which can be downloaded from the App Store .

  3. Try our React Native sample app Single Entry React Native.

SDK Installation

The React Native CaptureSDK is released as a NPM (Node Package Manager) package.

Using yarn

yarn add react-native-capture

Using npm

npm install --save react-native-capture

Using CaptureSDK

First, once the NPM package has been installed CaptureSDK can be imported as shown below:

import { CaptureRn, CaptureEventIds, SktErrors } from 'react-native-capture'; // or 'react-native-capture' if using the deprecated version.

Here are the usual steps to follow:

  1. Open Capture with the App credentials and provide event handler function

  2. Handle device arrival and open the device in the event handler function

  3. Handle device removal and close the device in the event handler function

  4. Handle decoded data in the event handler function

Opening Capture with App credentials

The React Native CaptureSDK is an extension of the CaptureJS SDK. The main difference is the first instance that uses CaptureRn instead of Capture:

const appInfo: AppInfoRn = {
  appIdIos: 'ios:com.socketmobile.reactjs.native.example.example',
  appIdAndroid: 'android:com.example',
  developerId: 'ecc6c526-970b-ec11-b6e6-0022480a2304',
  appKeyIos: 'MC0CFHL9no0HS6LohlvgGj3s6R4fUTTGAhUAjkIUkoWjCi8NXAjDB9uk9WMdlJc=',
  appKeyAndroid:
    'MC0CFBJxr9ERxurLZQk8voZsFC7BH+8zAhUAxbT41GqB8EwOu7JtVYhffCnTdmI=',
};

const App = () => {
  const [capture] = useState(new CaptureRn());
  const [status, setStatus] = useState<string>('Opening Capture...');
  const [isOpen, setIsOpen] = useState<boolean>(false);

  const openCapture = () => {
    capture
      .open(appInfo, onCaptureEvent as Notification)
      .then(() => {
        setStatus('capture open success');
        setIsOpen(true);
      })
      .catch((err: any) => {
        myLogger.error(resToString(err));
        const {error} = err;
        const {code, message} = error;
        setStatus(`failed to open Capture: ${code} \n ${message}`);
        // this is for Android which requires Socket Mobile Companion
        if (code === SktErrors.ESKT_UNABLEOPENDEVICE) {
          setStatus('Is Socket Mobile Companion app installed?');
        }
      });
  };

Handle device arrival and open the device

When the application receives a Device Arrival notification, it can create a new CaptureRn object that represents the new device.

The application opens the device by passing GUID and the main CaptureRn reference as arguments of the device open function.

Opening the device allows to receive the decoded data from this device.

Note

the device GUID changes everytime the device connects. It identifies a connection session with a device.

Note

If a Socket Mobile device is already connected to the host prior to the app opening CaptureSDK, the device arrival notification will still be sent to make the application aware that the device is connected.

Note

The second argument of the onCaptureEvent, handle, is optional. It could be used to identify which Capture object is the source of the notification.

In the onCaptureEvent callback passed when opening Capture you could have code similar to this handling the device arrival notification:

const onCaptureEvent = (e, handle) => {
  if (!e) {
    return;
  }

  switch (e.id) {
    // **********************************
    // Device Arrival Event
    //   a device needs to be opened in
    //   to receive the decoded data
    //  e = {
    //    id: CaptureEventIds.DeviceArrival,
    //    type: CaptureEventTypes.DeviceInfo,
    //    value: {
    //      guid: "b876d9a8-85b6-1bb5-f1f6-1bb5d78a2c6e",
    //      name: "Socket S740 [E2ABB4]",
    //      type: CaptureDeviceType.ScannerS740
    //    }
    //  }
    // **********************************
    case CaptureEventIds.DeviceArrival:
      const newDevice = new CaptureRn();
      openDeviceHelper(newDevice, e, false);
      break;

    // OTHER EVENT CASES


}

const genDevice = (
  dev: CaptureRn,
  guid: String,
  name: String,
  type: number,
) => {
  return {
    guid,
    name,
    type,
    handle: dev.clientOrDeviceHandle,
    devCapture: dev,
  } as CaptureDeviceInfo;
};

const openDeviceHelper = (
  dev: CaptureRn,
  e: CaptureEvent<any>,
  isManager: boolean,
  ) => {
    let {name, guid, type} = e.value;
    let loggedOption = isManager ? 'device manager' : 'device';
    dev
      .openDevice(guid, capture)
      .then((result: number) => {
        myLogger.log(`opening a ${loggedOption} returns: `, `${result}`);
        setStatus(`result of opening ${name} : ${result}`);
        let myMap = {...stateRef.current.deviceGuidMap};
        if (!myMap[guid] && !isManager) {
          let device = genDevice(dev, guid, name, type);
          let devs = [...stateRef.current.devices, device];
          setDevices(devs);
          myMap[guid] = '1';
          setDeviceGuidMap(myMap);
        }
        if (!isManager) {
          // check for SocketCam device type
          if (SocketCamTypes.indexOf(e.value.type) > -1) {
            let device = genDevice(dev, guid, name, type);
            setSocketCamDevice(device);
          } else {
            setDeviceCapture(dev);
          }
        } else {
          setBleDeviceManagerCapture(dev);
          getFavorite(dev);
        }
      })
      .catch((res: JRpcError) => {
        let {error} = res;
        const {code, message} = error;
        myLogger.error(resToString(error));
        setStatus(`error opening a device: ${code} \n ${message}}`);
      });
  };

CaptureDeviceInfo

In the above code, genDevice creates an instance of CaptureDeviceInfo, which is a new interface used to combine a CaptureRn instance with a DeviceInfo instance. It is meant to combine the metadata from DeviceInfo with the core functionality of a CaptureRn instance.

This new interface will make device management and differentiation clearer in the UI, not only when using multiple devices but also to different device capture instances from the root capture instance.

Also in the above code, setDeviceGuid is being used to store different device guids in a hash map-this way you can more easily detect which devices are included in the list of devices and which are not by simply checking the map instead of searching through the devices array.

Handle device removal and close the device

The device removal occurs when the Socket Mobile is no longer connected to the host. It is recommended to close it.

In the onCaptureEvent callback passed when opening Capture you could have code similar to this:

// **********************************
// Device Removal Event
//   it is better to close the device
//  e = {
//    id: CaptureEventIds.DeviceRemoval,
//    type: CaptureEventTypes.DeviceInfo,
//    value: {
//      guid: "b876d9a8-85b6-1bb5-f1f6-1bb5d78a2c6e",
//      name: "Socket S740 [E2ABB4]",
//      type: CaptureDeviceType.ScannerS740
//    }
//  }
// **********************************
case CaptureEventIds.DeviceRemoval:
  let index = devs.findIndex((d: CaptureDeviceInfo) => {
      return d.guid === e.value.guid;
    });
    if (index < 0) {
      myLogger.error(`no matching devices found for ${e.value.name}`);
      return;
    } else {
      let removeDevice = devs[index];
      myLogger.log('removeDevice: ', removeDevice?.name);
      removeDevice!.devCapture
        .close()
        .then((result: number) => {
          myLogger.log('closing a device returns: ', `${result}`);
          setStatus(`result of closing ${removeDevice?.name}: ${result}`);
          devs.splice(index, 1);
          setDevices(devs);
          let myMap = {...stateRef.current.deviceGuidMap};
          delete myMap[e.value.guid];
          setDeviceGuidMap(myMap);
          let bleDeviceManagerCaptureDev =
            bleDeviceManagerCapture as CaptureDeviceInfo;
          if (
            bleDeviceManagerCaptureDev &&
            e.value.guid === bleDeviceManagerCaptureDev.guid
          ) {
            setBleDeviceManagerCapture(null);
          } else {
            setDeviceCapture(null);
          }
        })
        .catch((res: JRpcError) => {
          let {error} = res;
          let {message, code} = error;
          myLogger.error(`error closing a device: ${code}: ${message}`);
          setStatus(`error closing a device: ${code}: ${message}`);
        });
    }
  break;

    // OTHER EVENT CASES

Handle decoded data in the event handler function

Each time a Socket Mobile device is successful at reading a barcode or an NFC tag, the decoded data notification is sent and can be handled as shown here:

Note

Capture does not interpret the decoded data, only the application knows how to interpret it. For demonstration purpose the decoded data can be displayed with the help of a function like this:

function arrayToString(dataArray) {
    return String.fromCharCode.apply(null, dataArray);
}
// **********************************
// Decoded Data
//   receive the decoded data from
//   a specific device
//  e = {
//    id: CaptureEventIds.DecodedData,
//    type: CaptureEventTypes.DecodedData,
//    value: {
//      data: [55, 97, 100, 57, 53, 100, 97, 98, 48, 102, 102, 99, 52, 53, 57, 48, 97,
//             52, 57, 54, 49, 97, 51, 49, 57, 50, 99, 49, 102, 51, 53, 55],
//      id: CaptureDataSourceID.SymbologyQRCode,
//      name: "QR Code"
//    }
//  }
// **********************************
case CaptureEventIds.DecodedData:
  let devWithInfo = stateRef.current.devices.find(
    (d: CaptureDeviceInfo) => {
      return d.handle === handle;
    },
  );

  if (devWithInfo) {
    setStatus('Decoded Data from ' + devWithInfo.name);
    if (
      SocketCamTypes.indexOf(devWithInfo.type) > -1 &&
      !stateRef.current.isContinuousTrigger &&
      !stateRef.current.os
    ) {
      CaptureSdk.dismissViewController();
    }
  } else {
    setStatus('Decoded Data!');
  }
  setDecodedDataList((prevList) => {
    const newDecodedData = {...lastDecodedData};
    newDecodedData.id = prevList.length + 1;
    return [newDecodedData, ...prevList];
  });
  lastDecodedData = {
    data: arrayToString(e.value.data),
    length: e.value.data.length,
    name: e.value.name,
    id: -1, //number placeholder
  };
  setDecodedData(lastDecodedData);
  break;

  // OTHER EVENT CASES

Enable or Disable symbology

In the Socket Mobile CaptureSDK, the barcode symbology is referred as Data Source. Retrieving, enabling and disabling data sources allows your application to control which barcode types the scanner is able to read. This can be useful to enable a data source that is not enabled by default or to prevent the user from scanning an unwanted barcode.

// To see all symbologies supported, control/command click on CaptureDataSourceID
// To see all flags supported, control/command click on CaptureDataSourceFlags
// To see all status types supported, control/command click on CaptureDataSourceStatus

const getSymbologyStatus = async () => {
  let property = new CaptureProperty(
    CapturePropertyIds.DataSourceDevice,
    CapturePropertyTypes.DataSource,
    {
      id: CaptureDataSourceID.SymbologyEan13,
      flags: CaptureDataSourceFlags.Status,
    },
  );
  try {
    let data = await deviceCapture?.getProperty(property);
    myLogger.log(JSON.stringify(data));
    setStatus('successfully retrieved symbology!');
  } catch (res: any) {
    let {code, message} = res.error;
    myLogger.error(`${code} : ${message}`);
    setStatus(`failed to retrieve symbology: ${code} : ${message}`);
  }
};

const setSymbology = async () => {
  let property = new CaptureProperty(
    CapturePropertyIds.DataSourceDevice,
    CapturePropertyTypes.DataSource,
    {
      id: CaptureDataSourceID.SymbologyEan13,
      flags: CaptureDataSourceFlags.Status,
      status: CaptureDataSourceStatus.Enable, // use CaptureDataSourceStatus.Disable to disable it
    },
  );

  try {
    let data = await deviceCapture?.setProperty(property);
    myLogger.log(JSON.stringify(data));
    setStatus('successfully set symbology!');
  } catch (res: any) {
    let {code, message} = res.error;
    myLogger.error(`${code} : ${message}`);
    setStatus(`failed to set symbology: ${code} : ${message}`);
  }
};

Using useRef React Hook You might encounter memory or state reference issues in your React Native app when trying to access them within the onCaptureEvent callback that is passed to the CaptureRn instance. This is likely because onCaptureEvent is not actually invoked by the component itself but rather as a side effect/listener that is operating in the context of the CaptureRn instance. This leads to complex data structures in state, such as arrays and objects, not being reliably accessible.

A workaround to this is to use the useRef hook. This will allow you to store various state values in an actual reference value related to the component. This reference can then be accessed in the context that onCaptureEvent is operating in. You can add by including it in the general import at the top of your component.

import React, {useState, useEffect, useRef} from 'react';

Then you can use it after you declare your React Hook state values.

const App = () => {
  const [devices, setDevices] = useState([]);

  const stateRef = useRef();

  stateRef.current = devices;

  const onCaptureEvent = (e, handle) => {
    if (!e) {
      return;
    }

    myLogger.log(`onCaptureEvent from ${handle}: `, e);
    let devs = stateRef.current.devices; // HERE is we can check a reliable and up to date list of devices.
    switch (e.id) {
      ...
    }

    ...

};

Issues with Hot Reload for iOS

Right now there is an issue when using an iOS device where the hot reload provided by react doesn’t lead to the full chain of events of the previous app state. For example, if you opened capture, then connected a device, if you were to save your code or reload your app entirely, then the only event picked up on the device will be the open capture.

We are currently working to resolve this. In the meantime, you will need to disconnect and reconnect your scanner (including SocketCam). This might result in an error, especially after a full reload, in the sample app saying no matching devices found for DEVICE_NAME. This is normal because the device arrival wasn’t triggered so while the device is still connected in the background, the UI doesn’t register it.

So when you disconnect it, a remove device event is triggered and picked up by the UI, but the function that finds and removes the device from the device list will throw an error saying the device couldn’t be found. At the moment this is normal. Just turn on your device or re-enable SocketCam and the new device arrival will be detected.

While this work around is not the most convenient for development, it has no effect on production. Android development and production are also not affected.