How to Build a Quasar QR Code Scanner with Capacitor
Tags: quasar, javascript, webdev, vue
Published: 2022-09-01 07:26:37


Quasar is a Vue.js-based framework, which allows web developers to quickly create responsive websites/apps, while Capacitor is a runtime for creating cross-platform iOS, Android, and Progressive Web Apps with JavaScript, HTML, and CSS.

In this article, we are going to use the two to create a QR code scanner using the [capacitor plugin](https://github.com/tony-xlh/capacitor-plugin-dynamsoft-barcode-reader) of [Dynamsoft Barcode Reader](https://www.dynamsoft.com/barcode-reader/overview/). The app can run as Web, Android and iOS apps.

[Online demo](https://tiny-haupia-869a3e.netlify.app/).

Check out the [Ionic Vue](https://www.dynamsoft.com/codepool/ionic-vue-qr-code-scanner.html) article, if you prefer to use the Ionic framework.

## Build a Quasar QR Code Scanner using Capacitor

### New Project

Install quasar cli:

```
npm install -g @quasar/cli
``` 

Then, use it to create a new project:

```
npm init quasar
```

Because it has to run in Android's webview, for compatibility concerns, we choose Quasar v1 + Vue.js 2 + Composition API to build this app.

```
√ What would you like to build? » App with Quasar CLI, let's go!
√ Project folder: ... quasar-qrcode-scanner
√ Pick Quasar version: » Quasar v1 (Vue 2)
√ Pick script type: » Typescript
√ Package name: ... quasar-qrcode-scanner
√ Project product name: (must start with letter if building mobile apps) ... Quasar QR Code Scanner
√ Project description: ... A Quasar Project
√ Author: ...
√ Pick a Vue component style: » Composition API (recommended) (https://github.com/vuejs/composition-api)
√ Pick your CSS preprocessor: » Sass with SCSS syntax
√ Pick a Quasar components & directives import strategy: (can be changed later) » * Auto-import in-use Quasar components & directives
        - also treeshakes Quasar; minimum bundle size
√ Check the features needed for your project: » ESLint
√ Pick an ESLint preset: » Prettier
```

### Install Dependencies

```
npm install capacitor-plugin-dynamsoft-barcode-reader
```

### Modify the Default Files

1. Remove the example components under `src/components`.
2. Open `src/layouts/MainLayout.vue`, simplify its template with the following content:

   ```html
   <template>
     <q-layout view="lHh Lpr lFf">
       <q-header elevated >
         <q-toolbar>
           <q-toolbar-title>
             QR Code Scanner
           </q-toolbar-title>
         </q-toolbar>
       </q-header>
       <q-page-container>
         <router-view />
       </q-page-container>
     </q-layout>
   </template>
   ```
   
3. Open `src/pages/Index.vue`, add a floating action button to start the scanner page to scan QR codes and a QList to display the results. Users can clikc the item to copy the result.

   ```html
   <template>
     <q-page class="row justify-evenly">
       <div class="full">
         <q-list v-if="results.barcodeResults.value.length>0" dense bordered separator padding class="rounded-borders">
           <q-item @click="copy(result.barcodeText)" clickable v-ripple v-for="(result, index) in results.barcodeResults.value" :key="index">
             <q-item-section>
               <q-item-label :lines="1">{{ result.barcodeText }}</q-item-label>
               <q-item-label caption>{{ result.barcodeFormat }}</q-item-label>
             </q-item-section>
           </q-item>
         </q-list>
         <q-page-sticky position="bottom-left" :offset="[18,18]">
           <q-btn @click="goScan" fab icon="camera_alt" color="blue" />
         </q-page-sticky>
       </div>
     </q-page>
   </template>
   ```
   
   The `goScan` and `copy` functions:
   
   ```js
   import { Clipboard } from '@capacitor/clipboard';
   import { Notify } from 'quasar';
   const goScan = async () => {
     await router.push('/scanner');
   }

   const copy = async (text:string) => {
     await Clipboard.write({
       string: text
     });
     Notify.create({
       message: 'Copied.'
     });
   }
   ```
   
4. Use vue-router with the composition API to navigate to another page.

   The dependent vue-router of the current version quasar does not have `useRoute` and `useRouter` to use in Vue 2.7 with the composition API. We can use the following workaround ([GitHub issue](https://github.com/vuejs/router/issues/1462)).
   
   
   1. Create a file under `utils/index.js`.
      
      ```js
      import { getCurrentInstance } from 'vue'

      export function useRoute() {
        const { proxy } = getCurrentInstance()
        const route = proxy.$route
        return route
      }
      export function useRouter() {
        const { proxy } = getCurrentInstance()
        const router = proxy.$router
        return router
      }
      ```
      
   2. In the index page, use it to navigate to the scanner page.

      ```js
      import { useRouter } from '../utils/index.js'
      export default defineComponent({
        name: 'PageIndex',
        components: {},
        setup() {
          const router = useRouter();
          const goScan = async () => {
            await router.push('/scanner');
          }
          return {goScan};
        }
      });
      ```
   

Screenshot of the index page:

![Index](https://www.dynamsoft.com/codepool/img/2022/09/quasar/quasar-qr-code-scanner-index.png)

### Add a Scanner Page

1. Create a new file named `Scanner.vue` under `src\pages`.
2. Open `src/router/routes.ts` to register the route for the scanner page.

   ```js
   {
     path: '/scanner',
     component: () => import('pages/Scanner.vue')
   },
   ```
   
3. Add a QR Code Scanner component. We've defined [the component](https://www.dynamsoft.com/codepool/ionic-vue-qr-code-scanner.html#create-a-qr-code-scanner-component) in the previous Ionic Vue article. Although that project is in Vue 3, we can still directly use the component in a Vue 2 project with the composition api enabled. We are going to put the vue file under `src/components/QRCodeScanner.vue`.

4. Use the QR Code Scanner component in the scanner page.

   ```html
   <template>
     <q-layout view="lHh Lpr lFf">
       <QRCodeScanner
         license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="
         :torchOn="torchOn"
         :zoomFactor="zoomFactor"
         :runtimeSettings="runtimeSettings"
         @onScanned="onScanned"
         @onPlayed="onPlayed"
       ></QRCodeScanner>
     </q-layout>
   </template>
   <script lang="ts">
   
   export default defineComponent({
     name: 'PageScanner',
     components: { QRCodeScanner },
     setup() {
       const scanned = ref(false);
       const zoomFactor = ref(1.0);
       const torchOn = ref(false);
       const runtimeSettings = ref('');
       const router = useRouter();
     
       const onPlayed = (resolution:string) => {
         console.log(resolution);
       }
       const onScanned = (result:ScanResult) => {
         console.log(result);
       };

       onMounted(() => {
         zoomFactor.value = 1.0;
         App.addListener("backButton",() => {
           router.go(-1);
         });
       });

       return {onPlayed, onScanned, goBack, zoomFactor, runtimeSettings};
     }});
    </script>
   ```
   
5. Add a float action button for cancelling the scanner and controlling the torch and zoom status.

   Template:
   
   ```html
   <q-page-sticky style="z-index:1;" position="bottom-left" :offset="[18,18]">
     <q-fab color="blue" icon="keyboard_arrow_up" direction="up">
       <q-fab-action @click="toggleTorch" color="blue" icon="flashlight_on" />
       <q-fab-action @click="zoomIn" color="blue" icon="add" />
       <q-fab-action @click="zoomOut" color="blue" icon="remove" />
       <q-fab-action @click="goBack" color="blue" icon="arrow_back" />
     </q-fab>
   </q-page-sticky>
   ```
   
   Script:
   
   ```js
   const zoomFactor = ref(1.0);
   const torchOn = ref(false);
   const toggleTorch = () => {
      torchOn.value = ! torchOn.value;
    };

   const zoomIn = () => {
     zoomFactor.value = zoomFactor.value + 0.3;
   };

   const zoomOut = () => {
     zoomFactor.value = Math.max(zoomFactor.value - 0.3, 1.0);
   };

   const goBack = () => {
     update([]);
     router.go(-1);
   }
   ```
   
Screenshot of the scanner page:

![Index](https://www.dynamsoft.com/codepool/img/2022/09/quasar/quasar-qr-code-scanner.jpg)
   
### Create a Store for Passing the QR Code Results to the Index Page

We can create a store to share data between different pages using Vuex.

1. Create a new Vuex store named barcodes.

   ```
   quasar new store barcodes
   ```
   
   It will add the following files to the project:
   
   ```
   .
   └── src/
       └── store/
           ├── index.js         # Vuex Store definition
           └── barcodes         # Module "barcodes"
               ├── index.js     # Gluing the module together
               ├── actions.js   # Module actions
               ├── getters.js   # Module getters
               ├── mutations.js # Module mutations
               └── state.js     # Module state
   ```
   
2. Edit `src/store/index.js` to add a reference to the new module.

   ```diff
   export default function (/* { ssrContext } */) {
     const Store = new Vuex.Store({
       modules: {
   +     barcodes
       },

       strict: process.env.DEBUGGING
     })

     return Store
   }
   ```
   
3. Update `state.js` to define the states and `mutations.js` to define the setter.

   ```js
   // src/store/barcodes/mutations.js
   export function update(state, results) {
     state.barcodeResults = results;
   }

   // src/store/barcodes/state.js
   // Always use a function to return state if you use SSR
   export default function () {
     return {
       barcodeResults: []
     }
   }
   ```
   
4. In the scanner page, if QR codes are found, update the state and return to the index page.

   ```js
   import { createNamespacedHelpers } from 'vuex-composition-helpers';
   const { useMutations } = createNamespacedHelpers('barcodes');
   //...
   setup() {
     const { update } = useMutations(['update']);
     const onScanned = (result:ScanResult) => {
       if (result.results.length>0 && scanned.value == false) {
         scanned.value = true;
         update(result.results);
         router.go(-1);
       }
     };
   }
   ```
   
5. In the index page, load the results using `useState`.

   ```js
   import { createNamespacedHelpers } from 'vuex-composition-helpers';
   const { useState } = createNamespacedHelpers('barcodes');
   setup() {
     const results = useState(["barcodeResults"]);
   }
   ```

All right, we've now finished writing the page. We can run the following to test the page:

```
quasar dev
```

### Use Capacitor to Build Android and iOS Apps

We can take a step further to turn the web app into an Android app or an iOS app using Capacitor. The quasar cli has a Capacitor mode but it uses Capacitor v2. We are going to use Capacitor v4 directly.

1. Drop Capacitor to the project:

   ```
   npm install @capacitor/cli @capacitor/core
   npx cap init
   ```
   
2. Then, we can create projects for Android and iOS.

   ```
   npm install @capacitor/ios @capacitor/android
   npx cap add ios
   npx cap add android
   ```

3. Build the web assets and sync the files to the projects.

   ```
   npm run build
   npx cap sync
   ```

4. Use the following commands to start the app:

   ```
   npx cap run android // run on Android devices
   npx cap run ios // run on iOS devices
   ```



Extra steps are needed.

* For the iOS platform:

   Add camera permission in `Info.plist`.

   ```xml
   <key>NSCameraUsageDescription</key>
   <string>For barcode scanning</string>
   ```
   
* For the Android platform, we may have to handle the back button event using the `@capacitor/app` plugin:

   ```js
   import { App } from '@capacitor/app';
   App.addListener("backButton",() => {
     console.log("back pressed");
   });
   ```
   
* For both platforms, enable `viewport-fit=cover` in the `index.template.html` so that the page will not be blocked by the status bar.

   ```diff
   - <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover <% } %>">
   + <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, viewport-fit=cover">
   ```

## Source Code

<https://github.com/tony-xlh/Quasar-QR-Code-Scanner>