@ -109,26 +109,99 @@ import { PlatformService } from "../services/PlatformService";
import QuickNav from "../components/QuickNav.vue" ;
import UserNameDialog from "../components/UserNameDialog.vue" ;
import { NotificationIface } from "../constants/app" ;
import { db , retrieveSettingsForActiveAccount } from "../db/index" ;
import { db } from "../db/index" ;
import { Contact } from "../db/tables/contacts" ;
import { MASTER_SETTINGS_KEY } from "../db/tables/settings" ;
import { getContactJwtFromJwtUrl } from "../libs/crypto" ;
import {
generateEndorserJwtUrlForAccount ,
isDid ,
register ,
setVisibilityUtil ,
} from "../libs/endorserServer" ;
import { decodeEndorserJwt , ETHR_DID_PREFIX } from "../libs/crypto/vc" ;
import { retrieveAccountMetadata } from "../libs/util" ;
import { Router } from "vue-router" ;
import { logger } from "../utils/logger" ;
import { Camera , CameraResultType , CameraSource } from "@capacitor/camera" ;
import { Camera , CameraResultType , CameraSource , ImageOptions , CameraPermissionState } from '@capacitor/camera' ;
import { App } from '@capacitor/app' ;
import jsQR from "jsqr" ;
/ / D e c l a r e g l o b a l c o n s t a n t s
declare const __USE_QR_READER__ : boolean ;
declare const __IS_MOBILE__ : boolean ;
/ / D e f i n e a l l p o s s i b l e c a m e r a s t a t e s
type CameraState =
| 'initializing'
| 'ready'
| 'error'
| 'checking_permissions'
| 'no_camera_capability'
| 'permission_status_checked'
| 'requesting_permission'
| 'permission_requested'
| 'permission_check_error'
| 'camera_initialized'
| 'camera_error'
| 'opening_camera'
| 'capture_already_in_progress'
| 'photo_captured'
| 'processing_photo'
| 'no_image_data'
| 'capture_error'
| 'user_cancelled'
| 'permission_error'
| 'hardware_unavailable'
| 'capture_completed'
| 'cleanup'
| 'processing_completed' ;
/ / D e f i n e a l l p o s s i b l e Q R p r o c e s s i n g s t a t e s
type QRProcessingState =
| 'processing_image'
| 'qr_code_detected'
| 'no_qr_code_found'
| 'processing_error' ;
interface CameraStateHistoryEntry {
state : CameraState | QRProcessingState ;
timestamp : number ;
details ? : Record < string , unknown > ;
}
/ / C u s t o m A p p S t a t e t y p e t o m a t c h o u r n e e d s
interface CustomAppState {
state : 'active' | 'inactive' | 'background' | 'foreground' ;
}
interface AppStateChangeEvent {
isActive : boolean ;
}
/ / D e f i n e w o r k e r m e s s a g e t y p e s
interface QRCodeResult {
data : string ;
location : {
topLeftCorner : { x : number ; y : number } ;
topRightCorner : { x : number ; y : number } ;
bottomRightCorner : { x : number ; y : number } ;
bottomLeftCorner : { x : number ; y : number } ;
} ;
}
interface WorkerSuccessMessage {
success : true ;
code : QRCodeResult ;
}
interface WorkerErrorMessage {
success : false ;
error : string ;
}
type WorkerMessage = WorkerSuccessMessage | WorkerErrorMessage ;
@ Component ( {
components : {
QrcodeStream : __USE_QR_READER__ ? QrcodeStream : null ,
@ -158,158 +231,339 @@ export default class ContactQRScanShow extends Vue {
private platformService : PlatformService =
PlatformServiceFactory . getInstance ( ) ;
private cameraActive = false ;
private lastCameraState : CameraState | QRProcessingState = 'initializing' ;
private cameraStateHistory : CameraStateHistoryEntry [ ] = [ ] ;
private readonly STATE_HISTORY_LIMIT = 20 ;
private isCapturingPhoto = false ;
private appStateListener ? : { remove : ( ) => Promise < void > } ;
async created ( ) {
const settings = await retrieveSettingsForActiveAccount ( ) ;
this . activeDid = settings . activeDid || "" ;
this . apiServer = settings . apiServer || "" ;
this . givenName = settings . firstName || "" ;
this . hideRegisterPromptOnNewContact =
! ! settings . hideRegisterPromptOnNewContact ;
this . isRegistered = ! ! settings . isRegistered ;
const account = await retrieveAccountMetadata ( this . activeDid ) ;
if ( account ) {
const name =
( settings . firstName || "" ) +
( settings . lastName ? ` ${ settings . lastName } ` : "" ) ;
this . qrValue = await generateEndorserJwtUrlForAccount (
account ,
! ! settings . isRegistered ,
name ,
settings . profileImageUrl || "" ,
false ,
) ;
logger . log ( 'ContactQRScanShow component created' ) ;
try {
/ / R e m o v e a n y e x i s t i n g l i s t e n e r s f i r s t
await App . removeAllListeners ( ) ;
/ / A d d a p p s t a t e l i s t e n e r s
const stateListener = await App . addListener ( 'appStateChange' , ( state : AppStateChangeEvent ) => {
logger . log ( 'App state changed:' , state ) ;
if ( ! state . isActive && this . cameraActive ) {
this . cleanupCamera ( ) ;
}
} ) ;
this . appStateListener = stateListener ;
/ / I n i t i a l i z e c a m e r a w i t h r e t r y l o g i c
if ( this . useQRReader ) {
await this . initializeCamera ( ) ;
await App . addListener ( 'pause' , ( ) => {
logger . log ( 'App paused' ) ;
if ( this . cameraActive ) {
this . cleanupCamera ( ) ;
}
} ) ;
await App . addListener ( 'resume' , ( ) => {
logger . log ( 'App resumed' ) ;
if ( this . cameraActive ) {
this . initializeCamera ( ) ;
}
} ) ;
async initializeCamera ( retryCount = 0 ) : Promise < void > {
try {
const capabilities = this . platformService . getCapabilities ( ) ;
if ( ! capabilities . hasCamera ) {
this . danger ( "No camera available on this device." , "Camera Error" ) ;
return ;
/ / L o a d i n i t i a l d a t a
await this . loadInitialData ( ) ;
logger . log ( 'ContactQRScanShow initialization complete' ) ;
} catch ( error ) {
logger . error ( 'Failed to initialize ContactQRScanShow:' , error ) ;
this . showError ( 'Failed to initialize. Please try again.' ) ;
}
}
/ / C h e c k c a m e r a p e r m i s s i o n s
const hasPermission = await this . checkCameraPermission ( ) ;
if ( ! hasPermission ) {
this . danger (
"Camera permission is required to scan QR codes. Please enable camera access in your device settings." ,
"Permission Required"
) ;
return ;
private async loadInitialData ( ) {
try {
/ / L o a d s e t t i n g s f r o m D B
await db . open ( ) ;
const settings = await db . settings . get ( MASTER_SETTINGS_KEY ) ;
if ( settings ) {
this . hideRegisterPromptOnNewContact = settings . hideRegisterPromptOnNewContact || false ;
}
logger . log ( 'Initial data loaded successfully' ) ;
} catch ( error ) {
logger . error ( 'Failed to load initial data:' , error ) ;
throw error ;
}
}
/ / I f w e g e t h e r e , c a m e r a s h o u l d b e a v a i l a b l e
this . $notify (
{
group : "alert" ,
type : "success" ,
title : "Camera Ready" ,
text : "Camera is ready to scan QR codes." ,
} ,
3000
) ;
private cleanupCamera ( ) {
try {
this . cameraActive = false ;
this . isCapturingPhoto = false ;
this . addCameraState ( 'cleanup' ) ;
logger . log ( 'Camera cleaned up successfully' ) ;
} catch ( error ) {
logger . error ( "Error initializing camera:" , error ) ;
logger . error ( 'Error during camera cleanup:' , error ) ;
}
}
/ / R e t r y u p t o 3 t i m e s f o r c e r t a i n e r r o r s
if ( retryCount < 3 ) {
const isPermissionError = error instanceof Error &&
( error . message . includes ( "permission" ) ||
error . message . includes ( "NotReadableError" ) ) ;
private addCameraState ( state : CameraState | QRProcessingState , details ? : Record < string , unknown > ) {
const entry : CameraStateHistoryEntry = {
state ,
timestamp : Date . now ( ) ,
details
} ;
if ( isPermissionError ) {
/ / W a i t b e f o r e r e t r y i n g
await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
return this . initializeCamera ( retryCount + 1 ) ;
}
this . cameraStateHistory . push ( entry ) ;
if ( this . cameraStateHistory . length > this . STATE_HISTORY_LIMIT ) {
this . cameraStateHistory . shift ( ) ;
}
this . danger (
"Failed to initialize camera. Please check your camera permissions and try again." ,
"Camera Error"
) ;
/ / E n h a n c e d l o g g i n g w i t h b e t t e r d e t a i l s
logger . log ( 'Camera state transition:' , {
state ,
details : {
... details ,
cameraActive : this . cameraActive ,
isCapturingPhoto : this . isCapturingPhoto ,
historyLength : this . cameraStateHistory . length
}
} ) ;
this . lastCameraState = state ;
}
async checkCameraPermission ( ) : Promise < boolean > {
beforeDestroy ( ) {
logger . log ( 'ContactQRScanShow component being destroyed, initiating cleanup' , {
cameraActive : this . cameraActive ,
lastState : this . lastCameraState ,
stateHistory : this . cameraStateHistory
} ) ;
/ / R e m o v e a l l a p p l i f e c y c l e l i s t e n e r s
const cleanupListeners = async ( ) => {
if ( this . appStateListener ) {
try {
const capabilities = this . platformService . getCapabilities ( ) ;
if ( ! capabilities . hasCamera ) {
return false ;
await this . appStateListener . remove ( ) ;
logger . log ( 'App state change listener removed successfully' ) ;
} catch ( error ) {
logger . error ( 'Error removing app state change listener:' , error ) ;
}
}
/ / T r y t o a c c e s s c a m e r a t o c h e c k p e r m i s s i o n s
await this . platformService . takePicture ( ) ;
return true ;
try {
await App . removeAllListeners ( ) ;
logger . log ( 'All app listeners removed successfully' ) ;
} catch ( error ) {
logger . error ( "Camera permission check failed:" , error ) ;
return false ;
logger . error ( 'Error removing all app listeners:' , error ) ;
}
} ;
/ / C l e a n u p e v e r y t h i n g
Promise . all ( [
cleanupListeners ( ) ,
this . cleanupCamera ( )
] ) . catch ( error => {
logger . error ( 'Error during component cleanup:' , error ) ;
} ) ;
}
async openMobileCamera ( ) : Promise < void > {
private async handleQRCodeResult ( data : string ) : Promise < void > {
try {
/ / C h e c k p e r m i s s i o n s f i r s t
const hasPermission = await this . checkCameraPermission ( ) ;
if ( ! hasPermission ) {
this . danger (
"Camera permission is required. Please enable camera access in your device settings." ,
"Permission Required"
) ;
await this . onScanDetect ( [ { rawValue : data } ] ) ;
} catch ( error ) {
console . error ( 'Failed to handle QR code result:' , error ) ;
this . showError ( 'Failed to process QR code data.' ) ;
}
}
async initializeCamera ( retryCount = 0 ) : Promise < void > {
if ( this . cameraActive ) {
console . log ( 'Camera already active, skipping initialization' ) ;
return ;
}
const image = await Camera . getPhoto ( {
try {
console . log ( 'Initializing camera...' , { retryCount } ) ;
/ / C h e c k c a m e r a p e r m i s s i o n s f i r s t
const permissionStatus = await this . checkCameraPermission ( ) ;
if ( permissionStatus . camera !== 'granted' ) {
throw new Error ( 'Camera permission not granted' ) ;
}
this . cameraActive = true ;
/ / C o n f i g u r e c a m e r a o p t i o n s
const cameraOptions : ImageOptions = {
quality : 90 ,
allowEditing : false ,
resultType : CameraResultType . DataUrl ,
source : CameraSource . Camera ,
} ) ;
source : CameraSource . Camera
} ;
if ( image . dataUrl ) {
console . log ( 'Opening camera with options:' , cameraOptions ) ;
const image = await Camera . getPhoto ( cameraOptions ) ;
if ( ! image || ! image . dataUrl ) {
throw new Error ( 'Failed to capture photo: No image data received' ) ;
}
console . log ( 'Photo captured successfully, processing image...' ) ;
await this . processImageForQRCode ( image . dataUrl ) ;
} catch ( error ) {
this . cameraActive = false ;
console . error ( 'Camera initialization failed:' , error instanceof Error ? error . message : String ( error ) ) ;
/ / H a n d l e u s e r c a n c e l l a t i o n s e p a r a t e l y
if ( error instanceof Error && error . message . includes ( 'User cancelled photos app' ) ) {
console . log ( 'User cancelled photo capture' ) ;
return ;
}
/ / H a n d l e r e t r y l o g i c
if ( retryCount < 2 ) {
console . log ( 'Retrying camera initialization...' ) ;
await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
return this . initializeCamera ( retryCount + 1 ) ;
}
this . showError ( 'Failed to initialize camera. Please try again.' ) ;
}
}
async checkCameraPermission ( ) : Promise < { camera : CameraPermissionState } > {
try {
this . addCameraState ( 'checking_permissions' ) ;
const capabilities = this . platformService . getCapabilities ( ) ;
if ( ! capabilities . hasCamera ) {
this . addCameraState ( 'no_camera_capability' ) ;
return { camera : 'denied' as CameraPermissionState } ;
}
const permissionStatus = await Camera . checkPermissions ( ) ;
this . addCameraState ( 'permission_status_checked' ) ;
if ( permissionStatus . camera === 'prompt' ) {
this . addCameraState ( 'requesting_permission' ) ;
const requestResult = await Camera . requestPermissions ( ) ;
this . addCameraState ( 'permission_requested' ) ;
return requestResult ;
}
return permissionStatus ;
} catch ( error ) {
logger . error ( "Error taking picture:" , error ) ;
this . danger (
"Failed to access camera. Please check your camera permissions." ,
"Camera Error"
) ;
this . addCameraState ( 'permission_check_error' ) ;
return { camera : 'denied' as CameraPermissionState } ;
}
}
async processImageForQRCode ( _imageDataUrl : string ) {
async processImageForQRCode ( imageDataUrl : string ) : Promise < void > {
try {
logger . log ( 'Starting QR code processing' ) ;
/ / C r e a t e w o r k e r f o r i m a g e p r o c e s s i n g
const worker = new Worker ( URL . createObjectURL ( new Blob ( [ `
self . onmessage = async function ( e ) {
const { imageData , width , height } = e . data ;
try {
/ / H e r e y o u w o u l d i m p l e m e n t Q R c o d e s c a n n i n g f r o m t h e i m a g e
/ / F o r e x a m p l e , u s i n g j s Q R :
/ / c o n s t i m a g e = n e w I m a g e ( ) ;
/ / i m a g e . s r c = i m a g e D a t a U r l ;
/ / i m a g e . o n l o a d = ( ) = > {
/ / c o n s t c a n v a s = d o c u m e n t . c r e a t e E l e m e n t ( ' c a n v a s ' ) ;
/ / c o n s t c o n t e x t = c a n v a s . g e t C o n t e x t ( ' 2 d ' ) ;
/ / c a n v a s . w i d t h = i m a g e . w i d t h ;
/ / c a n v a s . h e i g h t = i m a g e . h e i g h t ;
/ / c o n t e x t . d r a w I m a g e ( i m a g e , 0 , 0 ) ;
/ / c o n s t i m a g e D a t a = c o n t e x t . g e t I m a g e D a t a ( 0 , 0 , c a n v a s . w i d t h , c a n v a s . h e i g h t ) ;
/ / c o n s t c o d e = j s Q R ( i m a g e D a t a . d a t a , i m a g e D a t a . w i d t h , i m a g e D a t a . h e i g h t ) ;
/ / i f ( c o d e ) {
/ / t h i s . o n S c a n D e t e c t ( [ { r a w V a l u e : c o d e . d a t a } ] ) ;
/ / }
/ / } ;
/ / I m p o r t j s Q R i n t h e w o r k e r
importScripts ( '${window.location.origin}/assets/jsqr.js' ) ;
const code = self . jsQR ( imageData , width , height , {
inversionAttempts : "dontInvert"
} ) ;
self . postMessage ( { success : true , code } ) ;
} catch ( error ) {
logger . error ( "Error processing image for QR code:" , error ) ;
this . danger (
"Failed to process the image. Please try again." ,
"Processing Error" ,
) ;
self . postMessage ( { success : false , error : error . message } ) ;
}
} ;
` ], { type: 'text/javascript' })));
const image = new Image ( ) ;
image . crossOrigin = 'anonymous' ;
image . src = imageDataUrl ;
await new Promise ( ( resolve , reject ) => {
const timeout = setTimeout ( ( ) => reject ( new Error ( 'Image load timeout' ) ) , 5000 ) ;
image . onload = ( ) => {
clearTimeout ( timeout ) ;
resolve ( undefined ) ;
} ;
image . onerror = ( ) => {
clearTimeout ( timeout ) ;
reject ( new Error ( 'Failed to load image' ) ) ;
} ;
} ) ;
logger . log ( 'Image loaded, creating canvas...' ) ;
const canvas = document . createElement ( 'canvas' ) ;
const maxDimension = 1024 ; / / L i m i t i m a g e s i z e f o r b e t t e r p e r f o r m a n c e
/ / S c a l e d o w n i m a g e i f n e e d e d w h i l e m a i n t a i n i n g a s p e c t r a t i o
let width = image . naturalWidth || 800 ;
let height = image . naturalHeight || 600 ;
if ( width > maxDimension || height > maxDimension ) {
if ( width > height ) {
height = Math . floor ( height * ( maxDimension / width ) ) ;
width = maxDimension ;
} else {
width = Math . floor ( width * ( maxDimension / height ) ) ;
height = maxDimension ;
}
}
canvas . width = width ;
canvas . height = height ;
const ctx = canvas . getContext ( '2d' ) ;
if ( ! ctx ) {
throw new Error ( 'Failed to get canvas context' ) ;
}
/ / D r a w i m a g e m a i n t a i n i n g o r i e n t a t i o n
ctx . save ( ) ;
ctx . drawImage ( image , 0 , 0 , width , height ) ;
ctx . restore ( ) ;
const imageData = ctx . getImageData ( 0 , 0 , width , height ) ;
logger . log ( 'Processing image data for QR code...' , {
width ,
height ,
dataLength : imageData . data . length
} ) ;
/ / P r o c e s s Q R c o d e i n w o r k e r
const result = await new Promise < QRCodeResult > ( ( resolve , reject ) => {
worker . onmessage = ( e : MessageEvent < WorkerMessage > ) => {
if ( e . data . success ) {
resolve ( e . data . code ) ;
} else {
reject ( new Error ( e . data . error ) ) ;
}
} ;
worker . onerror = reject ;
worker . postMessage ( {
imageData : imageData . data ,
width : imageData . width ,
height : imageData . height
} ) ;
} ) ;
worker . terminate ( ) ;
if ( result ) {
logger . log ( 'QR code found:' , { data : result . data } ) ;
await this . handleQRCodeResult ( result . data ) ;
} else {
logger . log ( 'No QR code found in image' ) ;
this . showError ( 'No QR code found. Please try again.' ) ;
}
} catch ( error ) {
logger . error ( 'QR code processing failed:' , error ) ;
this . showError ( 'Failed to process QR code. Please try again.' ) ;
} finally {
this . cameraActive = false ;
this . isCapturingPhoto = false ;
this . addCameraState ( 'processing_completed' ) ;
}
}
@ -712,5 +966,96 @@ export default class ContactQRScanShow extends Vue {
get isMobile ( ) : boolean {
return __IS_MOBILE__ ;
}
private showError ( message : string ) {
/ / I m p l e m e n t t h e l o g i c t o s h o w a u s e r - f r i e n d l y e r r o r m e s s a g e t o t h e u s e r
console . error ( message ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error" ,
text : message ,
} ,
5000 ,
) ;
}
async openMobileCamera ( ) {
if ( this . isCapturingPhoto ) {
this . addCameraState ( 'capture_already_in_progress' ) ;
logger . warn ( 'Camera capture already in progress, ignoring request' ) ;
return ;
}
try {
this . isCapturingPhoto = true ;
this . addCameraState ( 'opening_camera' ) ;
const config = {
quality : 90 ,
allowEditing : false ,
resultType : CameraResultType . DataUrl ,
source : CameraSource . Camera
} ;
logger . log ( 'Camera configuration:' , config ) ;
const image = await Camera . getPhoto ( config ) ;
this . addCameraState ( 'photo_captured' , {
hasDataUrl : ! ! image . dataUrl ,
dataUrlLength : image . dataUrl ? . length ,
format : image . format ,
saved : image . saved ,
webPath : image . webPath ,
base64String : image . dataUrl ? . substring ( 0 , 50 ) + '...' / / L o g f i r s t 5 0 c h a r s o f b a s e 6 4
} ) ;
if ( image . dataUrl ) {
this . addCameraState ( 'processing_photo' ) ;
await this . processImageForQRCode ( image . dataUrl ) ;
} else {
this . addCameraState ( 'no_image_data' ) ;
logger . error ( 'Camera returned no image data' ) ;
this . $notify ( {
type : 'error' ,
title : 'Camera Error' ,
text : 'No image was captured. Please try again.' ,
group : 'qr-scanner'
} ) ;
}
} catch ( error ) {
this . addCameraState ( 'capture_error' , {
error ,
errorName : error instanceof Error ? error . name : 'Unknown' ,
errorMessage : error instanceof Error ? error . message : String ( error ) ,
errorStack : error instanceof Error ? error . stack : undefined
} ) ;
if ( error instanceof Error ) {
if ( error . message . includes ( 'User cancelled photos app' ) ) {
logger . log ( 'User cancelled photo capture' ) ;
this . addCameraState ( 'user_cancelled' ) ;
} else if ( error . message . includes ( 'permission' ) ) {
logger . error ( 'Camera permission error during capture' ) ;
this . addCameraState ( 'permission_error' ) ;
} else if ( error . message . includes ( 'Camera is not available' ) ) {
logger . error ( 'Camera hardware not available' ) ;
this . addCameraState ( 'hardware_unavailable' ) ;
}
}
this . $notify ( {
type : 'error' ,
title : 'Camera Error' ,
text : 'Failed to capture photo. Please check camera permissions and try again.' ,
group : 'qr-scanner'
} ) ;
} finally {
this . isCapturingPhoto = false ;
this . addCameraState ( 'capture_completed' ) ;
}
}
}
< / script >