Writing a Wrapper to use Cordova Plugins in an Ionic App

The Ionic framework has shifted its native runtime from Cordova to Capacitor. But for compatibility concerns and the fact that there are many good Cordova plugins, we can still use Cordova plugins in an Ionic project.

We can directly use Cordova plugins with the cordova object, but there is a better way. The Awesome Cordova Plugins is a curated set of wrappers for Cordova plugins that make adding any native functionality you need to your Ionic mobile app easy. It wraps plugin callbacks in a Promise or Observable and provides a common interface.

In this article, we are going to write a wrapper for the cordova-plugin-dynamsoft-barcode-reader and use it in an Ionic React QR code scanning app.

Writing a Wrapper for the Cordova Plugin

Let’s follow this guide to create a wrapper.

Create a New Plugin Wrapper from Template

  1. clone the project and change the root to the project’s directory:

     git clone https://github.com/danielsogl/awesome-cordova-plugins/
     cd awesome-cordova-plugins
    
  2. install npm packages: npm install
  3. create a new plugin wrapper named DynamsoftBarcodeScanner:

     gulp plugin:create -n DynamsoftBarcodeScanner
    

    We can find the wrapper in src\@awesome-cordova-plugins\plugins\dynamsoft-barcode-scanner.

Implement the Plugin Wrapper

Next, we are going to implement the plugin wrapper.

First, let’s fill in the meta data of the plugin according to the guide of the comments:

@Plugin({
  pluginName: 'DynamsoftBarcodeScanner',
  plugin: 'cordova-plugin-dynamsoft-barcode-reader', // npm package name, example: cordova-plugin-camera
  pluginRef: 'cordova.plugins.DBR', // the variable reference to call the plugin, example: navigator.geolocation
  repo: 'https://github.com/xulihang/cordova-plugin-dynamsoft-barcode-reader', // the github repository URL for the plugin
  install: '', // OPTIONAL install command, in case the plugin requires variables
  installVariables: [], // OPTIONAL the plugin requires variables
  platforms: ['Android', 'iOS'] // Array of platforms supported, example: ['Android', 'iOS']
})

Then, add the plugin methods.

@Injectable()
export class BarcodeScanner extends AwesomeCordovaNativePlugin {
  /**
   * Initialize Dynamsoft Barcode Reader
   * @param license {string}
   * @return {Promise<any>} Returns a promise that resolves when the initialization is done
   */
  @Cordova({
    successIndex: 1,
    errorIndex: 2,
  })
  init(license: string): Promise<any> {
    return;
  }
  
  /**
   * start the camera to scan barcodes
   * @param dceLicense {string} License of Dynamsoft Camera Enhancer
   * @return {Observable<FrameResult>}
   */
  @Cordova({
    successIndex: 1,
    errorIndex: 2,
    observable: true,
  })
  startScanning(dceLicense?: string): Observable<FrameResult> {
    return;
  }
  
  /**
   * stop scanning
   * @return {Promise<any>} Returns a promise
   */
  @Cordova({ successIndex: 1, errorIndex: 2 })
  stopScanning(): Promise<any> {
    return;
  }
  
  /**
   * switch torch
   * @param desiredStatus {string} on or off
   * @return {Promise<any>} Returns a promise
   */
  @Cordova({ successIndex: 1, errorIndex: 2 })
  switchTorch(desiredStatus: string): Promise<any> {
    return;
  }
  
  /**
   * Set up runtime settings
   * @param settings {string} runtime settings template in JSON
   * @return {Promise<any>} Returns a promise
   */
  @Cordova({
    successIndex: 1,
    errorIndex: 2,
  })
  initRuntimeSettingsWithString(settings?: string): Promise<any> {
    return;
  }
}

Normally, the methods return a promise. The startScanning method will call its callbacks multiple times to pass barcode scanning results, so it returns an observable. Its usage is like this:

