@ -1,4 +1,10 @@
import { QRScannerService , ScanListener , QRScannerOptions , CameraState , CameraStateListener } from "./types" ;
import {
QRScannerService ,
ScanListener ,
QRScannerOptions ,
CameraState ,
CameraStateListener ,
} from "./types" ;
import { logger } from "@/utils/logger" ;
import { EventEmitter } from "events" ;
import jsQR from "jsqr" ;
@ -22,7 +28,7 @@ export class WebInlineQRScanner implements QRScannerService {
private readonly FRAME_INTERVAL = 1000 / 15 ; // ~67ms between frames
private lastFrameTime = 0 ;
private cameraStateListeners : Set < CameraStateListener > = new Set ( ) ;
private currentState : CameraState = 'off' ;
private currentState : CameraState = "off" ;
private currentStateMessage? : string ;
constructor ( private options? : QRScannerOptions ) {
@ -49,15 +55,21 @@ export class WebInlineQRScanner implements QRScannerService {
private updateCameraState ( state : CameraState , message? : string ) {
this . currentState = state ;
this . currentStateMessage = message ;
this . cameraStateListeners . forEach ( listener = > {
this . cameraStateListeners . forEach ( ( listener ) = > {
try {
listener . onStateChange ( state , message ) ;
logger . info ( ` [WebInlineQRScanner: ${ this . id } ] Camera state changed to: ${ state } ` , {
state ,
message ,
} ) ;
logger . info (
` [WebInlineQRScanner: ${ this . id } ] Camera state changed to: ${ state } ` ,
{
state ,
message ,
} ,
) ;
} catch ( error ) {
logger . error ( ` [WebInlineQRScanner: ${ this . id } ] Error in camera state listener: ` , error ) ;
logger . error (
` [WebInlineQRScanner: ${ this . id } ] Error in camera state listener: ` ,
error ,
) ;
}
} ) ;
}
@ -74,7 +86,7 @@ export class WebInlineQRScanner implements QRScannerService {
async checkPermissions ( ) : Promise < boolean > {
try {
this . updateCameraState ( 'initializing' , 'Checking camera permissions...' ) ;
this . updateCameraState ( "initializing" , "Checking camera permissions..." ) ;
logger . error (
` [WebInlineQRScanner: ${ this . id } ] Checking camera permissions... ` ,
) ;
@ -86,7 +98,7 @@ export class WebInlineQRScanner implements QRScannerService {
permissions . state ,
) ;
const granted = permissions . state === "granted" ;
this . updateCameraState ( granted ? 'ready' : 'permission_denied' ) ;
this . updateCameraState ( granted ? "ready" : "permission_denied" ) ;
return granted ;
} catch ( error ) {
logger . error (
@ -96,14 +108,17 @@ export class WebInlineQRScanner implements QRScannerService {
stack : error instanceof Error ? error.stack : undefined ,
} ,
) ;
this . updateCameraState ( 'error' , 'Error checking camera permissions' ) ;
this . updateCameraState ( "error" , "Error checking camera permissions" ) ;
return false ;
}
}
async requestPermissions ( ) : Promise < boolean > {
try {
this . updateCameraState ( 'initializing' , 'Requesting camera permissions...' ) ;
this . updateCameraState (
"initializing" ,
"Requesting camera permissions..." ,
) ;
logger . error (
` [WebInlineQRScanner: ${ this . id } ] Requesting camera permissions... ` ,
) ;
@ -141,8 +156,8 @@ export class WebInlineQRScanner implements QRScannerService {
} ,
} ) ;
this . updateCameraState ( 'ready' , 'Camera permissions granted' ) ;
this . updateCameraState ( "ready" , "Camera permissions granted" ) ;
// Stop the test stream immediately
stream . getTracks ( ) . forEach ( ( track ) = > {
logger . error ( ` [WebInlineQRScanner: ${ this . id } ] Stopping test track: ` , {
@ -154,20 +169,35 @@ export class WebInlineQRScanner implements QRScannerService {
} ) ;
return true ;
} catch ( error ) {
const wrappedError = error instanceof Error ? error : new Error ( String ( error ) ) ;
const wrappedError =
error instanceof Error ? error : new Error ( String ( error ) ) ;
// Update state based on error type
if ( wrappedError . name === "NotFoundError" || wrappedError . name === "DevicesNotFoundError" ) {
this . updateCameraState ( 'not_found' , 'No camera found on this device' ) ;
if (
wrappedError . name === "NotFoundError" ||
wrappedError . name === "DevicesNotFoundError"
) {
this . updateCameraState ( "not_found" , "No camera found on this device" ) ;
throw new Error ( "No camera found on this device" ) ;
} else if ( wrappedError . name === "NotAllowedError" || wrappedError . name === "PermissionDeniedError" ) {
this . updateCameraState ( 'permission_denied' , 'Camera access denied' ) ;
throw new Error ( "Camera access denied. Please grant camera permission and try again" ) ;
} else if ( wrappedError . name === "NotReadableError" || wrappedError . name === "TrackStartError" ) {
this . updateCameraState ( 'in_use' , 'Camera is in use by another application' ) ;
} else if (
wrappedError . name === "NotAllowedError" ||
wrappedError . name === "PermissionDeniedError"
) {
this . updateCameraState ( "permission_denied" , "Camera access denied" ) ;
throw new Error (
"Camera access denied. Please grant camera permission and try again" ,
) ;
} else if (
wrappedError . name === "NotReadableError" ||
wrappedError . name === "TrackStartError"
) {
this . updateCameraState (
"in_use" ,
"Camera is in use by another application" ,
) ;
throw new Error ( "Camera is in use by another application" ) ;
} else {
this . updateCameraState ( 'error' , wrappedError . message ) ;
this . updateCameraState ( "error" , wrappedError . message ) ;
throw new Error ( ` Camera error: ${ wrappedError . message } ` ) ;
}
}
@ -406,7 +436,7 @@ export class WebInlineQRScanner implements QRScannerService {
this . isScanning = true ;
this . scanAttempts = 0 ;
this . lastScanTime = Date . now ( ) ;
this . updateCameraState ( 'initializing' , 'Starting camera...' ) ;
this . updateCameraState ( "initializing" , "Starting camera..." ) ;
logger . error ( ` [WebInlineQRScanner: ${ this . id } ] Starting scan ` ) ;
// Get camera stream
@ -421,8 +451,8 @@ export class WebInlineQRScanner implements QRScannerService {
} ,
} ) ;
this . updateCameraState ( 'active' , 'Camera is active' ) ;
this . updateCameraState ( "active" , "Camera is active" ) ;
logger . error ( ` [WebInlineQRScanner: ${ this . id } ] Camera stream obtained: ` , {
tracks : this.stream.getTracks ( ) . map ( ( t ) = > ( {
kind : t.kind ,
@ -448,15 +478,22 @@ export class WebInlineQRScanner implements QRScannerService {
this . scanQRCode ( ) ;
} catch ( error ) {
this . isScanning = false ;
const wrappedError = error instanceof Error ? error : new Error ( String ( error ) ) ;
const wrappedError =
error instanceof Error ? error : new Error ( String ( error ) ) ;
// Update state based on error type
if ( wrappedError . name === "NotReadableError" || wrappedError . name === "TrackStartError" ) {
this . updateCameraState ( 'in_use' , 'Camera is in use by another application' ) ;
if (
wrappedError . name === "NotReadableError" ||
wrappedError . name === "TrackStartError"
) {
this . updateCameraState (
"in_use" ,
"Camera is in use by another application" ,
) ;
} else {
this . updateCameraState ( 'error' , wrappedError . message ) ;
this . updateCameraState ( "error" , wrappedError . message ) ;
}
if ( this . scanListener ? . onError ) {
this . scanListener . onError ( wrappedError ) ;
}
@ -513,8 +550,11 @@ export class WebInlineQRScanner implements QRScannerService {
` [WebInlineQRScanner: ${ this . id } ] Stream stopped event emitted ` ,
) ;
} catch ( error ) {
logger . error ( ` [WebInlineQRScanner: ${ this . id } ] Error stopping scan: ` , error ) ;
this . updateCameraState ( 'error' , 'Error stopping camera' ) ;
logger . error (
` [WebInlineQRScanner: ${ this . id } ] Error stopping scan: ` ,
error ,
) ;
this . updateCameraState ( "error" , "Error stopping camera" ) ;
throw error ;
} finally {
this . isScanning = false ;
@ -557,8 +597,11 @@ export class WebInlineQRScanner implements QRScannerService {
` [WebInlineQRScanner: ${ this . id } ] Cleanup completed successfully ` ,
) ;
} catch ( error ) {
logger . error ( ` [WebInlineQRScanner: ${ this . id } ] Error during cleanup: ` , error ) ;
this . updateCameraState ( 'error' , 'Error during cleanup' ) ;
logger . error (
` [WebInlineQRScanner: ${ this . id } ] Error during cleanup: ` ,
error ,
) ;
this . updateCameraState ( "error" , "Error during cleanup" ) ;
throw error ;
}
}