Files
ISA-Frontend/libs/shared/scanner/README.md
2025-11-25 14:13:44 +01:00

17 KiB

@isa/shared/scanner

Enterprise-grade barcode scanning library for ISA-Frontend using the Scandit SDK, providing mobile barcode scanning capabilities for iOS and Android platforms.

Overview

The @isa/shared/scanner library provides a complete, production-ready solution for mobile barcode scanning with comprehensive support for multiple symbologies, platform detection, ready-state management, and seamless integration with Angular's reactive patterns. Built on the Scandit SDK, it offers ready-to-use components, directives, and services for implementing barcode scanning workflows throughout the ISA application.

Key Use Cases:

  • Product lookup and inventory management
  • Order processing and verification
  • Returns and remission workflows
  • Asset tracking and identification
  • QR code scanning for digital workflows

Type: Shared component library (UI + Services)

Installation

import {
  ScannerService,
  ScannerButtonComponent,
  ScannerReadyDirective
} from '@isa/shared/scanner';

Features

  • Multiple Symbologies: EAN-8, EAN-13/UPC-A, UPC-E, Code 128, Code 39, Code 93, Interleaved 2 of 5, QR Code
  • Platform Detection: Automatic iOS/Android detection with unsupported platform handling
  • Ready-State Management: Reactive signals for scanner initialization status
  • Overlay UI: Full-screen camera overlay with CDK integration
  • Form Integration: Output bindings for Angular forms
  • Configurable: Injection tokens for license key, library location, and symbologies
  • TypeScript: Full type safety with Zod schema validation
  • Logging: Integrated with @isa/core/logging for debugging
  • E2E Testing: E2E attributes for automated testing (data-which="scan-button")

API Reference

Services

ScannerService

Core service that manages barcode scanning functionality using the Scandit SDK.

Properties:

  • ready: Signal<boolean> - Computed signal indicating whether the scanner is initialized and ready to use. Automatically triggers configuration on first access.

Methods:

  • configure(): Promise<void> - Manually configure the Scandit SDK. Called automatically by open() and the ready signal. Handles platform compatibility checks and license validation.
  • open(options?: ScannerInputs): Promise<string | null> - Opens the scanner overlay interface and returns a promise that resolves to the scanned barcode value. Returns null if the user cancels scanning.

Configuration Options (ScannerInputs):

type ScannerInputs = {
  symbologies?: Symbology[];  // Override default barcode types
  abortSignal?: AbortSignal;  // Cancel scanning operation
};

Scanner Status:

The service tracks initialization through the following states:

  • None - Initial state, not yet initialized
  • Initializing - Configuration in progress
  • Ready - Scanner fully initialized and ready for use
  • Error - Initialization failed

Platform Support:

Only iOS and Android platforms are supported. Attempting to use the scanner on unsupported platforms will log a warning and prevent initialization. The service throws PlatformNotSupportedError for unsupported platforms.

Default Symbologies:

The scanner recognizes the following barcode formats by default:

  • EAN-8
  • EAN-13 / UPC-A
  • UPC-E
  • Code 128
  • Code 39
  • Code 93
  • Interleaved 2 of 5
  • QR Code

Components

ScannerButtonComponent

A ready-to-use button component that triggers the barcode scanner when clicked. Automatically handles scanner lifecycle and cleanup.

Selector: shared-scanner-button

Inputs:

  • size: InputSignal<IconButtonSize> - Button size (default: 'large')
  • disabled: ModelSignal<boolean> - Whether the button is disabled (default: false)

Outputs:

  • scan: OutputEmitterRef<string | null> - Emits the scanned barcode value or null if the user cancels

Features:

  • Only visible when scanner is ready (uses *sharedScannerReady directive internally)
  • Opens full-screen scanner overlay on click
  • Automatically aborts scanning operations on component destroy
  • Includes E2E testing attribute: data-which="scan-button"
  • Primary color styling with scanner icon

Lifecycle:

  • Implements OnDestroy to abort any in-progress scanning operations
  • Uses AbortController for proper cleanup

ScannerComponent

