mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
615 lines
17 KiB
Markdown
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"]');
|
|
```
|