await BarcodeScanner.init("barcode reader license");
BarcodeScanner.startScanning("camera enhancer license").subscribe(result => {
  console.log(result);
});

The licenses of Dynamsoft Barcode Reader and Dynamsoft Camera Enhancer are required to use the plugin. You can apply for a trial license here.

We also need to define the returned frame reading results. The plugin returns a frame result with the frame width, frame height and the barcode results.

export interface FrameResult {
  frameWidth: number;
  frameHeight: number;
  results: BarcodeResult[];
}

export interface BarcodeResult {
  barcodeText: string;
  barcodeFormat: string;
  barcodeBytesBase64?: string;
  x1: number;
  x2: number;
  x3: number;
  x4: number;
  y1: number;
  y2: number;
  y3: number;
  y4: number;
}

Using the Wrapper to Create a QR Code Scanning App

Let’s create a QR code scanning app using Ionic React and the wrapper. The app will have two pages: a home page and a scanner page.

On the home page, users can select whether to scan continuously and whether to scan QR codes only and then start the scanner page to scan QR codes. Scanned results will be displayed and users can copy the text result.

home_page

On the scanner page, there is a floating action button. Users can use it to turn on the flashlight or stop scanning. Scanned QR codes will be highlighted.

scanner_page

Read the following for more details.

New Project

Use the Ionic cli tool to create a new project.

ionic start qr-code-scanner tabs --type=react

Add iOS and Android platforms:

ionic capacitor add ios
ionic capacitor add android

Run the app:

ionic capacitor run android 
ionic capacitor run ios

Add Camera Permission

For iOS, add the following to ios\App\App\Info.plist:

<key>NSCameraUsageDescription</key>
<string>For barcode scanning</string>

Install Dependencies

npm install cordova-plugin-dynamsoft-barcode-reader @awesome-cordova-plugins/core

We also need to install the wrapper. We can install the already published package:

npm install @awesome-cordova-plugins/dynamsoft-barcode-scanner

Or build it and install it this way:

  1. Under the directory of awesome-cordova-plugins, run npm run build.
  2. Copy the dist\@awesome-cordova-plugins\plugins\dynamsoft-barcode-scanner to the app’s node_modules\@awesome-cordova-plugins.

Create a QR Code Scanner Component

Create a new QR code scanner component named QRCodeScanner.tsx under src\components.

import { BarcodeScanner as DBR, FrameResult } from '@awesome-cordova-plugins/dynamsoft-barcode-scanner';
import { useEffect } from 'react';

const QRCodeScanner = (props: { isActive: boolean;
  torchOn?:boolean;
  runtimeSettings?:string;
  onFrameRead?: (frameResult:FrameResult) => void;
  license?:string}) => {
  useEffect(() => {
    const init = async () => {
      let license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==";
      if (props.license) {
        license = props.license;
      }
      let result = await DBR.init(license);
    }

    init();

    return () => {
      console.log("unmount");
      DBR.stopScanning();
    }
  }, []);

  useEffect(() => {
    if (props.isActive == true) {
      let license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==";
      if (props.license) {
        license = props.license;
      }
      DBR.startScanning(license).subscribe((result:FrameResult) => {
        console.log(result);
        if (props.onFrameRead) {
          props.onFrameRead(result);
        }
      });
    }else if (props.isActive == false){
      DBR.stopScanning();
    }
    
  }, [props.isActive]);

  useEffect(() => {
    if (props.torchOn == true) {
      DBR.switchTorch("on");
    }else if (props.torchOn == false){
      DBR.switchTorch("off");
    }
  }, [props.torchOn]);

  useEffect(() => {
    if (props.runtimeSettings) {
      DBR.initRuntimeSettingsWithString(props.runtimeSettings);
    }
  }, [props.runtimeSettings]);

  return (
    <div></div>
  );
};

export default QRCodeScanner;

Create a Home Page

Create Home.tsx under src\pages with the following code:

