@ -8,10 +8,15 @@
class = "dialog relative bg-white rounded-lg shadow-xl max-w-lg w-full mx-4"
>
<!-- Header -- >
< div class = "p-4 border-b border-gray-200" >
< h3 class = "text-lg font-medium text-gray-900" > Scan QR Code < / h3 >
< div
class = "p-4 border-b border-gray-200 flex justify-between items-center"
>
< div >
< h3 class = "text-lg font-medium text-gray-900" > Scan QR Code < / h3 >
< span class = "text-xs text-gray-500" > v1 .1 .0 < / span >
< / div >
< button
class = "absolute top-4 right-4 text-gray-400 hover:text-gray-500"
class = "text-gray-400 hover:text-gray-500"
aria - label = "Close dialog"
@ click = "close"
>
@ -38,39 +43,89 @@
class = "relative aspect-square"
>
<!-- Status Message -- >
< div
< div
class = "absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-center py-2 z-10"
>
< p v-if ="isInitializing" > Initializing camera... < / p >
< p v -else -if = " isScanning " > Position QR code in the frame < / p >
< p v -else -if = " error " class = "text-red-300" > { { error } } < / p >
< p v-else > Ready to scan < / p >
< div
v - if = "isInitializing"
class = "flex items-center justify-center space-x-2"
>
< svg
class = "animate-spin h-5 w-5 text-white"
xmlns = "http://www.w3.org/2000/svg"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
< / svg >
< span > { { initializationStatus } } < / span >
< / div >
< p
v - else - if = "isScanning"
class = "flex items-center justify-center space-x-2"
>
< span
class = "inline-block w-2 h-2 bg-green-500 rounded-full animate-pulse"
> < / span >
< span > Position QR code in the frame < / span >
< / p >
< p v -else -if = " error " class = "text-red-300" >
< span class = "font-medium" > Error : < / span > { { error } }
< / p >
< p v -else class = "flex items-center justify-center space-x-2" >
< span
class = "inline-block w-2 h-2 bg-blue-500 rounded-full"
> < / span >
< span > Ready to scan < / span >
< / p >
< / div >
< qrcode -stream
: camera = "options?.camera === 'front' ? 'user' : 'environment'"
: camera = "preferredCamera "
@ decode = "onDecode"
@ init = "onInit"
@ detect = "onDetect"
@ error = "onError"
@ camera - on = "onCameraOn"
@ camera - off = "onCameraOff"
/ >
<!-- Scanning Frame -- >
< div
class = "absolute inset-0 border-2"
: class = " {
'border-blue-500' : ! error && ! isScanning ,
'border-green-500 animate-pulse' : isScanning ,
'border-red-500' : error
'border-red-500' : error ,
} "
style = "opacity: 0.5; pointer-events: none;"
style = "opacity: 0.5; pointer-events: none"
> < / div >
<!-- Debug Info -- >
< div
class = "absolute bottom-16 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center py-1"
>
Camera : { { preferredCamera === "user" ? "Front" : "Back" } } |
Status : { { cameraStatus } }
< / div >
<!-- Camera Switch Button -- >
< button
@ click = "toggleCamera"
class = "absolute bottom-4 right-4 bg-white rounded-full p-2 shadow-lg"
title = "Switch camera"
@ click = "toggleCamera"
>
< svg
class = "h-6 w-6 text-gray-600"
@ -157,6 +212,16 @@ interface ScanProps {
options ? : QRScannerOptions ;
}
interface DetectionResult {
content ? : string ;
location ? : {
topLeft : { x : number ; y : number } ;
topRight : { x : number ; y : number } ;
bottomLeft : { x : number ; y : number } ;
bottomRight : { x : number ; y : number } ;
} ;
}
@ Component ( {
components : {
QrcodeStream ,
@ -167,6 +232,9 @@ export default class QRScannerDialog extends Vue {
@ Prop ( { type : Function } ) onError ? : ScanProps [ "onError" ] ;
@ Prop ( { type : Object } ) options ? : ScanProps [ "options" ] ;
/ / V e r s i o n
readonly version = "1.1.0" ;
visible = true ;
error : string | null = null ;
useQRReader = __USE_QR_READER__ ;
@ -175,10 +243,12 @@ export default class QRScannerDialog extends Vue {
__IS_MOBILE__ ||
Capacitor . getPlatform ( ) === "android" ||
Capacitor . getPlatform ( ) === "ios" ;
isInitializing = true ;
isScanning = false ;
preferredCamera : 'user' | 'environment' = 'environment' ;
preferredCamera : "user" | "environment" = "environment" ;
initializationStatus = "Checking camera access..." ;
cameraStatus = "Initializing" ;
created ( ) {
logger . log ( "QRScannerDialog platform detection:" , {
@ -189,7 +259,9 @@ export default class QRScannerDialog extends Vue {
isNativePlatform : this . isNativePlatform ,
userAgent : navigator . userAgent ,
mediaDevices : ! ! navigator . mediaDevices ,
getUserMedia : ! ! ( navigator . mediaDevices && navigator . mediaDevices . getUserMedia ) ,
getUserMedia : ! ! (
navigator . mediaDevices && navigator . mediaDevices . getUserMedia
) ,
} ) ;
/ / I f o n n a t i v e p l a t f o r m , c l o s e i m m e d i a t e l y a n d d o n ' t i n i t i a l i z e w e b s c a n n e r
@ -200,7 +272,6 @@ export default class QRScannerDialog extends Vue {
}
async onInit ( promise : Promise < void > ) : Promise < void > {
/ / D o n ' t i n i t i a l i z e o n m o b i l e p l a t f o r m s
if ( this . isNativePlatform ) {
logger . log ( "Skipping web scanner initialization on native platform" ) ;
return ;
@ -208,16 +279,85 @@ export default class QRScannerDialog extends Vue {
this . isInitializing = true ;
this . error = null ;
logger . log ( "Initializing QR scanner..." ) ;
this . initializationStatus = "Checking camera access..." ;
try {
/ / F i r s t c h e c k i f m e d i a D e v i c e s A P I i s a v a i l a b l e
if ( ! navigator . mediaDevices ) {
throw new Error (
"Camera API not available. Please ensure you're using HTTPS." ,
) ;
}
logger . log ( "Starting QR scanner initialization..." , {
mediaDevices : ! ! navigator . mediaDevices ,
getUserMedia : ! ! (
navigator . mediaDevices && navigator . mediaDevices . getUserMedia
) ,
constraints : {
video : true ,
facingMode : this . preferredCamera ,
} ,
} ) ;
/ / E x p l i c i t l y r e q u e s t c a m e r a p e r m i s s i o n f i r s t
this . initializationStatus = "Requesting camera permission..." ;
try {
const stream = await navigator . mediaDevices . getUserMedia ( {
video : {
facingMode : this . preferredCamera ,
} ,
} ) ;
/ / S t o p t h e t e s t s t r e a m i m m e d i a t e l y
stream . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
this . initializationStatus = "Camera permission granted..." ;
logger . log ( "Camera permission granted" ) ;
} catch ( permissionError ) {
const error = permissionError as Error ;
logger . error ( "Camera permission error:" , {
name : error . name ,
message : error . message ,
} ) ;
if (
error . name === "NotAllowedError" ||
error . name === "PermissionDeniedError"
) {
throw new Error (
"Camera access denied. Please grant camera permission and try again." ,
) ;
} else if (
error . name === "NotFoundError" ||
error . name === "DevicesNotFoundError"
) {
throw new Error (
"No camera found. Please ensure your device has a camera." ,
) ;
} else if (
error . name === "NotReadableError" ||
error . name === "TrackStartError"
) {
throw new Error ( "Camera is in use by another application." ) ;
} else {
throw new Error ( ` Camera error: ${ error . message } ` ) ;
}
}
/ / N o w i n i t i a l i z e t h e Q R s c a n n e r
this . initializationStatus = "Starting QR scanner..." ;
logger . log ( "Initializing QR scanner..." ) ;
await promise ;
this . isInitializing = false ;
this . cameraStatus = "Ready" ;
logger . log ( "QR scanner initialized successfully" ) ;
} catch ( error ) {
const wrappedError =
error instanceof Error ? error : new Error ( String ( error ) ) ;
this . error = wrappedError . message ;
this . cameraStatus = "Error" ;
if ( this . onError ) {
this . onError ( wrappedError ) ;
}
@ -225,28 +365,70 @@ export default class QRScannerDialog extends Vue {
error : wrappedError . message ,
stack : wrappedError . stack ,
name : wrappedError . name ,
type : wrappedError . constructor . name ,
} ) ;
} finally {
this . isInitializing = false ;
}
}
onDetect ( promise : Promise < any > ) : void {
onCameraOn ( ) : void {
this . cameraStatus = "Active" ;
logger . log ( "Camera turned on successfully" ) ;
}
onCameraOff ( ) : void {
this . cameraStatus = "Off" ;
logger . log ( "Camera turned off" ) ;
}
onDetect ( result : DetectionResult | Promise < DetectionResult > ) : void {
this . isScanning = true ;
this . cameraStatus = "Detecting" ;
logger . log ( "QR code detected, processing..." ) ;
promise
. then ( ( result ) => {
logger . log ( "QR code processed successfully:" , result ) ;
} )
. catch ( ( error ) => {
logger . error ( "Error processing QR code:" , {
error : error instanceof Error ? error . message : String ( error ) ,
stack : error instanceof Error ? error . stack : undefined ,
} ) ;
} )
. finally ( ( ) => {
/ / H a n d l e b o t h p r o m i s e a n d d i r e c t v a l u e c a s e s
const processResult = ( detection : DetectionResult ) => {
try {
logger . log ( "QR code processed successfully:" , detection ) ;
} catch ( error ) {
this . handleError ( error ) ;
} finally {
this . isScanning = false ;
} ) ;
this . cameraStatus = "Active" ;
}
} ;
/ / H a n d l e b o t h p r o m i s e a n d n o n - p r o m i s e r e s u l t s
if ( result && typeof result . then === "function" ) {
result
. then ( processResult )
. catch ( ( error : Error ) => this . handleError ( error ) )
. finally ( ( ) => {
this . isScanning = false ;
this . cameraStatus = "Active" ;
} ) ;
} else {
processResult ( result ) ;
}
}
private handleError ( error : unknown ) : void {
const wrappedError =
error instanceof Error ? error : new Error ( String ( error ) ) ;
this . error = wrappedError . message ;
this . cameraStatus = "Error" ;
if ( this . onError ) {
this . onError ( wrappedError ) ;
}
logger . error ( "QR scanner error:" , {
error : wrappedError . message ,
stack : wrappedError . stack ,
name : wrappedError . name ,
type : wrappedError . constructor . name ,
} ) ;
}
onDecode ( result : string ) : void {
@ -255,35 +437,13 @@ export default class QRScannerDialog extends Vue {
this . onScan ( result ) ;
this . close ( ) ;
} catch ( error ) {
const wrappedError =
error instanceof Error ? error : new Error ( String ( error ) ) ;
this . error = wrappedError . message ;
if ( this . onError ) {
this . onError ( wrappedError ) ;
}
logger . error ( "Error handling QR scan result:" , {
error : wrappedError . message ,
stack : wrappedError . stack ,
name : wrappedError . name ,
} ) ;
}
}
onError ( error : Error ) : void {
this . isScanning = false ;
logger . error ( "QR scanner error:" , {
error : error . message ,
stack : error . stack ,
name : error . name ,
} ) ;
this . error = error . message ;
if ( this . onError ) {
this . onError ( error ) ;
this . handleError ( error ) ;
}
}
toggleCamera ( ) : void {
this . preferredCamera = this . preferredCamera === 'user' ? 'environment' : 'user' ;
this . preferredCamera =
this . preferredCamera === "user" ? "environment" : "user" ;
}
retryScanning ( ) : void {