Internal component that renders the camera view and handles barcode detection. Used by ScannerService via CDK overlay and typically not used directly in application code.

Selector: shared-scanner

Inputs:

  • symbologies: InputSignal<Symbology[]> - Array of barcode types to detect

Outputs:

  • scan: OutputEmitterRef<string> - Emits detected barcode data

Features:

  • Full-screen camera view with close button
  • Progress indicator during initialization
  • Zone-aware event handling for optimal change detection
  • Automatic camera lifecycle management (on/off)
  • Cleanup on destroy to prevent memory leaks

Directives

ScannerReadyDirective

Structural directive that conditionally renders content based on scanner ready state. Similar to *ngIf, but specifically tied to scanner initialization status.

Selector: *sharedScannerReady

Inputs:

  • scannerReadyElse: InputSignal<TemplateRef<unknown> | undefined> - Optional template to show when scanner is not ready (similar to *ngIf else template)

Features:

  • Reactive to scanner status changes using Angular effects
  • Supports else template for fallback content
  • Automatically updates when scanner becomes ready
  • No manual subscription management required

Injection Tokens

SCANDIT_LICENSE

Injection token for the Scandit license key.

Default: Retrieves from application config at licence.scandit path Type: InjectionToken<string>

SCANDIT_LIBRARY_LOCATION

Injection token for the Scandit SDK library location.

Default: /scandit relative to document.baseURI Type: InjectionToken<string>

SCANDIT_DEFAULT_SYMBOLOGIES

Injection token for the default barcode symbologies to scan.

Default: Array of common symbologies (EAN, UPC, Code128, Code39, Code93, ITF, QR) Type: InjectionToken<Symbology[]>

Errors

PlatformNotSupportedError

Thrown when attempting to use the scanner on non-mobile platforms.

export class PlatformNotSupportedError extends Error {
  constructor(); // Message: "ScannerService is only supported on iOS and Android platforms"
}

Behavior:

  • Caught by ScannerService.configure() and logged as a warning
  • Prevents scanner initialization on unsupported platforms
  • Does not crash the application

Usage Examples

Basic Programmatic Scanning

import { Component, inject } from '@angular/core';
import { ScannerService } from '@isa/shared/scanner';

@Component({
  selector: 'app-checkout',
  template: `
    <button (click)="scanProduct()">Scan Product</button>
    @if (scannedCode) {
      <p>Scanned: {{ scannedCode }}</p>
    }
  `
})
export class CheckoutComponent {
  #scannerService = inject(ScannerService);

  scannedCode: string | null = null;

  async scanProduct() {
    try {
      this.scannedCode = await this.#scannerService.open();
      if (this.scannedCode) {
        console.log('Product code:', this.scannedCode);
        // Process the scanned code (e.g., fetch product details)
      }
    } catch (error) {
      console.error('Scanning failed:', error);
    }
  }
}

Using Scanner Button Component

import { Component } from '@angular/core';
import { ScannerButtonComponent } from '@isa/shared/scanner';

@Component({
  selector: 'app-inventory',
  imports: [ScannerButtonComponent],
  template: `
    <h2>Inventory Lookup</h2>

    <shared-scanner-button
      [size]="'large'"
      [disabled]="isProcessing"
      (scan)="onScan($event)"
    />

    @if (lastScan) {
      <div class="scan-result">
        <p>Last scanned: {{ lastScan }}</p>
        <p>Product: {{ productName }}</p>
      </div>
    }
  `
})
export class InventoryComponent {
  lastScan: string | null = null;
  productName: string = '';
  isProcessing = false;

  async onScan(code: string | null) {
    if (code) {
      this.lastScan = code;
      this.isProcessing = true;
      await this.processBarcode(code);
      this.isProcessing = false;
    }
  }

  private async processBarcode(code: string) {
    // Fetch product details from API
    const response = await fetch(`/api/products/${code}`);
    const product = await response.json();
    this.productName = product.name;
  }
}

Scanning with Custom Symbologies

