@ -116,7 +116,7 @@
< script lang = "ts" >
import { AxiosError } from "axios" ;
import QRCodeVue3 from "qr-code-generator-vue3" ;
import { Component , Vue , Ref } from "vue-facing-decorator" ;
import { Component , Vue } from "vue-facing-decorator" ;
import { QrcodeStream } from "vue-qrcode-reader" ;
import { PlatformServiceFactory } from "../services/PlatformServiceFactory" ;
import { PlatformService } from "../services/PlatformService" ;
@ -205,19 +205,6 @@ interface AppState {
scannerState : ScannerState ;
}
interface Barcode {
rawValue : string ;
bytes ? : number [ ] ;
}
interface MLKitScanResult {
barcodes : Barcode [ ] ;
}
interface WebQRResult {
rawValue : string ;
}
@ Component ( {
components : {
QRCodeVue3 ,
@ -229,11 +216,6 @@ interface WebQRResult {
export default class ContactQRScanShowView extends Vue {
$notify ! : ( notification : NotificationIface , timeout ? : number ) => void ;
$router ! : Router ;
@ Ref ( )
readonly userNameDialog ! : {
open : ( callback : ( name : string ) => void ) => void ;
} ;
declare $refs : {
userNameDialog : {
open : ( callback : ( name : string ) => void ) => void ;
@ -441,7 +423,42 @@ export default class ContactQRScanShowView extends Vue {
this . lastCameraState = state ;
}
private async openMobileCamera ( ) {
beforeDestroy ( ) {
logger . log (
"ContactQRScanShow component being destroyed, initiating cleanup" ,
) ;
/ / C l e a n u p s c a n n e r
this . stopScanning ( ) . catch ( ( error ) => {
logger . error ( "Error stopping scanner during destroy:" , error ) ;
} ) ;
/ / 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 {
await this . appStateListener . remove ( ) ;
logger . log ( "App state change listener removed successfully" ) ;
} catch ( error ) {
logger . error ( "Error removing app state change listener:" , error ) ;
}
}
try {
await App . removeAllListeners ( ) ;
logger . log ( "All app listeners removed successfully" ) ;
} catch ( error ) {
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 ( ) {
try {
this . state . isProcessing = true ;
this . state . processingStatus = "Starting camera..." ;
@ -449,7 +466,7 @@ export default class ContactQRScanShowView extends Vue {
/ / C h e c k c u r r e n t p e r m i s s i o n s t a t u s
const status = await BarcodeScanner . checkPermissions ( ) ;
logger . log ( "Camera permission status:" , status ) ;
logger . log ( "Camera permission status:" , JSON . stringify ( status , null , 2 ) ) ;
if ( status . camera !== "granted" ) {
/ / R e q u e s t p e r m i s s i o n i f n o t g r a n t e d
@ -458,32 +475,36 @@ export default class ContactQRScanShowView extends Vue {
if ( permissionStatus . camera !== "granted" ) {
throw new Error ( "Camera permission not granted" ) ;
}
logger . log ( "Camera permission granted:" , permissionStatus ) ;
logger . log (
"Camera permission granted:" ,
JSON . stringify ( permissionStatus , null , 2 ) ,
) ;
}
/ / 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 f i r s t
await this . cleanupScanListener ( ) ;
try {
if ( this . scanListener ) {
logger . log ( "Removing existing barcode listener" ) ;
await this . scanListener . remove ( ) ;
this . scanListener = null ;
}
} catch ( error ) {
logger . error ( "Error removing existing listener:" , error ) ;
/ / C o n t i n u e w i t h s e t u p e v e n i f r e m o v a l f a i l s
}
/ / S e t u p t h e l i s t e n e r b e f o r e s t a r t i n g t h e s c a n
logger . log ( "Setting up new barcode listener" ) ;
this . scanListener = await BarcodeScanner . addListener (
"barcodesScanned" ,
async ( result : ScanResult ) => {
try {
logger . log ( "Barcode scan result received:" , result ) ;
if ( result . barcodes && result . barcodes . length > 0 ) {
const barcode = result . barcodes [ 0 ] ;
if ( ! barcode . rawValue ) {
logger . warn ( "Received empty barcode value" ) ;
return ;
}
this . state . processingDetails = ` Processing QR code: ${ barcode . rawValue } ` ;
await this . handleScanResult ( barcode . rawValue ) ;
}
} catch ( error ) {
logger . error ( "Error processing barcode result:" , error ) ;
this . showError ( "Failed to process QR code" ) ;
logger . log (
"Barcode scan result received:" ,
JSON . stringify ( result , null , 2 ) ,
) ;
if ( result . barcodes && result . barcodes . length > 0 ) {
this . state . processingDetails = ` Processing QR code: ${ result . barcodes [ 0 ] . rawValue } ` ;
await this . handleScanResult ( result . barcodes [ 0 ] . rawValue ) ;
}
} ,
) ;
@ -506,19 +527,14 @@ export default class ContactQRScanShowView extends Vue {
) ;
/ / C l e a n u p o n e r r o r
await this . cleanupScanListener ( ) ;
}
}
private async cleanupScanListener ( ) : Promise < void > {
try {
if ( this . scanListener ) {
logger . log ( "Removing existing barcode listener" ) ;
await this . scanListener . remove ( ) ;
this . scanListener = null ;
try {
if ( this . scanListener ) {
await this . scanListener . remove ( ) ;
this . scanListener = null ;
}
} catch ( cleanupError ) {
logger . error ( "Error during cleanup:" , cleanupError ) ;
}
} catch ( error ) {
logger . error ( "Error removing barcode listener:" , error ) ;
}
}
@ -527,27 +543,15 @@ export default class ContactQRScanShowView extends Vue {
this . state . isProcessing = true ;
this . state . processingStatus = "Processing QR code..." ;
this . state . processingDetails = ` Scanned value: ${ rawValue } ` ;
logger . log ( "Processing scanned QR code:" , rawValue ) ;
/ / S t o p s c a n n i n g b e f o r e p r o c e s s i n g
await this . stopScanning ( ) ;
/ / V a l i d a t e U R L f o r m a t f i r s t
if ( ! rawValue . startsWith ( "http://" ) && ! rawValue . startsWith ( "https://" ) ) {
throw new Error (
"Invalid QR code format. Please scan a valid TimeSafari contact QR code." ,
) ;
}
/ / P r o c e s s t h e s c a n r e s u l t
await this . onScanDetect ( { rawValue } ) ;
} catch ( error ) {
logger . error ( "Error handling scan result:" , error ) ;
this . showError (
error instanceof Error
? error . message
: "Failed to process scan result" ,
) ;
this . showError ( "Failed to process scan result" ) ;
} finally {
this . state . isProcessing = false ;
this . state . processingStatus = "" ;
@ -590,110 +594,124 @@ export default class ContactQRScanShowView extends Vue {
}
/ * *
* Handle QR code scan result
* @ param content scan result from barcode scanner
*
* @ param content i s the result of a QR scan , an array with one item with a rawValue property
* /
async onScanDetect ( content : unknown ) : Promise < void > {
/ / E x t r a c t U R L f r o m d i f f e r e n t p o s s i b l e f o r m a t s
let url : string | null = null ;
if ( typeof content === "object" && content !== null ) {
/ / H a n d l e C a p a c i t o r M L K i t s c a n n e r f o r m a t
if (
"barcodes" in content &&
Array . isArray ( ( content as MLKitScanResult ) . barcodes )
) {
const mlkitResult = content as MLKitScanResult ;
url = mlkitResult . barcodes [ 0 ] ? . rawValue ;
}
/ / H a n d l e w e b Q R r e a d e r f o r m a t
else if ( Array . isArray ( content ) ) {
const webResult = content as WebQRResult [ ] ;
url = webResult [ 0 ] ? . rawValue ;
}
/ / H a n d l e d i r e c t o b j e c t f o r m a t
else if ( "rawValue" in content ) {
const directResult = content as WebQRResult ;
url = directResult . rawValue ;
}
/ / U n f o r t u n a t e l y , t h e r e a r e n o t t y p e s c r i p t d e f i n i t i o n s f o r t h e q r c o d e - s t r e a m c o m p o n e n t y e t .
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
async onScanDetect ( content : any ) : Promise < void > {
/ / L o g t h e r e c e i v e d c o n t e n t f o r d e b u g g i n g
logger . log ( "Scan result received:" , JSON . stringify ( content , null , 2 ) ) ;
/ / H a n d l e b o t h a r r a y f o r m a t a n d d i r e c t o b j e c t f o r m a t
let rawValue : string | undefined ;
if ( Array . isArray ( content ) ) {
rawValue = content [ 0 ] ? . rawValue ;
logger . log ( "Processing array format, rawValue:" , rawValue ) ;
} else if ( content ? . barcodes ? . [ 0 ] ? . rawValue ) {
rawValue = content . barcodes [ 0 ] . rawValue ;
logger . log ( "Processing barcodes array format, rawValue:" , rawValue ) ;
} else if ( content ? . rawValue ) {
rawValue = content . rawValue ;
logger . log ( "Processing object format, rawValue:" , rawValue ) ;
} else if ( typeof content === "string" ) {
rawValue = content ;
logger . log ( "Processing string format, rawValue:" , rawValue ) ;
}
/ / H a n d l e d i r e c t s t r i n g f o r m a t
else if ( typeof content === "string" ) {
url = content ;
if ( ! rawValue ) {
logger . error ( "No valid QR code content found in:" , content ) ;
this . danger ( "No QR code detected. Please try again." , "Scan Error" ) ;
return ;
}
if ( ! url ) {
logger . error ( "No valid QR code URL detected" ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Invalid Contact QR Code" ,
text : "No QR code detected with contact information." ,
} ,
5000 ,
/ / V a l i d a t e U R L f o r m a t f i r s t
if ( ! rawValue . startsWith ( "http://" ) && ! rawValue . startsWith ( "https://" ) ) {
logger . error ( "Invalid URL format:" , rawValue ) ;
this . danger (
"Invalid QR code format. Please scan a valid TimeSafari contact QR code." ,
"Invalid Format" ,
) ;
return ;
}
let newContact : Contact ;
try {
logger . log ( "Attempting to extract JWT from URL:" , url ) ;
const jwt = getContactJwtFromJwtUrl ( u rl) ;
/ / E x t r a c t J W T f r o m U R L
const jwt = getContactJwtFromJwtUrl ( rawVa lue ) ;
if ( ! jwt ) {
logger . error ( "Failed to extract JWT from URL" ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Invalid QR Code" ,
text : "Could not extract contact information from QR code." ,
} ,
3000 ,
logger . error ( "Failed to extract JWT from URL:" , rawValue ) ;
this . danger (
"Could not extract contact information from the QR code. Please try again." ,
"Invalid QR Code" ,
) ;
return ;
}
logger . log ( "Successfully extracted JWT, attempting to decode" ) ;
const { payload } = decodeEndorserJwt ( jwt ) ;
/ / L o g J W T f o r d e b u g g i n g
logger . log ( "Extracted JWT:" , jwt ) ;
/ / V a l i d a t e J W T f o r m a t
if (
! jwt . match ( /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/ )
) {
logger . error ( "Invalid JWT format:" , jwt ) ;
this . danger (
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code." ,
"Invalid Data" ,
) ;
return ;
}
const { payload } = decodeEndorserJwt ( jwt ) ;
if ( ! payload ) {
logger . error ( "JWT payload is null or undefined" ) ;
this . danger ( "Invalid JWT format" , "Contact Error" ) ;
logger . error ( "Failed to decode JWT payload" ) ;
this . danger (
"Could not decode the contact information. Please try again." ,
"Decode Error" ,
) ;
return ;
}
if ( ! payload . own ) {
logger . error ( "JWT payload missing 'own' property" ) ;
this . danger ( "Contact information is incomplete" , "Contact Error" ) ;
/ / L o g d e c o d e d p a y l o a d f o r d e b u g g i n g
logger . log ( "Decoded JWT payload:" , JSON . stringify ( payload , null , 2 ) ) ;
/ / V a l i d a t e r e q u i r e d f i e l d s
if ( ! payload . own && ! payload . iss ) {
logger . error ( "Missing required fields in payload:" , payload ) ;
this . danger (
"Missing required contact information. Please scan a valid TimeSafari contact QR code." ,
"Incomplete Data" ,
) ;
return ;
}
newContact = {
did : payload . own . did || payload . iss ,
name : payload . own . name || "" ,
nextPubKeyHashB64 : payload . own . nextPublicEncKeyHash || "" ,
profileImageUrl : payload . own . profileImageUrl || "" ,
publicKeyBase64 : payload . own . publicEncKey || "" ,
registered : payload . own . registered || false ,
did : payload . own ? . did || payload . iss ,
name : payload . own ? . name ,
nextPubKeyHashB64 : payload . own ? . nextPublicEncKeyHash ,
profileImageUrl : payload . own ? . profileImageUrl ,
publicKeyBase64 : payload . own ? . publicEncKey ,
registered : payload . own ? . registered ,
} ;
if ( ! newContact . did ) {
logger . error ( "Contact missing DID" ) ;
this . danger ( "Contact is missing identifier" , "Invalid Contact" ) ;
this . danger (
"Missing contact identifier. Please scan a valid TimeSafari contact QR code." ,
"Incomplete Contact" ,
) ;
return ;
}
if ( ! isDid ( newContact . did ) ) {
logger . error ( "Invalid DID format:" , newContact . did ) ;
this . danger ( "Invalid contact identifier format" , "Invalid Contact" ) ;
this . danger (
"Invalid contact identifier format. The identifier must begin with 'did:'." ,
"Invalid Identifier" ,
) ;
return ;
}
logger . log ( "Saving new contact:" , {
... newContact ,
publicKeyBase64 : "[REDACTED]" ,
} ) ;
await db . open ( ) ;
await db . contacts . add ( newContact ) ;
@ -754,15 +772,10 @@ export default class ContactQRScanShowView extends Vue {
} , 500 ) ;
}
} catch ( e ) {
logger . error ( "Error processing contact QR code:" , e ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Contact Error" ,
text : "Could not process contact information. Please try scanning again." ,
} ,
5000 ,
logger . error ( "Error processing QR code:" , e ) ;
this . danger (
"Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code." ,
"Processing Error" ,
) ;
}
}
@ -985,21 +998,5 @@ export default class ContactQRScanShowView extends Vue {
this . state . error = errorMessage ;
this . state . scannerState . error = errorMessage ;
}
async beforeDestroy ( ) {
logger . log (
"ContactQRScanShow component being destroyed, initiating cleanup" ,
) ;
/ / C l e a n u p s c a n n e r
await Promise . all ( [
this . stopScanning ( ) ,
this . cleanupScanListener ( ) ,
this . cleanupAppListeners ( ) ,
this . cleanupCamera ( ) ,
] ) . catch ( ( error ) => {
logger . error ( "Error during component cleanup:" , error ) ;
} ) ;
}
}
< / script >