import { IonButton, IonContent, IonHeader, IonItem, IonLabel, IonList, IonListHeader, IonPage, IonTitle, IonToggle, IonToolbar, useIonToast } from '@ionic/react';
import { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router';
import './Home.css';
import { BarcodeResult } from '@awesome-cordova-plugins/dynamsoft-barcode-scanner';
import copy from 'copy-to-clipboard';

const Home = (props:RouteComponentProps) => {
  const [present, dismiss] = useIonToast();
  const [continuous, setContinuous] = useState(false);
  const [QRCodeOnly, setQRCodeOnly] = useState(false);
  const [barcodeResults, setBarcodeResults] = useState([] as BarcodeResult[]);
  
  useEffect(() => {
    const state = props.location.state as { results?: BarcodeResult[] };
    console.log(state);
    if (state) {
      if (state.results) {
        setBarcodeResults(state.results);
        props.history.replace({ state: {} });
      }
    }
  }, [props.location.state]);

  const startScan = () => {
    props.history.push("scanner",{continuous:continuous,QRCodeOnly:QRCodeOnly})
  }

  const copyBarcode = (text:string) => {
    if (copy(text)){
      present("copied",500);
    }
  }

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Home</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonButton expand='full' onClick={startScan} >Scan Barcodes</IonButton>
        <IonList>
          <IonItem>
            <IonLabel>Continuous scan</IonLabel>
            <IonToggle slot="end" checked={continuous} onIonChange={e => setContinuous(e.detail.checked)} />
          </IonItem>
          <IonItem>
            <IonLabel>Scan only QR code</IonLabel>
            <IonToggle slot="end" checked={QRCodeOnly} onIonChange={e => setQRCodeOnly(e.detail.checked)} />
          </IonItem>
          {(barcodeResults.length>0) &&
            <IonListHeader>
              <IonLabel>Results:</IonLabel>
            </IonListHeader>
          }
          {barcodeResults.map((tr,idx) => (
            <IonItem key={idx}>
              <IonLabel>{(idx+1) + ". " + tr.barcodeFormat + ": " + tr.barcodeText}</IonLabel>
               <IonLabel style={{color:"green"}} slot="end" onClick={() =>{copyBarcode(tr.barcodeText)}}>copy</IonLabel>
            </IonItem>
          ))}
        </IonList>
      </IonContent>
    </IonPage>
  );
};

export default Home;

In App.tsx, add the router for the home page.

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
      <Route path="/home" component={Home} exact={true} />
      <Route exact path="/">
        <Redirect to="/home" />
      </Route>
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
);

export default App;

Create a Scanner Page

Create Scanner.tsx under src\pages with the following template:

const Scanner = (props:RouteComponentProps) => {
  return (
    <IonPage>
      <IonContent>
      </IonContent>
    </IonPage>
  );
};

export default Scanner;

In App.tsx, add the router for the scanner page.

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
      <Route path="/home" component={Home} exact={true} />
+     <Route path="/scanner" component={Scanner} exact={true} />
      <Route exact path="/">
        <Redirect to="/home" />
      </Route>
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
);

export default App;

Next, let’s add functions to the scanner page.

Add the QR Code Scanner Component

The Cordova plugin puts the native camera preview view under the WebView so that we can overlay web controls above the camera preview. We need to set the background to transparent so that the web content will not block the camera preview.