import { Component, inject } from '@angular/core';
import { ScannerService } from '@isa/shared/scanner';
import { Symbology } from 'scandit-web-datacapture-barcode';

@Component({
  selector: 'app-qr-scanner',
  template: `
    <button (click)="scanQRCode()">Scan QR Code Only</button>
    <button (click)="scanEAN()">Scan Product Barcode</button>
  `
})
export class QRScannerComponent {
  #scannerService = inject(ScannerService);

  async scanQRCode() {
    // Only scan QR codes
    const result = await this.#scannerService.open({
      symbologies: [Symbology.QR]
    });

    if (result) {
      this.handleQRCode(result);
    }
  }

  async scanEAN() {
    // Only scan EAN barcodes
    const result = await this.#scannerService.open({
      symbologies: [Symbology.EAN8, Symbology.EAN13UPCA]
    });

    if (result) {
      this.handleProductCode(result);
    }
  }

  private handleQRCode(data: string) {
    console.log('QR Code:', data);
  }

  private handleProductCode(code: string) {
    console.log('Product Code:', code);
  }
}

Using Scanner Ready Directive

import { Component } from '@angular/core';
import { ScannerButtonComponent, ScannerReadyDirective } from '@isa/shared/scanner';

@Component({
  selector: 'app-product-lookup',
  imports: [ScannerButtonComponent, ScannerReadyDirective],
  template: `
    <div *sharedScannerReady="; else scannerNotReady">
      <h2>Scan Product Barcode</h2>
      <p>Point your camera at a barcode to scan</p>
      <shared-scanner-button (scan)="onScan($event)" />
    </div>

    <ng-template #scannerNotReady>
      <div class="fallback">
        <p>Camera scanner is not available on this device</p>
        <p>Please enter the barcode manually:</p>
        <input
          type="text"
          placeholder="Enter barcode"
          (input)="onManualEntry($event)"
        />
      </div>
    </ng-template>
  `
})
export class ProductLookupComponent {
  onScan(code: string | null) {
    if (code) {
      this.lookupProduct(code);
    }
  }

  onManualEntry(event: Event) {
    const input = event.target as HTMLInputElement;
    if (input.value.length >= 8) {
      this.lookupProduct(input.value);
    }
  }

  private lookupProduct(barcode: string) {
    console.log('Looking up product:', barcode);
    // Fetch product by barcode
  }
}

Cancelling a Scan Operation

import { Component, inject, OnDestroy } from '@angular/core';
import { ScannerService } from '@isa/shared/scanner';

@Component({
  selector: 'app-advanced-scanner',
  template: `
    <button (click)="startScan()" [disabled]="isScanning">
      {{ isScanning ? 'Scanning...' : 'Start Scan' }}
    </button>
    <button (click)="cancelScan()" [disabled]="!isScanning">
      Cancel
    </button>

    @if (errorMessage) {
      <p class="error">{{ errorMessage }}</p>
    }
  `
})
export class AdvancedScannerComponent implements OnDestroy {
  #scannerService = inject(ScannerService);
  #abortController = new AbortController();

  isScanning = false;
  errorMessage = '';

  async startScan() {
    this.isScanning = true;
    this.errorMessage = '';

    try {
      const result = await this.#scannerService.open({
        abortSignal: this.#abortController.signal
      });

      if (result) {
        console.log('Scanned:', result);
      } else {
        console.log('Scan was cancelled by user');
      }
    } catch (error) {
      this.errorMessage = 'Scan was cancelled or failed';
      console.error('Scan error:', error);
    } finally {
      this.isScanning = false;
    }
  }

  cancelScan() {
    this.#abortController.abort();
    this.#abortController = new AbortController(); // Create new controller for next scan
  }

  ngOnDestroy() {
    this.#abortController.abort();
  }
}

Checking Scanner Readiness

import { Component, inject, effect } from '@angular/core';
import { ScannerService } from '@isa/shared/scanner';

@Component({
  selector: 'app-scanner-status',
  template: `
    <div class="status-indicator">
      @if (scannerReady()) {
        <span class="ready">Scanner Ready</span>
      } @else {
        <span class="initializing">Initializing Scanner...</span>
      }
    </div>
  `
})
export class ScannerStatusComponent {
  #scannerService = inject(ScannerService);

