@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/loggingfor 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 byopen()and thereadysignal. 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. Returnsnullif 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 initializedInitializing- Configuration in progressReady- Scanner fully initialized and ready for useError- 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 ornullif the user cancels
Features:
- Only visible when scanner is ready (uses
*sharedScannerReadydirective 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
OnDestroyto abort any in-progress scanning operations - Uses
AbortControllerfor 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*ngIfelse 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/loggingfactory pattern - Memory Safety: Proper cleanup in
OnDestroylifecycle 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:
ScannerButtonComponentincludesdata-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"]');