Here are the steps to display the QR code scanner while keeping desired web controls above the camera preview.

  1. Create a function to render the desired web controls to document.body using createPortal.

     const renderToBody = () => {
       return createPortal(
         <div>
           <QRCodeScanner 
             isActive={isActive} 
             torchOn={torchOn}
             runtimeSettings={runtimeSettings}
             onFrameRead={(frameResult) => {onFrameRead(frameResult)}}
           ></QRCodeScanner>
           {renderResults()}
           <IonFab vertical="bottom" horizontal="start" slot="fixed">
             <IonFabButton>
               <IonIcon icon={ellipsisHorizontalOutline} />
             </IonFabButton>
             <IonFabList side="top">
               <IonFabButton onClick={toggleTorch}>
                 <IonIcon icon={flashlightOutline} />
               </IonFabButton>
               <IonFabButton onClick={close}>
                 <IonIcon icon={closeOutline} />
               </IonFabButton>
             </IonFabList>
           </IonFab>
         </div>,document.body
       );
     }
    
  2. Set the display attribute of the IonContent to none.

     return (
       <IonPage>
         <IonContent style={{display:"none"}}>
         {renderToBody()}
         </IonContent>
       </IonPage>
     );
    

Here are the functions controlling the behavior of the QR code scanner, like whether to start scanning, whether to turn on the torch and whether to scan QR codes only.

const [isActive, setIsActive] = useState(false);
const [torchOn, setTorchOn] = useState(false);
const [runtimeSettings,setRuntimeSettings] = useState("{\"ImageParameter\":{\"BarcodeFormatIds\":[\"BF_ALL\"],\"Description\":\"\",\"Name\":\"Settings\"},\"Version\":\"3.0\"}") // default runtime settings which read all barcode formats

const startScan = () => {
  setIsActive(true);
}

const toggleTorch = () => {
  setTorchOn(!torchOn);
}

const close = () => {
  setIsActive(false);
  props.history.goBack();
}

useEffect(() => {
  const state = props.location.state as { continuous:boolean,QRCodeOnly:boolean };
  if (state.QRCodeOnly == true) {
    setRuntimeSettings("{\"ImageParameter\":{\"BarcodeFormatIds\":[\"BF_QR_CODE\"],\"Description\":\"\",\"Name\":\"Settings\"},\"Version\":\"3.0\"}"); //modify the runtime settings to scan QR codes only
  }
  startScan(); //start scanning when the page is mounted
}, []);

Handle Frame Reading Results

We can get the frame reading results in the onFrameRead event.

If it is in continuous scanning mode, draw barcode overlays. Otherwise, close the scanner and return to the home page with the barcode results.

const onFrameRead = (frameResult:FrameResult) => {
  const state = props.location.state as { continuous:boolean,QRCodeOnly:boolean };
  if (state.continuous == false) {
    if (frameResult.results.length>0) {
      props.history.replace({ state: {results:frameResult.results} });
      close();
    }
  }else{
    setViewBox("0 0 "+frameResult.frameWidth+" "+frameResult.frameHeight);
    setBarcodeResults(frameResult.results);
  }
}

We use SVG to draw barcode and QR code overlays (check out this article to learn more):

const [viewBox, setViewBox] = useState("0 0 720 1280");

const getPointsData = (lr:BarcodeResult) => {
  let pointsData = lr.x1 + "," + lr.y1 + " ";
  pointsData = pointsData + lr.x2+ "," + lr.y2 + " ";
  pointsData = pointsData + lr.x3+ "," + lr.y3 + " ";
  pointsData = pointsData + lr.x4+ "," + lr.y4;
  return pointsData;
}

const renderResults = () => {
  return (
    <div className="overlay">
      <svg
        viewBox={viewBox}
        className="overlay"
        xmlns="<http://www.w3.org/2000/svg>"
      >
        {barcodeResults.map((tr,idx) => (
          <polygon key={"poly-"+idx} xmlns="<http://www.w3.org/2000/svg>"
          points={getPointsData(tr)}
          className="barcode-polygon"
          />
        ))}
        {barcodeResults.map((tr,idx) => (
          <text key={"text-"+idx} xmlns="<http://www.w3.org/2000/svg>"
          x={tr.x1}
          y={tr.y1}
          fill="red"
          fontSize="20"
          >{tr.barcodeText}</text>
        ))}
      </svg>
    </div>
  );
}

All right, we’ve finished writing the Ionic QR code scanner using the Cordova plugin wrapper. You can check out the source code to have a try.

Source Code