Skip to main content
With the help of this function, customers can call up products via QR code or barcode and add them to the basket. A scanner icon in the search field opens the camera in full-screen mode. If a URL is scanned, a direct redirection takes place. If, on the other hand, a barcode is scanned, the product with the scanned ID is placed directly into the basket.

Modules

The following modules are relevant for the integration of Scan & Order:
  • $wsViews - current URL, destination pages, view URLs

Frontend integration

Dependencies

The scanner is based on the open-source library html5-qrcode. The file html5-qrcode.min.js is stored in the scripts/ directory and is only included via lazy load when the scanner is opened.

QR button in the search field

The button is placed directly in front of the search field within the ws-search-box component and opens the scanner. Depending on the end device, it appears in the following locations:
  • in the desktop search bar of the header
    Image
  • in the mobile offcanvas navigation menu
    Image

The integration into the header is done via the file components/layout/header.htm:
<ws-search-box ...>
    <button type="button"
            class="btn btn-light border rounded-start"
            aria-label="%%QRCodeBtnTitle%%"
            data-bs-toggle="modal"
            data-bs-target="#wsQRCodeScanModal">
        <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" viewBox="0 0 16 16">
            <path d="M0 .5A.5.5 0 0 1 .5 0h3a.5.5 0 0 1 0 1H1v2.5a.5.5 0 0 1-1 0v-3Zm12 0a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0V1h-2.5a.5.5 0 0 1-.5-.5ZM.5 12a.5.5 0 0 1 .5.5V15h2.5a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 .5-.5Zm15 0a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1 0-1H15v-2.5a.5.5 0 0 1 .5-.5ZM4 4h1v1H4V4Z"/>
            <path d="M7 2H2v5h5V2ZM3 3h3v3H3V3Zm2 8H4v1h1v-1Z"/>
            <path d="M7 9H2v5h5V9Zm-4 1h3v3H3v-3Zm8-6h1v1h-1V4Z"/>
            <path d="M9 2h5v5H9V2Zm1 1v3h3V3h-3ZM8 8v2h1v1H8v1h2v-2h1v2h1v-1h2v-1h-3V8H8Zm2 2H9V9h1v1Zm4 2h-1v1h-2v1h3v-2Zm-4 2v-1H8v1h2Z"/>
            <path d="M12 9h2V8h-2v1Z"/>
        </svg>
    </button>
    <input type="search" name="query" ...>
    ...
</ws-search-box>
The button must be placed identically both in the desktop header and in the mobile offcanvas menu. Using the attribute aria-label=%QRCodeBtnTitle%, the shop text snippets are used for accessibility.
The full-screen modal is inserted once in the base layout. The area #wsQRScanner is responsible for displaying the camera preview from HTML5-QR-Code. The integration is done via the file layouts/default.htm:
<div id="wsQRCodeScanModal" class="modal" tabindex="-1">
    <div class="modal-dialog modal-fullscreen">
        <div class="modal-content">
            <div class="modal-header">
                <p class="h5 modal-title">
                    <svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" fill="currentColor"
                         class="me-2 align-top" viewBox="0 0 16 16">
                        <path d="M0 .5A.5.5 0 0 1 .5 0h3a.5.5 0 0 1 0 1H1v2.5a.5.5 0 0 1-1 0v-3Zm12 0a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0V1h-2.5a.5.5 0 0 1-.5-.5ZM.5 12a.5.5 0 0 1 .5.5V15h2.5a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 .5-.5Zm15 0a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1 0-1H15v-2.5a.5.5 0 0 1 .5-.5ZM4 4h1v1H4V4Z"/>
                        <path d="M7 2H2v5h5V2ZM3 3h3v3H3V3Zm2 8H4v1h1v-1Z"/>
                        <path d="M7 9H2v5h5V9Zm-4 1h3v3H3v-3Zm8-6h1v1h-1V4Z"/>
                        <path d="M9 2h5v5H9V2Zm1 1v3h3V3h-3ZM8 8v2h1v1H8v1h2v-2h1v2h1v-1h2v-1h-3V8H8Zm2 2H9V9h1v1Zm4 2h-1v1h-2v1h3v-2Zm-4 2v-1H8v1h2Z"/>
                        <path d="M12 9h2V8h-2v1Z"/>
                    </svg>
                    %%QrScan%%
                </p>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="%%Close%%"></button>
            </div>
            <div class="modal-body position-relative">
                <div id="wsQRScanner" class="h-100"></div>
            </div>
        </div>
    </div>
