@ -137,7 +137,7 @@ import {
BarcodeScanner ,
BarcodeScanner ,
type ScanResult ,
type ScanResult ,
} from "@capacitor-mlkit/barcode-scanning" ;
} from "@capacitor-mlkit/barcode-scanning" ;
import { ref , type Ref , reactive } from "vue" ;
import { ref , reactive } from "vue" ;
/ / D e c l a r e g l o b a l c o n s t a n t s
/ / 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 __USE_QR_READER__ : boolean ;
@ -241,7 +241,7 @@ export default class ContactQRScanShowView extends Vue {
private isCapturingPhoto = false ;
private isCapturingPhoto = false ;
private appStateListener ? : { remove : ( ) => Promise < void > } ;
private appStateListener ? : { remove : ( ) => Promise < void > } ;
private scanListener : Ref < PluginListenerHandle | null > = ref ( null ) ;
private scanListener : PluginListenerHandle | null = null ;
private state = reactive < AppState > ( {
private state = reactive < AppState > ( {
isProcessing : false ,
isProcessing : false ,
processingStatus : "" ,
processingStatus : "" ,
@ -307,7 +307,17 @@ export default class ContactQRScanShowView extends Vue {
this . appStateListener = await App . addListener (
this . appStateListener = await App . addListener (
"appStateChange" ,
"appStateChange" ,
( state : AppStateChangeEvent ) => {
( state : AppStateChangeEvent ) => {
logger . log ( "App state changed:" , state ) ;
const stateInfo = {
isActive : state . isActive ,
timestamp : new Date ( ) . toISOString ( ) ,
cameraActive : this . cameraActive ,
scannerState : {
... this . state . scannerState ,
/ / C o n v e r t c o m p l e x o b j e c t s t o s t r i n g s t o a v o i d [ o b j e c t O b j e c t ]
error : this . state . scannerState . error ? . toString ( ) || null ,
} ,
} ;
logger . log ( "App state changed:" , JSON . stringify ( stateInfo , null , 2 ) ) ;
if ( ! state . isActive ) {
if ( ! state . isActive ) {
this . cleanupCamera ( ) ;
this . cleanupCamera ( ) ;
}
}
@ -316,15 +326,31 @@ export default class ContactQRScanShowView extends Vue {
/ / A d d p a u s e l i s t e n e r
/ / A d d p a u s e l i s t e n e r
await App . addListener ( "pause" , ( ) => {
await App . addListener ( "pause" , ( ) => {
logger . log ( "App paused" ) ;
const pauseInfo = {
timestamp : new Date ( ) . toISOString ( ) ,
cameraActive : this . cameraActive ,
scannerState : {
... this . state . scannerState ,
error : this . state . scannerState . error ? . toString ( ) || null ,
} ,
isProcessing : this . state . isProcessing ,
} ;
logger . log ( "App paused:" , JSON . stringify ( pauseInfo , null , 2 ) ) ;
this . cleanupCamera ( ) ;
this . cleanupCamera ( ) ;
} ) ;
} ) ;
/ / A d d r e s u m e l i s t e n e r
/ / A d d r e s u m e l i s t e n e r
await App . addListener ( "resume" , ( ) => {
await App . addListener ( "resume" , ( ) => {
logger . log ( "App resumed" ) ;
const resumeInfo = {
/ / D o n ' t a u t o m a t i c a l l y r e i n i t i a l i z e c a m e r a o n r e s u m e
timestamp : new Date ( ) . toISOString ( ) ,
/ / L e t u s e r e x p l i c i t l y r e q u e s t c a m e r a a c c e s s a g a i n
cameraActive : this . cameraActive ,
scannerState : {
... this . state . scannerState ,
error : this . state . scannerState . error ? . toString ( ) || null ,
} ,
isProcessing : this . state . isProcessing ,
} ;
logger . log ( "App resumed:" , JSON . stringify ( resumeInfo , null , 2 ) ) ;
} ) ;
} ) ;
logger . log ( "App lifecycle listeners setup complete" ) ;
logger . log ( "App lifecycle listeners setup complete" ) ;
@ -340,7 +366,7 @@ export default class ContactQRScanShowView extends Vue {
await db . open ( ) ;
await db . open ( ) ;
const settings = await db . settings . get ( MASTER_SETTINGS_KEY ) ;
const settings = await db . settings . get ( MASTER_SETTINGS_KEY ) ;
if ( settings ) {
if ( settings ) {
this . hideRegisterPromptOnNewContact =
this . hideRegisterPromptOnNewContact =
settings . hideRegisterPromptOnNewContact || false ;
settings . hideRegisterPromptOnNewContact || false ;
}
}
logger . log ( "Initial data loaded successfully" ) ;
logger . log ( "Initial data loaded successfully" ) ;
@ -436,41 +462,59 @@ export default class ContactQRScanShowView extends Vue {
try {
try {
this . state . isProcessing = true ;
this . state . isProcessing = true ;
this . state . processingStatus = "Starting camera..." ;
this . state . processingStatus = "Starting camera..." ;
logger . log ( "Opening mobile camera - starting initialization" ) ;
/ / 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
/ / 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 ( ) ;
const status = await BarcodeScanner . checkPermissions ( ) ;
logger . log ( "Camera permission status:" , JSON . stringify ( status , null , 2 ) ) ;
if ( status . camera !== "granted" ) {
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
/ / 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
logger . log ( "Requesting camera permissions..." ) ;
const permissionStatus = await BarcodeScanner . requestPermissions ( ) ;
const permissionStatus = await BarcodeScanner . requestPermissions ( ) ;
if ( permissionStatus . camera !== "granted" ) {
if ( permissionStatus . camera !== "granted" ) {
throw new Error ( "Camera permission not granted" ) ;
throw new Error ( "Camera permission not granted" ) ;
}
}
logger . log (
"Camera permission granted:" ,
JSON . stringify ( permissionStatus , null , 2 ) ,
) ;
}
}
/ / 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
/ / 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
try {
try {
const listener = await BarcodeScanner . addListener (
if ( this . scanListener ) {
"barcodesScanned" ,
logger . log ( "Removing existing barcode listener" ) ;
async ( result : ScanResult ) => {
await this . scanListener . remove ( ) ;
if ( result . barcodes && result . barcodes . length > 0 ) {
this . scanListener = null ;
this . state . processingDetails = ` Processing QR code: ${ result . barcodes [ 0 ] . rawValue } ` ;
await this . handleScanResult ( result . barcodes [ 0 ] . rawValue ) ;
}
} ,
) ;
/ / O n l y s e t t h e l i s t e n e r i f w e s u c c e s s f u l l y g o t o n e
if ( listener ) {
this . scanListener . value = listener ;
}
}
} catch ( error ) {
} catch ( error ) {
logger . error ( "Error setting up barcode listener:" , error ) ;
logger . error ( "Error removing existing listener:" , error ) ;
throw new Error ( "Failed to initialize barcode scanner" ) ;
/ / 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 ) => {
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 ) ;
}
} ,
) ;
logger . log ( "Barcode listener setup complete" ) ;
/ / S t a r t t h e s c a n n e r
/ / S t a r t t h e s c a n n e r
logger . log ( "Starting barcode scanner" ) ;
await BarcodeScanner . startScan ( ) ;
await BarcodeScanner . startScan ( ) ;
logger . log ( "Barcode scanner started successfully" ) ;
this . state . isProcessing = false ;
this . state . isProcessing = false ;
this . state . processingStatus = "" ;
this . state . processingStatus = "" ;
} catch ( error ) {
} catch ( error ) {
@ -481,19 +525,37 @@ export default class ContactQRScanShowView extends Vue {
this . showError (
this . showError (
error instanceof Error ? error . message : "Failed to open camera" ,
error instanceof Error ? error . message : "Failed to open camera" ,
) ;
) ;
/ / C l e a n u p o n e r r o r
try {
if ( this . scanListener ) {
await this . scanListener . remove ( ) ;
this . scanListener = null ;
}
} catch ( cleanupError ) {
logger . error ( "Error during cleanup:" , cleanupError ) ;
}
}
}
}
}
private async handleScanResult ( rawValue : string ) {
private async handleScanResult ( rawValue : string ) {
this . state . isProcessing = true ;
this . state . processingStatus = "Processing QR code..." ;
this . state . processingDetails = ` Scanned value: ${ rawValue } ` ;
try {
try {
this . state . isProcessing = true ;
this . state . processingStatus = "Processing QR code..." ;
this . state . processingDetails = ` Scanned value: ${ 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 ( ) ;
await this . stopScanning ( ) ;
/ / P r o c e s s t h e s c a n r e s u l t
await this . onScanDetect ( { rawValue } ) ;
await this . onScanDetect ( { rawValue } ) ;
} catch ( error ) {
} catch ( error ) {
logger . error ( "Error handling scan result:" , error ) ;
logger . error ( "Error handling scan result:" , error ) ;
this . showError ( "Failed to process scan result" ) ;
this . showError ( "Failed to process scan result" ) ;
} finally {
this . state . isProcessing = false ;
this . state . processingStatus = "" ;
this . state . processingDetails = "" ;
}
}
}
}
@ -553,11 +615,11 @@ export default class ContactQRScanShowView extends Vue {
return ;
return ;
}
}
let newContact : Contact ;
let newContact : Contact ;
try {
try {
/ / E x t r a c t J W T f r o m U R L
/ / E x t r a c t J W T f r o m U R L
const jwt = getContactJwtFromJwtUrl ( url ) ;
const jwt = getContactJwtFromJwtUrl ( url ) ;
if ( ! jwt ) {
if ( ! jwt ) {
this . danger (
this . danger (
"Could not extract contact information from the QR code. Please try again." ,
"Could not extract contact information from the QR code. Please try again." ,
"Invalid QR Code" ,
"Invalid QR Code" ,
@ -572,11 +634,11 @@ export default class ContactQRScanShowView extends Vue {
this . danger (
this . danger (
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code." ,
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code." ,
"Invalid Data" ,
"Invalid Data" ,
) ;
) ;
return ;
return ;
}
}
const { payload } = decodeEndorserJwt ( jwt ) ;
const { payload } = decodeEndorserJwt ( jwt ) ;
if ( ! payload ) {
if ( ! payload ) {
this . danger (
this . danger (
"Could not decode the contact information. Please try again." ,
"Could not decode the contact information. Please try again." ,
@ -594,7 +656,7 @@ export default class ContactQRScanShowView extends Vue {
return ;
return ;
}
}
newContact = {
newContact = {
did : payload . own ? . did || payload . iss ,
did : payload . own ? . did || payload . iss ,
name : payload . own ? . name ,
name : payload . own ? . name ,
nextPubKeyHashB64 : payload . own ? . nextPublicEncKeyHash ,
nextPubKeyHashB64 : payload . own ? . nextPublicEncKeyHash ,
@ -603,15 +665,15 @@ export default class ContactQRScanShowView extends Vue {
registered : payload . own ? . registered ,
registered : payload . own ? . registered ,
} ;
} ;
if ( ! newContact . did ) {
if ( ! newContact . did ) {
this . danger (
this . danger (
"Missing contact identifier. Please scan a valid TimeSafari contact QR code." ,
"Missing contact identifier. Please scan a valid TimeSafari contact QR code." ,
"Incomplete Contact" ,
"Incomplete Contact" ,
) ;
) ;
return ;
return ;
}
}
if ( ! isDid ( newContact . did ) ) {
if ( ! isDid ( newContact . did ) ) {
this . danger (
this . danger (
"Invalid contact identifier format. The identifier must begin with 'did:'." ,
"Invalid contact identifier format. The identifier must begin with 'did:'." ,
"Invalid Identifier" ,
"Invalid Identifier" ,
@ -619,66 +681,66 @@ export default class ContactQRScanShowView extends Vue {
return ;
return ;
}
}
await db . open ( ) ;
await db . open ( ) ;
await db . contacts . add ( newContact ) ;
await db . contacts . add ( newContact ) ;
let addedMessage ;
let addedMessage ;
if ( this . activeDid ) {
if ( this . activeDid ) {
await this . setVisibility ( newContact , true ) ;
await this . setVisibility ( newContact , true ) ;
newContact . seesMe = true ;
newContact . seesMe = true ;
addedMessage = "They were added, and your activity is visible to them." ;
addedMessage = "They were added, and your activity is visible to them." ;
} else {
} else {
addedMessage = "They were added." ;
addedMessage = "They were added." ;
}
}
this . $notify (
this . $notify (
{
{
group : "alert" ,
group : "alert" ,
type : "success" ,
type : "success" ,
title : "Contact Added" ,
title : "Contact Added" ,
text : addedMessage ,
text : addedMessage ,
} ,
} ,
3000 ,
3000 ,
) ;
) ;
if (
if (
this . isRegistered &&
this . isRegistered &&
! this . hideRegisterPromptOnNewContact &&
! this . hideRegisterPromptOnNewContact &&
! newContact . registered
! newContact . registered
) {
) {
setTimeout ( ( ) => {
setTimeout ( ( ) => {
this . $notify (
this . $notify (
{
{
group : "modal" ,
group : "modal" ,
type : "confirm" ,
type : "confirm" ,
title : "Register" ,
title : "Register" ,
text : "Do you want to register them?" ,
text : "Do you want to register them?" ,
onCancel : async ( stopAsking ? : boolean ) => {
onCancel : async ( stopAsking ? : boolean ) => {
if ( stopAsking ) {
if ( stopAsking ) {
await db . settings . update ( MASTER_SETTINGS_KEY , {
await db . settings . update ( MASTER_SETTINGS_KEY , {
hideRegisterPromptOnNewContact : stopAsking ,
hideRegisterPromptOnNewContact : stopAsking ,
} ) ;
} ) ;
this . hideRegisterPromptOnNewContact = stopAsking ;
this . hideRegisterPromptOnNewContact = stopAsking ;
}
}
} ,
} ,
onNo : async ( stopAsking ? : boolean ) => {
onNo : async ( stopAsking ? : boolean ) => {
if ( stopAsking ) {
if ( stopAsking ) {
await db . settings . update ( MASTER_SETTINGS_KEY , {
await db . settings . update ( MASTER_SETTINGS_KEY , {
hideRegisterPromptOnNewContact : stopAsking ,
hideRegisterPromptOnNewContact : stopAsking ,
} ) ;
} ) ;
this . hideRegisterPromptOnNewContact = stopAsking ;
this . hideRegisterPromptOnNewContact = stopAsking ;
}
}
} ,
} ,
onYes : async ( ) => {
onYes : async ( ) => {
await this . register ( newContact ) ;
await this . register ( newContact ) ;
} ,
} ,
promptToStopAsking : true ,
promptToStopAsking : true ,
} ,
} ,
- 1 ,
- 1 ,
) ;
) ;
} , 500 ) ;
} , 500 ) ;
}
}
} catch ( e ) {
} catch ( e ) {
logger . error ( "Error processing QR code:" , e ) ;
logger . error ( "Error processing QR code:" , e ) ;
this . danger (
this . danger (
"Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code." ,
"Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code." ,
@ -795,15 +857,15 @@ export default class ContactQRScanShowView extends Vue {
async onCopyUrlToClipboard ( ) : Promise < void > {
async onCopyUrlToClipboard ( ) : Promise < void > {
try {
try {
await this . platformService . writeToClipboard ( this . qrValue ) ;
await this . platformService . writeToClipboard ( this . qrValue ) ;
this . $notify (
this . $notify (
{
{
group : "alert" ,
group : "alert" ,
type : "toast" ,
type : "toast" ,
title : "Copied" ,
title : "Copied" ,
text : "Contact URL was copied to clipboard." ,
text : "Contact URL was copied to clipboard." ,
} ,
} ,
2000 ,
2000 ,
) ;
) ;
} catch ( error ) {
} catch ( error ) {
logger . error ( "Error copying to clipboard:" , error ) ;
logger . error ( "Error copying to clipboard:" , error ) ;
this . danger ( "Failed to copy to clipboard" , "Error" ) ;
this . danger ( "Failed to copy to clipboard" , "Error" ) ;
@ -813,15 +875,15 @@ export default class ContactQRScanShowView extends Vue {
async onCopyDidToClipboard ( ) : Promise < void > {
async onCopyDidToClipboard ( ) : Promise < void > {
try {
try {
await this . platformService . writeToClipboard ( this . activeDid ) ;
await this . platformService . writeToClipboard ( this . activeDid ) ;
this . $notify (
this . $notify (
{
{
group : "alert" ,
group : "alert" ,
type : "info" ,
type : "info" ,
title : "Copied" ,
title : "Copied" ,
text : "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you." ,
text : "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you." ,
} ,
} ,
5000 ,
5000 ,
) ;
) ;
} catch ( error ) {
} catch ( error ) {
logger . error ( "Error copying to clipboard:" , error ) ;
logger . error ( "Error copying to clipboard:" , error ) ;
this . danger ( "Failed to copy to clipboard" , "Error" ) ;
this . danger ( "Failed to copy to clipboard" , "Error" ) ;
@ -880,6 +942,13 @@ export default class ContactQRScanShowView extends Vue {
async stopScanning ( ) {
async stopScanning ( ) {
try {
try {
/ / R e m o v e t h e l i s t e n e r f i r s t
if ( this . scanListener ) {
await this . scanListener . remove ( ) ;
this . scanListener = null ;
}
/ / S t o p t h e s c a n n e r
await BarcodeScanner . stopScan ( ) ;
await BarcodeScanner . stopScan ( ) ;
this . state . scannerState . processingStatus = "Scan stopped" ;
this . state . scannerState . processingStatus = "Scan stopped" ;
this . state . scannerState . isProcessing = false ;
this . state . scannerState . isProcessing = false ;
@ -889,6 +958,7 @@ export default class ContactQRScanShowView extends Vue {
error instanceof Error ? error . message : String ( error ) ;
error instanceof Error ? error . message : String ( error ) ;
this . state . scannerState . error = ` Error stopping scan: ${ errorMessage } ` ;
this . state . scannerState . error = ` Error stopping scan: ${ errorMessage } ` ;
this . state . scannerState . isProcessing = false ;
this . state . scannerState . isProcessing = false ;
logger . error ( "Error stopping scanner:" , error ) ;
}
}
}
}