# @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"]');
```