</div>

JavaScript constant

The path to the library is set as a JavaScript variable in the {{ autoescape "js" }} block of the layout file layouts/default.htm.
{{ autoescape "js" }}
    {{ var $cAddToBasketTarget = $wsViews.viewUrl("basket.htm") }}
    {{ var $cAddToBasketParams = { wscsrf: $wsActions.csrfToken, quantity: 1 } }}
    {{ var $cAddToBasketLink = $wsActions.url("BasketItemAdd", $cAddToBasketTarget, $cAddToBasketParams) }}
    <script>
        const wsQRCodeJS = "{{= static('scripts/html5-qrcode.min.js') }}";
        const wsAddToBasketLink = "{{= $cAddToBasketLink }}";
    </script>
{{ /autoescape }}

Scanner logic

The following code block is to be inserted into the file scripts/wsGlobal.js. The flow is as follows:
  1. When the scanner is opened for the first time, html5-qrcode.min.js is loaded.
  2. If the library was already loaded, the scanner is started directly via wsInitQRScan().
  3. html5-qrcode starts the camera and shows the preview in the #wsQRScanner area.
  4. After a successful scan, URL codes redirect directly to the scanned URL — barcodes place the product with the scanned ID directly into the basket.
  5. When the window is closed, the camera is stopped.
// ---------------------------------------------------------- QR code scanner - start
// const html5QrCode and function wsInitQRScan is in script html5-qrcode.min.js

function wsInitQRScan() {
    Html5Qrcode.getCameras().then(devices => {
        if (devices && devices.length) {
            // use this to start scanning
            html5QrCode.start(
                {
                    facingMode: "environment"
                },
                {
                    fps: 10,
                    qrbox: { width: 250, height: 250 }
                },
                (decodedText, decodedResult) => {
                    // do something when code is read
                    html5QrCode.stop().then((ignore) => {
                        // QR Code scanning is stopped
                        if (decodedText.startsWith("http")) {
                            // execute QR code if code is a URL
                            location.href = decodedText;
                        } else {
                            // execute WEBSALE search if Barcode
                            location.href = wsAddToBasketLink + "&productId=" + decodedText;
                        }
                    }).catch((err) => {
                        // Stop failed, handle it
                    });
                },
                (errorMessage) => {
                    // parse error, ignore it
                })
                .catch((err) => {
                    // Start failed, handle it
                }
            );
        }
    }).catch(err => {
        // access denied
    });
}

document.querySelector("#wsQRCodeScanModal").addEventListener("show.bs.modal", function(e) {
    var wsQRCodeScanScript = document.querySelector("#wsQRCodeScanScript");
    if (!wsQRCodeScanScript) {
        // lazyload script for better performance
        var script = document.createElement("script");
        script.setAttribute("id", "wsQRCodeScanScript");
        script.setAttribute("src", wsQRCodeJS);
        document.head.appendChild(script);
    } else {
        // execute function if script is already loaded
        wsInitQRScan();
    }
});

document.querySelector("#wsQRCodeScanModal").addEventListener("hidden.bs.modal", function(e) {
    html5QrCode.stop().then((ignore) => {
        // QR Code scanning is stopped
    }).catch((err) => {
        // Stop failed, handle it
    });
});

// ---------------------------------------------------------- QR code scanner - end