cordova-plugin-qrscanner: Cant get the camera to open in Ionic 4

Hi all,

I am trying to use this QR scanner with an Ionic 4 project, but just cant seem to get it to open the camera. I have implemented the example as per the Ionic 4 docs (https://beta.ionicframework.com/docs/native/qr-scanner/) as here:

...
import {QRScanner, QRScannerStatus} from '@ionic-native/qr-scanner/ngx';
...

export class CardPage {

    ...

    payment() {
        console.log('Scan start');

        this.qrScanner.getStatus().then((status) => {
            console.log(status)
        });

        this.qrScanner.prepare()
            .then((status: QRScannerStatus) => {
                console.log('Scan status', status);
                if (status.authorized) {
                    // camera permission was granted


                    // start scanning
                    let scanSub = this.qrScanner.scan().subscribe((text: string) => {
                        console.log('Scanned something', text);

                        this.qrScanner.hide(); // hide camera preview
                        scanSub.unsubscribe(); // stop scanning
                    });

                    this.showCamera();

                    this.qrScanner.resumePreview();

                    // show camera preview
                    this.qrScanner.show()
                        .then((data: QRScannerStatus) => {
                            console.log('datashowing', data.showing);
                        }, err => {
                            console.log('show error', err);
                        });

                } else if (status.denied) {
                    // camera permission was permanently denied
                    // you must use QRScanner.openSettings() method to guide the user to the settings page
                    // then they can grant the permission from there
                } else {
                    // permission was denied, but not permanently. You can ask for permission again at a later time.
                }
            })
            .catch((e: any) => console.log('Scan error is', e));
    }

    showCamera() {
        (window.document.querySelector('ion-app') as HTMLElement).classList.add('cameraView');
    }

    hideCamera() {
        (window.document.querySelector('ion-app') as HTMLElement).classList.remove('cameraView');
        let content = <HTMLElement>document.getElementsByTagName("body")[0];
        content.style.background = "white !important";
    }

...

}

Which when clicking the button that fires payment() does initial give me the prompt to allow permissions for the camera, but after pressing yes nothing happens. The console logs are firing, even the one after the show() promise of console.log('datashowing', data.showing); so the function is working, but the camera just doesn’t show up

I found this issue (https://github.com/bitpay/cordova-plugin-qrscanner/issues/156#issuecomment-387002328) which suggests that we need to make the ion-app element have a transparent background which I have implemented in the above code and by using inspector I can see that its being applied, but that doesn’t help.

Does anyone know what is wrong?

This is happening in both Android emulator (API 26) and iOS simulator (iOS 11)

I realise this is not specifically an Ionic git repo but the Ionic docs point here.

Ionic info below

Ionic:

ionic (Ionic CLI) : 4.2.1 Ionic Framework : @ionic/angular 4.0.0-beta.12 @angular-devkit/build-angular : 0.7.5 @angular-devkit/schematics : 0.7.5 @angular/cli : 6.1.5 @ionic/angular-toolkit : 1.0.0

Cordova:

cordova (Cordova CLI) : 8.1.1 (cordova-lib@8.1.0) Cordova Platforms : android 7.1.1, browser 5.0.4 Cordova Plugins : cordova-plugin-ionic-keyboard 2.1.3, cordova-plugin-ionic-webview 2.2.0, (and 6 other plugins)

About this issue

Most upvoted comments

So I’ve sort of solved this. It’s a bit of a hacky solution, but it works for me.

Instead of hiding the app entirely, I added a class to the html element:

const startScanner = () => {
            // Show scanner 

            const rootElement = <HTMLElement>document.getElementsByTagName('html')[0];
            rootElement.classList.add('qr-scanner-open');
});

const closeScanner = () => {
            // Hide and unsubscribe from scanner

            const rootElement = <HTMLElement>document.getElementsByTagName('html')[0];
            rootElement.classList.remove('qr-scanner-open');
});

In my HTML, I added a Close scanner button just inside the ion-content

<ion-header>
    <!-- ... -->
</ion-header>

<ion-content >
    <div id="close-scanner-button" text-center>
        <ion-button (click)="closeScanner()" color="warning">
            Close Scanner
        </ion-button>
    </div>
    
    <!-- Other content -->
</ion-content>

<ion-footer>
    <!-- ... -->
</ion-footer>

Finally, in my CSS, I hid everything on the page besides the scanner button, and changed the height of the application to be smaller and only be large enough to show the close scanner button:

#close-scanner-button {
    display: none;
}

html.qr-scanner-open {

    ion-app,
    ion-content {
        height: 70px !important;
    }

    ion-content {
        --overflow: hidden !important;
    }

    ion-header,
    ion-footer,
    ion-content > *:not(#close-scanner-button) {
        display: none;
    }

    #close-scanner-button {
        display: block;
    }
}

The result is something like this:

img_d02b4af931b3-1

@onfire @OhYuuKii

This is related to shadow DOM styling, which I just heard about on ionic v4 haha, don’t know if this is the best solution but it works.

Solution 1 - Recommended

You can put this css on your component’s scss.

ion-content {
  --background: none transparent;
}

