3 changed files with 434 additions and 3 deletions
@ -0,0 +1,426 @@ |
|||||
|
<template> |
||||
|
<!-- CONTENT --> |
||||
|
<section id="Content" class="relativew-[100vw] h-[100vh]"> |
||||
|
<div class="absolute inset-x-0 bottom-0 bg-black/50 p-6"> |
||||
|
<p class="text-center text-white mb-3"> |
||||
|
Point your camera at a TimeSafari contact QR code to scan it automatically. |
||||
|
</p> |
||||
|
|
||||
|
<p v-if="error" class="text-center text-rose-300 mb-3">{{ error }}</p> |
||||
|
|
||||
|
<div class="text-center"> |
||||
|
<button |
||||
|
class="text-center text-white leading-none bg-slate-400 p-2 rounded-full" |
||||
|
@click="$router.back()" |
||||
|
> |
||||
|
<font-awesome icon="xmark" class="w-[1em]"></font-awesome> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="text-center"> |
||||
|
|
||||
|
</div> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from "vue-facing-decorator"; |
||||
|
import { Router } from "vue-router"; |
||||
|
import { logger } from "../utils/logger"; |
||||
|
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory"; |
||||
|
import QuickNav from "../components/QuickNav.vue"; |
||||
|
import { NotificationIface } from "../constants/app"; |
||||
|
import { db } from "../db/index"; |
||||
|
import { Contact } from "../db/tables/contacts"; |
||||
|
import { getContactJwtFromJwtUrl } from "../libs/crypto"; |
||||
|
import { decodeEndorserJwt } from "../libs/crypto/vc"; |
||||
|
import { retrieveSettingsForActiveAccount } from "../db/index"; |
||||
|
import { setVisibilityUtil } from "../libs/endorserServer"; |
||||
|
|
||||
|
interface QRScanResult { |
||||
|
rawValue?: string; |
||||
|
barcode?: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
QuickNav, |
||||
|
}, |
||||
|
}) |
||||
|
export default class ContactQRScan extends Vue { |
||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void; |
||||
|
$router!: Router; |
||||
|
|
||||
|
isScanning = false; |
||||
|
error: string | null = null; |
||||
|
activeDid = ""; |
||||
|
apiServer = ""; |
||||
|
|
||||
|
// Add new properties to track scanning state |
||||
|
private lastScannedValue: string = ""; |
||||
|
private lastScanTime: number = 0; |
||||
|
private readonly SCAN_DEBOUNCE_MS = 2000; // Prevent duplicate scans within 2 seconds |
||||
|
|
||||
|
// Add cleanup tracking |
||||
|
private isCleaningUp = false; |
||||
|
private isMounted = false; |
||||
|
|
||||
|
async created() { |
||||
|
try { |
||||
|
const settings = await retrieveSettingsForActiveAccount(); |
||||
|
this.activeDid = settings.activeDid || ""; |
||||
|
this.apiServer = settings.apiServer || ""; |
||||
|
} catch (error) { |
||||
|
logger.error("Error initializing component:", { |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
stack: error instanceof Error ? error.stack : undefined, |
||||
|
}); |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Initialization Error", |
||||
|
text: "Failed to initialize QR scanner. Please try again.", |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async startScanning() { |
||||
|
if (this.isCleaningUp) { |
||||
|
logger.debug("Cannot start scanning during cleanup"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
this.error = null; |
||||
|
this.isScanning = true; |
||||
|
this.lastScannedValue = ""; |
||||
|
this.lastScanTime = 0; |
||||
|
|
||||
|
const scanner = QRScannerFactory.getInstance(); |
||||
|
|
||||
|
// Check if scanning is supported first |
||||
|
if (!(await scanner.isSupported())) { |
||||
|
this.error = |
||||
|
"Camera access requires HTTPS. Please use a secure connection."; |
||||
|
this.isScanning = false; |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "warning", |
||||
|
title: "HTTPS Required", |
||||
|
text: "Camera access requires a secure (HTTPS) connection", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Check permissions first |
||||
|
if (!(await scanner.checkPermissions())) { |
||||
|
const granted = await scanner.requestPermissions(); |
||||
|
if (!granted) { |
||||
|
this.error = "Camera permission denied"; |
||||
|
this.isScanning = false; |
||||
|
// Show notification for better visibility |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "warning", |
||||
|
title: "Camera Access Required", |
||||
|
text: "Camera permission denied", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Add scan listener |
||||
|
scanner.addListener({ |
||||
|
onScan: this.onScanDetect, |
||||
|
onError: this.onScanError, |
||||
|
}); |
||||
|
|
||||
|
// Start scanning |
||||
|
await scanner.startScan(); |
||||
|
} catch (error) { |
||||
|
this.error = error instanceof Error ? error.message : String(error); |
||||
|
this.isScanning = false; |
||||
|
logger.error("Error starting scan:", { |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
stack: error instanceof Error ? error.stack : undefined, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async stopScanning() { |
||||
|
try { |
||||
|
const scanner = QRScannerFactory.getInstance(); |
||||
|
await scanner.stopScan(); |
||||
|
} catch (error) { |
||||
|
logger.error("Error stopping scan:", { |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
stack: error instanceof Error ? error.stack : undefined, |
||||
|
}); |
||||
|
} finally { |
||||
|
this.isScanning = false; |
||||
|
this.lastScannedValue = ""; |
||||
|
this.lastScanTime = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async cleanupScanner() { |
||||
|
if (this.isCleaningUp) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.isCleaningUp = true; |
||||
|
try { |
||||
|
logger.info("Cleaning up QR scanner resources"); |
||||
|
await this.stopScanning(); |
||||
|
await QRScannerFactory.cleanup(); |
||||
|
} catch (error) { |
||||
|
logger.error("Error during scanner cleanup:", { |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
stack: error instanceof Error ? error.stack : undefined, |
||||
|
}); |
||||
|
} finally { |
||||
|
this.isCleaningUp = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle QR code scan result with debouncing to prevent duplicate scans |
||||
|
*/ |
||||
|
async onScanDetect(result: string | QRScanResult) { |
||||
|
try { |
||||
|
// Extract raw value from different possible formats |
||||
|
const rawValue = |
||||
|
typeof result === "string" |
||||
|
? result |
||||
|
: result?.rawValue || result?.barcode; |
||||
|
if (!rawValue) { |
||||
|
logger.warn("Invalid scan result - no value found:", result); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Debounce duplicate scans |
||||
|
const now = Date.now(); |
||||
|
if ( |
||||
|
rawValue === this.lastScannedValue && |
||||
|
now - this.lastScanTime < this.SCAN_DEBOUNCE_MS |
||||
|
) { |
||||
|
logger.info("Ignoring duplicate scan:", rawValue); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Update scan tracking |
||||
|
this.lastScannedValue = rawValue; |
||||
|
this.lastScanTime = now; |
||||
|
|
||||
|
logger.info("Processing QR code scan result:", rawValue); |
||||
|
|
||||
|
// Extract JWT |
||||
|
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.", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Process JWT and contact info |
||||
|
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.", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const contactInfo = decodedJwt.payload.own; |
||||
|
if (!contactInfo.did) { |
||||
|
logger.warn("Invalid contact info - missing DID"); |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Invalid Contact", |
||||
|
text: "The contact DID is missing.", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Create contact object |
||||
|
const contact = { |
||||
|
did: contactInfo.did, |
||||
|
name: contactInfo.name || "", |
||||
|
email: contactInfo.email || "", |
||||
|
phone: contactInfo.phone || "", |
||||
|
company: contactInfo.company || "", |
||||
|
title: contactInfo.title || "", |
||||
|
notes: contactInfo.notes || "", |
||||
|
}; |
||||
|
|
||||
|
// Add contact and stop scanning |
||||
|
logger.info("Adding new contact to database:", { |
||||
|
did: contact.did, |
||||
|
name: contact.name, |
||||
|
}); |
||||
|
await this.addNewContact(contact); |
||||
|
await this.stopScanning(); |
||||
|
this.$router.back(); // Return to previous view after successful scan |
||||
|
} catch (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.", |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onScanError(error: Error) { |
||||
|
this.error = error.message; |
||||
|
logger.error("QR code scan error:", { |
||||
|
error: error.message, |
||||
|
stack: error.stack, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async setVisibility(contact: Contact, visibility: boolean) { |
||||
|
const result = await setVisibilityUtil( |
||||
|
this.activeDid, |
||||
|
this.apiServer, |
||||
|
this.axios, |
||||
|
db, |
||||
|
contact, |
||||
|
visibility, |
||||
|
); |
||||
|
if (result.error) { |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error Setting Visibility", |
||||
|
text: result.error as string, |
||||
|
}); |
||||
|
} else if (!result.success) { |
||||
|
logger.warn("Unexpected result from setting visibility:", result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async addNewContact(contact: Contact) { |
||||
|
try { |
||||
|
logger.info("Opening database connection for new contact"); |
||||
|
await db.open(); |
||||
|
|
||||
|
// Check if contact already exists |
||||
|
const existingContacts = await db.contacts.toArray(); |
||||
|
const existingContact = existingContacts.find( |
||||
|
(c) => c.did === contact.did, |
||||
|
); |
||||
|
|
||||
|
if (existingContact) { |
||||
|
logger.info("Contact already exists", { did: contact.did }); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "warning", |
||||
|
title: "Contact Exists", |
||||
|
text: "This contact has already been added to your list.", |
||||
|
}, |
||||
|
3000, |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Add new contact |
||||
|
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, |
||||
|
); |
||||
|
} 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, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Lifecycle hooks |
||||
|
mounted() { |
||||
|
this.isMounted = true; |
||||
|
document.addEventListener("pause", this.handleAppPause); |
||||
|
document.addEventListener("resume", this.handleAppResume); |
||||
|
this.startScanning(); // Automatically start scanning when view is mounted |
||||
|
} |
||||
|
|
||||
|
beforeDestroy() { |
||||
|
this.isMounted = false; |
||||
|
document.removeEventListener("pause", this.handleAppPause); |
||||
|
document.removeEventListener("resume", this.handleAppResume); |
||||
|
this.cleanupScanner(); |
||||
|
} |
||||
|
|
||||
|
async handleAppPause() { |
||||
|
if (!this.isMounted) return; |
||||
|
|
||||
|
logger.info("App paused, stopping scanner"); |
||||
|
await this.stopScanning(); |
||||
|
} |
||||
|
|
||||
|
handleAppResume() { |
||||
|
if (!this.isMounted) return; |
||||
|
|
||||
|
logger.info("App resumed, scanner can be restarted by user"); |
||||
|
this.isScanning = false; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.aspect-square { |
||||
|
aspect-ratio: 1 / 1; |
||||
|
} |
||||
|
</style> |
Loading…
Reference in new issue