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

615 lines
17 KiB
Markdown

# @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](https://www.scandit.com/), 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
```ts
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`):**
```ts
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.
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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:
```json
{
"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:
```ts
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
```ts
// Automatically handled by ScannerService
if (!platform.IOS && !platform.ANDROID) {
// Logs warning and prevents initialization
// Does not crash the application
}
```
### Configuration Errors
```ts
try {
await scannerService.configure();
} catch (error) {
// Configuration errors are logged and set status to 'Error'
console.error('Scanner configuration failed:', error);
}
```
### Scan Operation Errors
```ts
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
```ts
// Example E2E test
await page.click('[data-which="scan-button"]');
```