Solution 2

    showCamera() {
        setTimeout(() => {
            window.document.querySelectorAll('ion-content')
                  .forEach(element => {
                      const element1 = element.shadowRoot.querySelector('style');
                      element1.innerHTML = element1.innerHTML
                                                   .replace('--background:var(--ion-background-color,#fff);', '--background: transparent');
                  });
        }, 300);
    }

    hideCamera() {
        window.document.querySelectorAll('ion-content')
              .forEach(element => {
                  const element1 = element.shadowRoot.querySelector('style');
                  element1.innerHTML = element1.innerHTML
                                               .replace('--background: transparent', '--background:var(--ion-background-color,#fff);');
              });
    }

Here is my solution using angular. Basically, I have a flag on ion-content to turn on and off a css class:

<ion-content [class.show-qr-scanner]="isOn">
       <-- other content here -->
</ion-content>
.show-qr-scanner {
  display: none;
}
import { Component, OnInit } from '@angular/core';

import { QRScanner, QRScannerStatus } from '@ionic-native/qr-scanner/ngx';

@Component({
  selector: 'app-qr-scanner',
  templateUrl: './qr-scanner.page.html',
  styleUrls: ['./qr-scanner.page.scss'],
})
export class QrScannerPage implements OnInit {

  isOn = false;
  scannedData: {};

  constructor(
    private qrScanner: QRScanner
  ) {
  }

  ngOnInit() {
  }

  startScanner() {

    this.qrScanner.prepare()
      .then((status: QRScannerStatus) => {
        if (status.authorized) {

          this.isOn = true;

          // start scanning
          const scanSub = this.qrScanner.scan().subscribe((text: string) => {
            console.log('Scanned something', text);

            this.isOn = false;
            this.qrScanner.hide().then();
            scanSub.unsubscribe();
          });

          this.qrScanner.show().then();


        } else if (status.denied) {
          // camera permission was permanently denied
          // you must use QRScanner.openSettings() method to guide the user to the settings page
          // then they can grant the permission from there
          this.qrScanner.openSettings();
        } else {
          // permission was denied, but not permanently. You can ask for permission again at a later time.
        }
      })
      .catch((e: any) => console.log('Error is', e));
  }

}

I found a quite nice solution too. You can use Angulars [class.hide]="scannerOpen" to attach a class with the background CSS var if you need it and hide elements based on that variable too.

.hide {
  --background: none transparent;
}

Template:

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Open</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true" [class.hide]="scannerShown">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Open box</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container" *ngIf="!scannerShown">
    <p><strong class="capitalize">Open box</strong></p>
    <ion-button (click)="scan()">
      <ion-icon name="qr-code" slot="start"></ion-icon>
      Scan QR code
    </ion-button>

    <!-- Test UI-->
    <br>
    <br>
    <p *ngIf="data">
     Scanned data is:
    </p>
    {{data}}
  </div>
</ion-content>

Page

import { Component, OnInit, ViewChild } from '@angular/core';
import { QRScanner, QRScannerStatus } from '@ionic-native/qr-scanner/ngx';
import { IonContent } from '@ionic/angular';

@Component({
  selector: 'app-login',
  templateUrl: './open.page.html',
  styleUrls: ['./open.page.scss'],
})
export class OpenPage {
  public data: string;
  public scannerShown: boolean;

  constructor(private qrScanner: QRScanner) { }


  scan() {
    // Optionally request the permission early
    this.qrScanner.prepare()
      .then((status: QRScannerStatus) => {
        console.debug(status);
        if (status.authorized) {
          // camera permission was granted


          // start scanning
          const scanSub = this.qrScanner.scan().subscribe((text: string) => {
            console.log('Scanned something', text);
            this.data = text;

            this.qrScanner.hide(); // hide camera preview
            this.scannerShown = false;
            scanSub.unsubscribe(); // stop scanning
          });
          this.qrScanner.show().then((showStatus) => console.debug('show status', showStatus)).catch((e) => console.error('show error', e));
          this.scannerShown = true;

        } else if (status.denied) {
          // camera permission was permanently denied
          // you must use QRScanner.openSettings() method to guide the user to the settings page
          // then they can grant the permission from there
        } else {
          // permission was denied, but not permanently. You can ask for permission again at a later time.
        }
      })
      .catch((e: any) => console.log('Error is', e));

  }

}

This is related to shadow DOM styling, which I just heard about on ionic v4 haha, don’t know if this is the best solution but it works.

Solution 1 - Recommended

You can put this css on your component’s scss.

ion-content {
  --background: none transparent;
}

Solution 2

    showCamera() {
        setTimeout(() => {
            window.document.querySelectorAll('ion-content')
                  .forEach(element => {
                      const element1 = element.shadowRoot.querySelector('style');
                      element1.innerHTML = element1.innerHTML
                                                   .replace('--background:var(--ion-background-color,#fff);', '--background: transparent');
                  });
        }, 300);
    }

    hideCamera() {
        window.document.querySelectorAll('ion-content')
              .forEach(element => {
                  const element1 = element.shadowRoot.querySelector('style');
                  element1.innerHTML = element1.innerHTML
                                               .replace('--background: transparent', '--background:var(--ion-background-color,#fff);');
              });
    }

it works 😃

What CSS do you have in the .cameraView class?

The way I had to do it was to set the display to none.

const ionApp = <HTMLElement>document.getElementsByTagName('ion-app')[0];

const startScanner = () => {
            scanSub = this.qrScanner.scan().subscribe((text: string) => {
                console.log(text);
            });

            this.qrScanner.show();
            ionApp.style.display = 'none';
});

const closeScanner = () => {
            this.qrScanner.hide();
            scanSub.unsubscribe();
            ionApp.style.display = 'block';
});