Browse Source
- Deleted ContactScanView.vue and removed its route from the router - Renamed ContactQRScanView.vue to ContactQRScanFullView.vue - Updated all router paths, names, and references to use 'contact-qr-scan-full' - Updated related router links in ContactQRScanShowView.vue for consistency - Ensured all naming and routing is consolidated and matches the new view/component namesql-wa-sqlite
4 changed files with 4 additions and 526 deletions
@ -1,430 +0,0 @@ |
|||||
<template> |
|
||||
<!-- CONTENT --> |
|
||||
<section id="Content" class="relativew-[100vw] h-[100vh]"> |
|
||||
<div |
|
||||
class="absolute inset-x-0 bottom-0 bg-black/50 p-6 pb-[calc(env(safe-area-inset-bottom)+1.5rem)]" |
|
||||
> |
|
||||
<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="flex justify-center items-center"> |
|
||||
<button |
|
||||
class="text-center text-slate-600 leading-none bg-white p-2 rounded-full drop-shadow-lg" |
|
||||
@click="handleBack" |
|
||||
> |
|
||||
<font-awesome icon="xmark" class="size-6"></font-awesome> |
|
||||
</button> |
|
||||
</div> |
|
||||
</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; |
|
||||
} |
|
||||
|
|
||||
async handleBack() { |
|
||||
await this.cleanupScanner(); |
|
||||
this.$router.back(); |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.aspect-square { |
|
||||
aspect-ratio: 1 / 1; |
|
||||
} |
|
||||
</style> |
|
@ -1,92 +0,0 @@ |
|||||
<template> |
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> |
|
||||
<!-- Breadcrumb --> |
|
||||
<div id="ViewBreadcrumb" class="mb-8"> |
|
||||
<h1 class="text-lg text-center font-light relative px-7"> |
|
||||
<!-- Cancel --> |
|
||||
<router-link |
|
||||
:to="{ name: 'account' }" |
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" |
|
||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome> |
|
||||
</router-link> |
|
||||
|
|
||||
Scan Contact |
|
||||
</h1> |
|
||||
</div> |
|
||||
|
|
||||
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code…</h3> |
|
||||
<div class="bg-black rounded overflow-hidden relative mb-4"> |
|
||||
<img src="https://picsum.photos/400/400?random=1" class="w-full" /> |
|
||||
|
|
||||
<!-- Darken overlay --> |
|
||||
<!-- Top --> |
|
||||
<div class="absolute top-0 left-0 right-0 bg-black/50 h-1/4"></div> |
|
||||
<!-- Reft --> |
|
||||
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div> |
|
||||
<!-- Right --> |
|
||||
<div class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"></div> |
|
||||
<!-- Bottom --> |
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div> |
|
||||
|
|
||||
<!-- Reticle overlay --> |
|
||||
<!-- Top-left --> |
|
||||
<div |
|
||||
class="absolute top-1/4 left-1/4 h-6 w-6 border-white border-t-4 border-l-4 drop-shadow" |
|
||||
></div> |
|
||||
<!-- Top-right --> |
|
||||
<div |
|
||||
class="absolute top-1/4 right-1/4 h-6 w-6 border-white border-t-4 border-r-4 drop-shadow" |
|
||||
></div> |
|
||||
<!-- Bottom-left --> |
|
||||
<div |
|
||||
class="absolute bottom-1/4 left-1/4 h-6 w-6 border-white border-b-4 border-l-4 drop-shadow" |
|
||||
></div> |
|
||||
<!-- Bottom-right --> |
|
||||
<div |
|
||||
class="absolute bottom-1/4 right-1/4 h-6 w-6 border-white border-b-4 border-r-4 drop-shadow" |
|
||||
></div> |
|
||||
</div> |
|
||||
|
|
||||
<h3 class="text-sm uppercase font-semibold mb-2">…or Enter Contact Data</h3> |
|
||||
<input |
|
||||
type="text" |
|
||||
placeholder="Name (optional)" |
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" |
|
||||
/> |
|
||||
<input |
|
||||
type="text" |
|
||||
placeholder="ID" |
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" |
|
||||
/> |
|
||||
<input |
|
||||
type="text" |
|
||||
placeholder="Public Key (optional)" |
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" |
|
||||
/> |
|
||||
|
|
||||
<div class="mt-8"> |
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> |
|
||||
<input |
|
||||
type="submit" |
|
||||
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" |
|
||||
value="Look Up Contact" |
|
||||
/> |
|
||||
<button |
|
||||
type="button" |
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" |
|
||||
> |
|
||||
Cancel |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
</section> |
|
||||
</template> |
|
||||
|
|
||||
<script lang="ts"> |
|
||||
import { Component, Vue } from "vue-facing-decorator"; |
|
||||
|
|
||||
@Component({ |
|
||||
components: {}, |
|
||||
}) |
|
||||
export default class ContactScanView extends Vue {} |
|
||||
</script> |
|
Loading…
Reference in new issue