Skip to content

Instantly share code, notes, and snippets.

@thomasmassmann
Last active January 12, 2025 09:11
Show Gist options
  • Save thomasmassmann/07a687e6e16d39ea6a3bd0694013856a to your computer and use it in GitHub Desktop.
Save thomasmassmann/07a687e6e16d39ea6a3bd0694013856a to your computer and use it in GitHub Desktop.
capacitor-barcode-scanner web patch
/**
* Patch for the `@capacitor/barcode-scanner` plugin for web.
* This is related to
* https://github.com/ionic-team/capacitor-barcode-scanner/issues/50
*
* This patch registers a new plugin for Capacitor that provides a fixed
* web implementation of the barcode scanner.
*
* The plugin is named `CapacitorBarcodeScannerPatchWeb` and only contains
* the web version. For iOS and Android, the original plugin needs to be used.
*/
import type {
CapacitorBarcodeScannerOptions,
CapacitorBarcodeScannerPlugin,
CapacitorBarcodeScannerScanResult,
} from "@capacitor/barcode-scanner";
import {
CapacitorBarcodeScannerCameraDirection,
CapacitorBarcodeScannerScanOrientation,
} from "@capacitor/barcode-scanner";
import { registerPlugin } from "@capacitor/core";
import {
applyCss,
barcodeScannerCss,
} from "@/patches/capacitor-barcode-scanner/utils";
/**
* Registers the `OSBarcode` plugin with Capacitor.
* For web platforms, it applies necessary CSS for the barcode scanner and dynamically imports the web implementation.
* This allows for lazy loading of the web code only when needed, optimizing overall bundle size.
*/
const CapacitorBarcodeScannerImpl =
registerPlugin<CapacitorBarcodeScannerPlugin>(
"CapacitorBarcodeScannerPatchWeb",
{
web: () => {
applyCss(barcodeScannerCss); // Apply the CSS styles necessary for the web implementation of the barcode scanner.
return import("./web").then((m) => new m.CapacitorBarcodeScannerWeb()); // Dynamically import the web implementation and instantiate it.
},
}
);
class CapacitorBarcodeScannerPatchWeb {
public static async scanBarcode(
options: CapacitorBarcodeScannerOptions
): Promise<CapacitorBarcodeScannerScanResult> {
// Ensure scanInstructions is at least a space.
options.scanInstructions = options.scanInstructions || " ";
// Set scanButton to false if not provided.
options.scanButton = options.scanButton || false;
// Ensure scanText is at least a space.
options.scanText = options.scanText || " ";
// Set cameraDirection to 'BACK' if not provided.
options.cameraDirection =
options.cameraDirection || CapacitorBarcodeScannerCameraDirection.BACK;
options.scanOrientation =
options.scanOrientation ||
// Set scanOrientation to 'ADAPTIVE' if not provided.
CapacitorBarcodeScannerScanOrientation.ADAPTIVE;
return CapacitorBarcodeScannerImpl.scanBarcode(options);
}
}
export { CapacitorBarcodeScannerPatchWeb };
import { Device } from "@capacitor/device";
import {
CapacitorBarcodeScanner,
CapacitorBarcodeScannerTypeHint,
} from "@capacitor/barcode-scanner";
/**
* Use a patched version of the CapacitorBarcodeScanner for the web until a fix
* is available for https://github.com/ionic-team/capacitor-barcode-scanner/issues/50.
*/
import { CapacitorBarcodeScannerPatchWeb } from "@/patches/capacitor-barcode-scanner";
import { onMounted, ref } from "vue";
const isPlatformWeb = ref(true);
// Code is copied from a utils module
const isWeb = async () => {
const deviceInfo = await Device.getInfo();
const platform = deviceInfo.platform;
return platform === "web";
}
onMounted(async () => {
// Check for the web platform, so we can use the patched version of the barcode
// scanner, if required.
isPlatformWeb.value = await isWeb();
});
const handleScanQRCode = async () => {
// The default scanner class for ios and android.
let scannerClass = CapacitorBarcodeScanner;
if (isPlatformWeb.value) {
// Patch the web implementation of the barcode scanner.
// Can be removed once the issue is fixed in the capacitor plugin.
scannerClass = CapacitorBarcodeScannerPatchWeb;
}
// Your code to scan...
};
/**
* This file is added only for the path, but taken as-is from the original source.
* We need to add this, since @capacitor/barcode-scanner does not export those functions in the dist.
*/
/**
* Predefined CSS rules for styling barcode scanner components.
* Each object in the array defines a CSS rule, with a selector and the CSS properties to apply.
*/
export const barcodeScannerCss = [
{ selector: ".scanner-container-display", css: "display: block;" },
{
selector: ".scanner-dialog",
css: "display: none; position: fixed; z-index: 999; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);",
},
{
selector: ".scanner-dialog-inner",
css: "background-color: #fefefe; margin: 2% auto; padding: 20px; border: 1px solid #888; width: 96%;",
},
{
selector: ".close-button",
css: "color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer;",
},
{ selector: ".close-button:hover", css: "color: #222;" },
{ selector: ".scanner-container-full-width", css: "width: 100%;" },
];
/**
* Dynamically applies a set of CSS rules to the document.
* If a custom style element with a specific ID does not exist, it is created and appended to the document's head.
* Existing rules in the style element are cleared before new ones are applied.
* This function supports both modern and older browsers by using `CSSStyleSheet.insertRule` and `textContent` respectively.
*
* @param {Array<{selector: string, css: string}>} cssRules - An array of objects containing CSS selectors and styles to apply.
*/
export function applyCss(cssRules: { selector: string; css: string }[]): void {
const styleId = "custom-style-os-cap-barcode"; // Unique identifier for the style element.
let styleElement = document.getElementById(
styleId
) as HTMLStyleElement | null;
if (!styleElement) {
// Create and append a new style element if it does not exist.
styleElement = document.createElement("style");
styleElement.type = "text/css";
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
if (styleElement.sheet) {
// Clear existing CSS rules.
while (styleElement.sheet.cssRules.length) {
styleElement.sheet.deleteRule(0);
}
// Insert new CSS rules.
for (const { selector, css } of cssRules) {
styleElement.sheet.insertRule(`${selector} { ${css} }`);
}
} else {
// Fallback for older browsers using textContent.
styleElement.textContent = "";
for (const { selector, css } of cssRules) {
styleElement.textContent += `${selector} { ${css} }`;
}
}
}
/**
* This file contains the actual patch for the web implementation of the barcode scanner.
*
* It fixes the error check in the HTML5 QR code scanner.
*/
import type { Html5QrcodeResult } from "html5-qrcode";
import { WebPlugin } from "@capacitor/core";
import { Html5Qrcode } from "html5-qrcode";
import type {
CapacitorBarcodeScannerPlugin,
CapacitorBarcodeScannerOptions,
CapacitorBarcodeScannerScanResult,
} from "@capacitor/barcode-scanner";
import { CapacitorBarcodeScannerScanOrientation } from "@capacitor/barcode-scanner";
/**
* Implements OSBarcodePlugin to provide web functionality for barcode scanning.
*/
export class CapacitorBarcodeScannerWeb
extends WebPlugin
implements CapacitorBarcodeScannerPlugin
{
/**
* Stops the barcode scanner and hides its UI.
* @private
* @returns {Promise<void>} A promise that resolves when the scanner has stopped and its UI is hidden.
*/
private async stopAndHideScanner(): Promise<void> {
console.log((window as any).OSBarcodeWebScanner);
if ((window as any).OSBarcodeWebScanner) {
await (window as any).OSBarcodeWebScanner.stop();
(window as any).OSBarcodeWebScanner = null;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
document.getElementById(
"cap-os-barcode-scanner-container-dialog"
)!.style.display = "none";
}
/**
* Builds the HTML elements necessary for the barcode scanner UI.
* This method checks if the scanner container exists before creating it to avoid duplicates.
* It also sets up the close button to stop and hide the scanner on click.
* @private
*/
private buildScannerElement(): void {
if (document.getElementById("cap-os-barcode-scanner-container")) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
document.getElementById("cap-os-barcode-scanner-container")!.className =
"scanner-container-display";
return;
}
// Create and configure scanner container elements
const caposbarcodescannercontainer = document.body.appendChild(
document.createElement("div")
);
caposbarcodescannercontainer.id = "cap-os-barcode-scanner-container";
const caposbarcodescannercontainerdialog = document.createElement("div");
caposbarcodescannercontainerdialog.id =
"cap-os-barcode-scanner-container-dialog";
caposbarcodescannercontainerdialog.className = "scanner-dialog";
// Inner dialog elements including the close button and scanner view
const caposbarcodescannercontainerdialoginner =
document.createElement("div");
caposbarcodescannercontainerdialoginner.className = "scanner-dialog-inner";
const caposbarcodescannercontainerdialoginnerclose =
document.createElement("span");
caposbarcodescannercontainerdialoginnerclose.className = "close-button";
caposbarcodescannercontainerdialoginnerclose.innerHTML = "&times;";
caposbarcodescannercontainerdialoginner.appendChild(
caposbarcodescannercontainerdialoginnerclose
);
const caposbarcodescannercontainerdialoginnercontainerp =
document.createElement("p");
caposbarcodescannercontainerdialoginnercontainerp.innerHTML = "&nbsp;";
caposbarcodescannercontainerdialoginner.appendChild(
caposbarcodescannercontainerdialoginnercontainerp
);
const caposbarcodescannercontainerdialoginnercontainer =
document.createElement("div");
caposbarcodescannercontainerdialoginnercontainer.className =
"scanner-container-full-width";
caposbarcodescannercontainerdialoginnercontainer.id =
"cap-os-barcode-scanner-container-scanner";
caposbarcodescannercontainerdialoginner.appendChild(
caposbarcodescannercontainerdialoginnercontainer
);
caposbarcodescannercontainerdialog.appendChild(
caposbarcodescannercontainerdialoginner
);
caposbarcodescannercontainer.appendChild(
caposbarcodescannercontainerdialog
);
caposbarcodescannercontainerdialoginnerclose.onclick =
this.stopAndHideScanner;
}
/**
* Initiates a barcode scan using the user's camera and HTML5 QR code scanner.
* Displays the scanner UI and waits for a scan to complete or fail.
* @param {OSBarcodeScanOptions} options Configuration options for the scan, including camera direction and UI preferences.
* @returns {Promise<OSBarcodeScanResult>} A promise that resolves with the scan result or rejects with an error.
*/
async scanBarcode(
options: CapacitorBarcodeScannerOptions
): Promise<CapacitorBarcodeScannerScanResult> {
this.buildScannerElement();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
document.getElementById(
"cap-os-barcode-scanner-container-dialog"
)!.style.display = "block";
return new Promise((resolve, reject) => {
const param = {
facingMode: options.cameraDirection === 1 ? "environment" : "user",
hasScannerButton: false,
scanButton:
options.scanButton === undefined ? false : options.scanButton,
showScanLine: false,
scanInstructions: options.scanInstructions
? options.scanInstructions
: "",
orientation: options.scanOrientation
? options.scanOrientation
: CapacitorBarcodeScannerScanOrientation.PORTRAIT,
showCameraSelection: options.web?.showCameraSelection
? options.web.showCameraSelection
: false,
typeHint: options.hint === 17 ? undefined : options.hint,
scannerFPS: options.web?.scannerFPS ? options.web.scannerFPS : 50,
};
// Set up and start the scanner
const scannerElement = document.getElementById(
"cap-os-barcode-scanner-container-scanner"
);
if (!scannerElement) {
throw new Error("Scanner Element is required for web");
}
(window as any).OSBarcodeWebScanner = new Html5Qrcode(scannerElement.id);
const Html5QrcodeConfig = {
fps: param.scannerFPS,
qrbox: scannerElement.getBoundingClientRect().width * (9 / 16) - 10,
aspectRatio: 16 / 9,
videoConstraints: {
focusMode: "continuous",
height: { min: 576, ideal: 1920 },
deviceId: undefined,
facingMode: undefined,
},
};
// Success and error callbacks for the scanner
const OSBarcodeWebScannerSuccessCallback = (
decodedText: string,
_decodedResult: Html5QrcodeResult
) => {
this.stopAndHideScanner();
resolve({ ScanResult: decodedText });
};
const OSBarcodeWebScannerErrorCallback = (error: string) => {
if (
/**
* ===========================================================
* START OF PATCH
* ===========================================================
*/
error.indexOf("No MultiFormat") === -1 &&
error.indexOf("No barcode or QR code detected") === -1
/**
* ===========================================================
* END OF PATCH
* ===========================================================
*/
) {
this.stopAndHideScanner();
console.error(`[Scanner Web Error] ${error}`);
reject(error);
}
};
(window as any).OSBarcodeWebScanner.start(
{ facingMode: param.facingMode },
Html5QrcodeConfig,
OSBarcodeWebScannerSuccessCallback,
OSBarcodeWebScannerErrorCallback
);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment