@ -78,7 +78,9 @@
< div class = "text-center" >
< h1 class = "text-4xl text-center font-light pt-6" > Scan Contact Info < / h1 >
< div v-if ="isScanning" class="relative aspect-square" >
< div class = "absolute inset-0 border-2 border-blue-500 opacity-50 pointer-events-none" > < / div >
< div
class = "absolute inset-0 border-2 border-blue-500 opacity-50 pointer-events-none"
> < / div >
< / div >
< div v-else >
< button
@ -122,6 +124,15 @@ import { Router } from "vue-router";
import { logger } from "../utils/logger" ;
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory" ;
interface QRScanResult {
rawValue ? : string ;
barcode ? : string ;
}
interface IUserNameDialog {
open : ( callback : ( name : string ) => void ) => void ;
}
@ Component ( {
components : {
QRCodeVue3 ,
@ -144,6 +155,11 @@ export default class ContactQRScanShow extends Vue {
ETHR_DID_PREFIX = ETHR_DID_PREFIX ;
/ / A d d n e w p r o p e r t i e s t o t r a c k s c a n n i n g s t a t e
private lastScannedValue : string = "" ;
private lastScanTime : number = 0 ;
private readonly SCAN_DEBOUNCE_MS = 2000 ; / / P r e v e n t d u p l i c a t e s c a n s w i t h i n 2 s e c o n d s
async created ( ) {
const settings = await retrieveSettingsForActiveAccount ( ) ;
this . activeDid = settings . activeDid || "" ;
@ -173,6 +189,8 @@ export default class ContactQRScanShow extends Vue {
try {
this . error = null ;
this . isScanning = true ;
this . lastScannedValue = "" ;
this . lastScanTime = 0 ;
const scanner = QRScannerFactory . getInstance ( ) ;
@ -189,7 +207,7 @@ export default class ContactQRScanShow extends Vue {
/ / A d d s c a n l i s t e n e r
scanner . addListener ( {
onScan : this . onScanDetect ,
onError : this . onScanError
onError : this . onScanError ,
} ) ;
/ / S t a r t s c a n n i n g
@ -205,10 +223,11 @@ export default class ContactQRScanShow extends Vue {
try {
const scanner = QRScannerFactory . getInstance ( ) ;
await scanner . stopScan ( ) ;
this . isScanning = false ;
this . lastScannedValue = "" ;
this . lastScanTime = 0 ;
} catch ( error ) {
logger . error ( "Error stopping scan:" , error ) ;
} finally {
this . isScanning = false ;
}
}
@ -225,116 +244,104 @@ export default class ContactQRScanShow extends Vue {
}
/ * *
* Handle QR code scan result
* Handle QR code scan result with debouncing to prevent duplicate scans
* /
async onScanDetect ( result : string ) {
async onScanDetect ( result : string | QRScanResult ) {
try {
let newContact : Contact ;
const jwt = getContactJwtFromJwtUrl ( result ) ;
if ( ! jwt ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "No Contact Info" ,
text : "The contact info could not be parsed." ,
} ,
3000 ,
) ;
/ / E x t r a c t r a w v a l u e 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
const rawValue = typeof result === 'string' ? result : ( result ? . rawValue || result ? . barcode ) ;
if ( ! rawValue ) {
logger . warn ( "Invalid scan result - no value found:" , result ) ;
return ;
}
const { payload } = decodeEndorserJwt ( jwt ) ;
newContact = {
did : payload . own . did || payload . iss , / / " . o w n . d i d " i s r e l i a b l e a s o f v 0 . 3 . 4 9
name : payload . own . name ,
nextPubKeyHashB64 : payload . own . nextPublicEncKeyHash ,
profileImageUrl : payload . own . profileImageUrl ,
} ;
if ( ! newContact . did ) {
this . danger ( "There is no DID." , "Incomplete Contact" ) ;
return ;
}
if ( ! isDid ( newContact . did ) ) {
this . danger ( "The DID must begin with 'did:'" , "Invalid DID" ) ;
/ / D e b o u n c e d u p l i c a t e s c a n s
const now = Date . now ( ) ;
if (
rawValue === this . lastScannedValue &&
now - this . lastScanTime < this . SCAN_DEBOUNCE_MS
) {
logger . info ( "Ignoring duplicate scan:" , rawValue ) ;
return ;
}
try {
await db . open ( ) ;
await db . contacts . add ( newContact ) ;
/ / U p d a t e s c a n t r a c k i n g
this . lastScannedValue = rawValue ;
this . lastScanTime = now ;
let addedMessage ;
if ( this . activeDid ) {
await this . setVisibility ( newContact , true ) ;
newContact . seesMe = true ; / / d i d n ' t w o r k i n s i d e s e t V i s i b i l i t y
addedMessage =
"They were added, and your activity is visible to them." ;
} else {
addedMessage = "They were added." ;
}
this . $notify (
{
group : "alert" ,
type : "success" ,
title : "Contact Added" ,
text : addedMessage ,
} ,
3000 ,
) ;
logger . info ( "Processing QR code scan result:" , rawValue ) ;
if ( this . isRegistered ) {
if ( ! this . hideRegisterPromptOnNewContact && ! newContact . registered ) {
setTimeout ( ( ) => {
this . $notify (
{
group : "modal" ,
type : "confirm" ,
title : "Register" ,
text : "Do you want to register them?" ,
onCancel : async ( stopAsking ? : boolean ) => {
if ( stopAsking ) {
await db . settings . update ( MASTER_SETTINGS_KEY , {
hideRegisterPromptOnNewContact : stopAsking ,
/ / E x t r a c t J W T
const jwt = getContactJwtFromJwtUrl ( rawValue ) ;
if ( ! jwt ) {
logger . warn ( "Invalid QR code format - no JWT found in URL" ) ;
this . $notify ( {
group : "alert" ,
type : "danger" ,
title : "Invalid QR Code" ,
text : "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code." ,
} ) ;
this . hideRegisterPromptOnNewContact = stopAsking ;
return ;
}
} ,
onNo : async ( stopAsking ? : boolean ) => {
if ( stopAsking ) {
await db . settings . update ( MASTER_SETTINGS_KEY , {
hideRegisterPromptOnNewContact : stopAsking ,
/ / P r o c e s s J W T a n d c o n t a c t i n f o
logger . info ( "Decoding JWT payload from QR code" ) ;
const decodedJwt = await decodeEndorserJwt ( jwt ) ;
if ( ! decodedJwt ? . payload ? . own ) {
logger . warn ( "Invalid JWT payload - missing 'own' field" ) ;
this . $notify ( {
group : "alert" ,
type : "danger" ,
title : "Invalid Contact Info" ,
text : "The contact information is incomplete or invalid." ,
} ) ;
this . hideRegisterPromptOnNewContact = stopAsking ;
}
} ,
onYes : async ( ) => {
await this . register ( newContact ) ;
} ,
promptToStopAsking : true ,
} ,
- 1 ,
) ;
} , 500 ) ;
}
return ;
}
} catch ( e ) {
logger . error ( "Error saving contact info:" , e ) ;
this . $notify (
{
const contactInfo = decodedJwt . payload . own ;
if ( ! contactInfo . did ) {
logger . warn ( "Invalid contact info - missing DID" ) ;
this . $notify ( {
group : "alert" ,
type : "danger" ,
title : "Contact Error" ,
text : "Could not save contact info. Check if it already exists." ,
} ,
5000 ,
) ;
title : "Invalid Contact" ,
text : "The contact DID is missing." ,
} ) ;
return ;
}
/ / S t o p s c a n n i n g a f t e r s u c c e s s f u l s c a n
/ / C r e a t e c o n t a c t o b j e c t
const contact = {
did : contactInfo . did ,
name : contactInfo . name || "" ,
email : contactInfo . email || "" ,
phone : contactInfo . phone || "" ,
company : contactInfo . company || "" ,
title : contactInfo . title || "" ,
notes : contactInfo . notes || "" ,
} ;
/ / A d d c o n t a c t a n d s t o p s c a n n i n g
logger . info ( "Adding new contact to database:" , {
did : contact . did ,
name : contact . name ,
} ) ;
await this . addNewContact ( contact ) ;
await this . stopScanning ( ) ;
} catch ( error ) {
this . error = error instanceof Error ? error . message : String ( error ) ;
logger . error ( "Error processing scan result:" , error ) ;
logger . error ( "Error processing contact QR code:" , {
error : error instanceof Error ? error . message : String ( error ) ,
stack : error instanceof Error ? error . stack : undefined ,
} ) ;
this . $notify ( {
group : "alert" ,
type : "danger" ,
title : "Error" ,
text :
error instanceof Error
? error . message
: "Could not process QR code. Please try again." ,
} ) ;
}
}
@ -350,11 +357,15 @@ export default class ContactQRScanShow extends Vue {
if ( result . error ) {
this . danger ( result . error as string , "Error Setting Visibility" ) ;
} else if ( ! result . success ) {
logger . error ( "Got strange result from setting visibility:", result ) ;
logger . warn ( "Unexpected result from setting visibility:", result ) ;
}
}
async register ( contact : Contact ) {
logger . info ( "Submitting contact registration" , {
did : contact . did ,
name : contact . name ,
} ) ;
this . $notify (
{
group : "alert" ,
@ -375,6 +386,7 @@ export default class ContactQRScanShow extends Vue {
if ( regResult . success ) {
contact . registered = true ;
db . contacts . update ( contact . did , { registered : true } ) ;
logger . info ( "Contact registration successful" , { did : contact . did } ) ;
this . $notify (
{
@ -400,12 +412,21 @@ export default class ContactQRScanShow extends Vue {
) ;
}
} catch ( error ) {
logger . error ( "Error when registering:" , error ) ;
logger . error ( "Error registering contact:" , {
did : contact . did ,
error : error instanceof Error ? error . message : String ( error ) ,
stack : error instanceof Error ? error . stack : undefined ,
} ) ;
let userMessage = "There was an error." ;
const serverError = error as AxiosError ;
if ( serverError ) {
if ( serverError . response ? . data && typeof serverError . response . data === 'object' && 'message' in serverError . response . data ) {
userMessage = ( serverError . response . data as { message : string } ) . message ;
if (
serverError . response ? . data &&
typeof serverError . response . data === "object" &&
"message" in serverError . response . data
) {
userMessage = ( serverError . response . data as { message : string } )
. message ;
} else if ( serverError . message ) {
userMessage = serverError . message ; / / I n f o f o r t h e u s e r
} else {
@ -429,7 +450,10 @@ export default class ContactQRScanShow extends Vue {
onScanError ( error : Error ) {
this . error = error . message ;
logger . error ( "Scan error:" , error ) ;
logger . error ( "QR code scan error:" , {
error : error . message ,
stack : error . stack ,
} ) ;
}
onCopyUrlToClipboard ( ) {
@ -468,15 +492,117 @@ export default class ContactQRScanShow extends Vue {
}
openUserNameDialog ( ) {
( this . $refs . userNameDialog as any ) . open ( ( name : string ) => {
( this . $refs . userNameDialog as IUserNameDialog ) . open ( ( name : string ) => {
this . givenName = name ;
} ) ;
}
beforeDestroy ( ) {
/ / C l e a n u p s c a n n e r w h e n c o m p o n e n t i s d e s t r o y e d
logger . info ( "Cleaning up QR scanner resources" ) ;
this . stopScanning ( ) ; / / E n s u r e s c a n n e r i s s t o p p e d
QRScannerFactory . cleanup ( ) ;
}
async addNewContact ( contact : Contact ) {
try {
logger . info ( "Opening database connection for new contact" ) ;
await db . open ( ) ;
await db . contacts . add ( contact ) ;
if ( this . activeDid ) {
logger . info ( "Setting contact visibility" , { did : contact . did } ) ;
await this . setVisibility ( contact , true ) ;
contact . seesMe = true ;
}
this . $notify (
{
group : "alert" ,
type : "success" ,
title : "Contact Added" ,
text : this . activeDid
? "They were added, and your activity is visible to them."
: "They were added." ,
} ,
3000 ,
) ;
if (
this . isRegistered &&
! this . hideRegisterPromptOnNewContact &&
! contact . registered
) {
setTimeout ( ( ) => {
this . $notify (
{
group : "modal" ,
type : "confirm" ,
title : "Register" ,
text : "Do you want to register them?" ,
onCancel : async ( stopAsking ? : boolean ) => {
if ( stopAsking ) {
await db . settings . update ( MASTER_SETTINGS_KEY , {
hideRegisterPromptOnNewContact : stopAsking ,
} ) ;
this . hideRegisterPromptOnNewContact = stopAsking ;
}
} ,
onNo : async ( stopAsking ? : boolean ) => {
if ( stopAsking ) {
await db . settings . update ( MASTER_SETTINGS_KEY , {
hideRegisterPromptOnNewContact : stopAsking ,
} ) ;
this . hideRegisterPromptOnNewContact = stopAsking ;
}
} ,
onYes : async ( ) => {
await this . register ( contact ) ;
} ,
promptToStopAsking : true ,
} ,
- 1 ,
) ;
} , 500 ) ;
}
} catch ( error ) {
logger . error ( "Error saving contact to database:" , {
did : contact . did ,
error : error instanceof Error ? error . message : String ( error ) ,
stack : error instanceof Error ? error . stack : undefined ,
} ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Contact Error" ,
text : "Could not save contact. Check if it already exists." ,
} ,
5000 ,
) ;
}
}
/ / A d d p a u s e / r e s u m e h a n d l e r s f o r m o b i l e
mounted ( ) {
document . addEventListener ( "pause" , this . handleAppPause ) ;
document . addEventListener ( "resume" , this . handleAppResume ) ;
}
beforeUnmount ( ) {
document . removeEventListener ( "pause" , this . handleAppPause ) ;
document . removeEventListener ( "resume" , this . handleAppResume ) ;
}
handleAppPause ( ) {
logger . info ( "App paused, stopping scanner" ) ;
this . stopScanning ( ) ;
}
handleAppResume ( ) {
logger . info ( "App resumed, scanner can be restarted by user" ) ;
/ / D o n ' t a u t o - r e s t a r t s c a n n i n g - l e t u s e r i n i t i a t e i t
this . isScanning = false ;
}
}
< / script >