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