  scannerReady = this.#scannerService.ready;

  constructor() {
    effect(() => {
      if (this.scannerReady()) {
        console.log('Scanner initialized successfully');
        // Perform actions when scanner becomes ready
      }
    });
  }
}

Configuration

Basic Configuration

The library requires a Scandit license key configured in your application config:

{
  "licence": {
    "scandit": "YOUR_SCANDIT_LICENSE_KEY"
  }
}

The Scandit SDK library files must be available at /scandit/ relative to your application's base URI.

Custom Configuration via Injection Tokens

Override default configuration by providing custom values:

import {
  SCANDIT_LICENSE,
  SCANDIT_LIBRARY_LOCATION,
  SCANDIT_DEFAULT_SYMBOLOGIES
} from '@isa/shared/scanner';
import { Symbology } from 'scandit-web-datacapture-barcode';

// In your providers array (e.g., app.config.ts):
export const appConfig: ApplicationConfig = {
  providers: [
    // Custom license key
    {
      provide: SCANDIT_LICENSE,
      useValue: 'YOUR-CUSTOM-LICENSE-KEY'
    },
    // Custom library location (e.g., CDN)
    {
      provide: SCANDIT_LIBRARY_LOCATION,
      useValue: 'https://cdn.example.com/scandit/'
    },
    // Custom default symbologies (only QR and Code128)
    {
      provide: SCANDIT_DEFAULT_SYMBOLOGIES,
      useValue: [Symbology.QR, Symbology.Code128]
    }
  ]
};

Dependencies

Core Dependencies

  • @angular/core - Angular framework (signals, effects, DI)
  • @angular/cdk - CDK platform detection and overlay
  • scandit-web-datacapture-core - Scandit SDK core functionality
  • scandit-web-datacapture-barcode - Scandit barcode scanning module
  • zod - Configuration schema validation

ISA Dependencies

  • @isa/core/config - Application configuration management
  • @isa/core/logging - Logging utilities with factory pattern
  • @isa/ui/buttons - Icon button component
  • @isa/icons - Icon library (scanner and close icons)

Error Handling

The scanner library provides comprehensive error handling:

Platform Compatibility

// Automatically handled by ScannerService
if (!platform.IOS && !platform.ANDROID) {
  // Logs warning and prevents initialization
  // Does not crash the application
}

Configuration Errors

try {
  await scannerService.configure();
} catch (error) {
  // Configuration errors are logged and set status to 'Error'
  console.error('Scanner configuration failed:', error);
}

Scan Operation Errors

try {
  const result = await scannerService.open();
} catch (error) {
  // Handle abort or configuration errors
  if (error.message === 'Scanner aborted') {
    console.log('User cancelled scanning');
  } else {
    console.error('Scanning failed:', error);
  }
}

Architecture Notes

  • Signals & Effects: Uses Angular signals for reactive state management and effects for side effect handling
  • CDK Overlay: Integrates with Angular CDK Overlay for full-screen scanner UI with backdrop and positioning
  • Camera Lifecycle: Automatic camera management (on/off state) with proper cleanup
  • Zone Management: Zone-aware event handling for optimal change detection performance
  • Standalone: All components and directives are standalone for easy tree-shaking
  • Logging: Comprehensive logging with @isa/core/logging factory pattern
  • Memory Safety: Proper cleanup in OnDestroy lifecycle hooks to prevent memory leaks

Platform Requirements

  • Supported Platforms: iOS and Android only
  • Scandit License: Valid Scandit SDK license key required
  • Camera Permissions: Application must request and obtain camera permissions
  • Library Files: Scandit SDK files must be accessible at configured location

E2E Testing

The scanner components include E2E testing attributes for automated test selection:

  • ScannerButtonComponent includes data-which="scan-button" attribute
  • Use this attribute to locate and interact with the scanner button in E2E tests
// Example E2E test
await page.click('[data-which="scan-button"]');