Browse Source

Remove ContactScanView and rename ContactQRScanView to ContactQRScanFullView

- Deleted ContactScanView.vue and its route from the router.
- Renamed ContactQRScanView.vue to ContactQRScanFullView.vue.
- Updated all router paths, names, and references for consistency.
- Fixed related links and imports to use the new view/component name.
qrcode-reboot
Matt Raymer 1 day ago
parent
commit
190c972f57
  1. 8
      Dockerfile
  2. 126
      src/components/ImageMethodDialog.vue
  3. 5
      src/router/index.ts
  4. 93
      src/services/QRScanner/WebInlineQRScanner.ts
  5. 430
      src/views/ContactQRScanFullView.vue

8
Dockerfile

@ -3,13 +3,7 @@ FROM node:22-alpine3.20 AS builder
# Install build dependencies # Install build dependencies
RUN apk add --no-cache \ RUN apk add --no-cache bash git python3 py3-pip py3-setuptools make g++ gcc
python3 \
py3-pip \
py3-setuptools \
make \
g++ \
gcc
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app

126
src/components/ImageMethodDialog.vue

@ -17,24 +17,32 @@
</div> </div>
<!-- FEEDBACK: Show if camera preview is not visible after mounting --> <!-- FEEDBACK: Show if camera preview is not visible after mounting -->
<div v-if="!showCameraPreview && !blob && isRegistered" class="bg-red-100 text-red-700 border border-red-400 rounded px-4 py-3 my-4 text-sm"> <div
v-if="!showCameraPreview && !blob && isRegistered"
class="bg-red-100 text-red-700 border border-red-400 rounded px-4 py-3 my-4 text-sm"
>
<strong>Camera preview not started.</strong> <strong>Camera preview not started.</strong>
<div v-if="cameraState === 'off'"> <div v-if="cameraState === 'off'">
<span v-if="platformCapabilities.isMobile"> <span v-if="platformCapabilities.isMobile">
<b>Note:</b> This mobile browser may not support direct camera access, or the app is treating it as a native app.<br> <b>Note:</b> This mobile browser may not support direct camera
<b>Tip:</b> Try using a desktop browser, or check if your browser supports camera access for web apps.<br> access, or the app is treating it as a native app.<br />
<b>Developer:</b> The platform detection logic may be skipping camera preview for mobile browsers. <br> <b>Tip:</b> Try using a desktop browser, or check if your browser
<b>Action:</b> Review <code>platformCapabilities.isMobile</code> and ensure web browsers on mobile are not treated as native apps. supports camera access for web apps.<br />
<b>Developer:</b> The platform detection logic may be skipping
camera preview for mobile browsers. <br />
<b>Action:</b> Review <code>platformCapabilities.isMobile</code> and
ensure web browsers on mobile are not treated as native apps.
</span> </span>
<span v-else> <span v-else>
<b>Tip:</b> Your browser supports camera APIs, but the preview did not start. Try refreshing the page or checking browser permissions. <b>Tip:</b> Your browser supports camera APIs, but the preview did
not start. Try refreshing the page or checking browser permissions.
</span> </span>
</div> </div>
<div v-else-if="cameraState === 'error'"> <div v-else-if="cameraState === 'error'">
<b>Error:</b> {{ error || cameraStateMessage }} <b>Error:</b> {{ error || cameraStateMessage }}
</div> </div>
<div v-else> <div v-else>
<b>Status:</b> {{ cameraStateMessage || 'Unknown reason.' }} <b>Status:</b> {{ cameraStateMessage || "Unknown reason." }}
</div> </div>
</div> </div>
@ -60,27 +68,48 @@
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div> <div>
<p><strong>Camera State:</strong> {{ cameraState }}</p> <p><strong>Camera State:</strong> {{ cameraState }}</p>
<p><strong>State Message:</strong> {{ cameraStateMessage || 'None' }}</p> <p>
<p><strong>Error:</strong> {{ error || 'None' }}</p> <strong>State Message:</strong>
<p><strong>Preview Active:</strong> {{ showCameraPreview ? 'Yes' : 'No' }}</p> {{ cameraStateMessage || "None" }}
<p><strong>Stream Active:</strong> {{ !!cameraStream ? 'Yes' : 'No' }}</p> </p>
<p><strong>Error:</strong> {{ error || "None" }}</p>
<p>
<strong>Preview Active:</strong>
{{ showCameraPreview ? "Yes" : "No" }}
</p>
<p>
<strong>Stream Active:</strong>
{{ !!cameraStream ? "Yes" : "No" }}
</p>
</div> </div>
<div> <div>
<p><strong>Browser:</strong> {{ userAgent }}</p> <p><strong>Browser:</strong> {{ userAgent }}</p>
<p><strong>HTTPS:</strong> {{ isSecureContext ? 'Yes' : 'No' }}</p> <p>
<p><strong>MediaDevices:</strong> {{ hasMediaDevices ? 'Yes' : 'No' }}</p> <strong>HTTPS:</strong>
<p><strong>GetUserMedia:</strong> {{ hasGetUserMedia ? 'Yes' : 'No' }}</p> {{ isSecureContext ? "Yes" : "No" }}
<p><strong>Platform:</strong> {{ platformCapabilities.isMobile ? 'Mobile' : 'Desktop' }}</p> </p>
<p>
<strong>MediaDevices:</strong>
{{ hasMediaDevices ? "Yes" : "No" }}
</p>
<p>
<strong>GetUserMedia:</strong>
{{ hasGetUserMedia ? "Yes" : "No" }}
</p>
<p>
<strong>Platform:</strong>
{{ platformCapabilities.isMobile ? "Mobile" : "Desktop" }}
</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Toggle Diagnostics Button --> <!-- Toggle Diagnostics Button -->
<button <button
@click="toggleDiagnostics"
class="absolute top-2 right-2 bg-black/50 text-white px-2 py-1 rounded text-xs z-30" class="absolute top-2 right-2 bg-black/50 text-white px-2 py-1 rounded text-xs z-30"
@click="toggleDiagnostics"
> >
{{ showDiagnostics ? 'Hide Diagnostics' : 'Show Diagnostics' }} {{ showDiagnostics ? "Hide Diagnostics" : "Show Diagnostics" }}
</button> </button>
<div class="camera-container w-full h-full relative"> <div class="camera-container w-full h-full relative">
<video <video
@ -284,8 +313,18 @@ export default class ImageMethodDialog extends Vue {
userAgent = navigator.userAgent; userAgent = navigator.userAgent;
isSecureContext = window.isSecureContext; isSecureContext = window.isSecureContext;
hasMediaDevices = !!navigator.mediaDevices; hasMediaDevices = !!navigator.mediaDevices;
hasGetUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); hasGetUserMedia = !!(
cameraState: 'off' | 'initializing' | 'ready' | 'active' | 'error' | 'permission_denied' | 'not_found' | 'in_use' = 'off'; navigator.mediaDevices && navigator.mediaDevices.getUserMedia
);
cameraState:
| "off"
| "initializing"
| "ready"
| "active"
| "error"
| "permission_denied"
| "not_found"
| "in_use" = "off";
cameraStateMessage?: string; cameraStateMessage?: string;
error: string | null = null; error: string | null = null;
@ -403,19 +442,21 @@ export default class ImageMethodDialog extends Vue {
if (this.platformCapabilities.isNativeApp) { if (this.platformCapabilities.isNativeApp) {
logger.debug("Using platform service for mobile device"); logger.debug("Using platform service for mobile device");
this.cameraState = 'initializing'; this.cameraState = "initializing";
this.cameraStateMessage = 'Using platform camera service...'; this.cameraStateMessage = "Using platform camera service...";
try { try {
const result = await this.platformService.takePicture(); const result = await this.platformService.takePicture();
this.blob = result.blob; this.blob = result.blob;
this.fileName = result.fileName; this.fileName = result.fileName;
this.cameraState = 'ready'; this.cameraState = "ready";
this.cameraStateMessage = 'Photo captured successfully'; this.cameraStateMessage = "Photo captured successfully";
} catch (error) { } catch (error) {
logger.error("Error taking picture:", error); logger.error("Error taking picture:", error);
this.cameraState = 'error'; this.cameraState = "error";
this.cameraStateMessage = error instanceof Error ? error.message : 'Failed to take picture'; this.cameraStateMessage =
this.error = error instanceof Error ? error.message : 'Failed to take picture'; error instanceof Error ? error.message : "Failed to take picture";
this.error =
error instanceof Error ? error.message : "Failed to take picture";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -431,8 +472,8 @@ export default class ImageMethodDialog extends Vue {
logger.debug("Starting camera preview for desktop browser"); logger.debug("Starting camera preview for desktop browser");
try { try {
this.cameraState = 'initializing'; this.cameraState = "initializing";
this.cameraStateMessage = 'Requesting camera access...'; this.cameraStateMessage = "Requesting camera access...";
this.showCameraPreview = true; this.showCameraPreview = true;
await this.$nextTick(); await this.$nextTick();
@ -441,8 +482,8 @@ export default class ImageMethodDialog extends Vue {
}); });
logger.debug("Camera access granted"); logger.debug("Camera access granted");
this.cameraStream = stream; this.cameraStream = stream;
this.cameraState = 'active'; this.cameraState = "active";
this.cameraStateMessage = 'Camera is active'; this.cameraStateMessage = "Camera is active";
await this.$nextTick(); await this.$nextTick();
@ -459,13 +500,22 @@ export default class ImageMethodDialog extends Vue {
} }
} catch (error) { } catch (error) {
logger.error("Error starting camera preview:", error); logger.error("Error starting camera preview:", error);
let errorMessage = error instanceof Error ? error.message : 'Failed to access camera'; let errorMessage =
if (error.name === 'NotReadableError' || error.name === 'TrackStartError') { error instanceof Error ? error.message : "Failed to access camera";
errorMessage = 'Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.'; if (
} else if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') { error.name === "NotReadableError" ||
errorMessage = 'Camera access was denied. Please allow camera access in your browser settings.'; error.name === "TrackStartError"
) {
errorMessage =
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
} else if (
error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError"
) {
errorMessage =
"Camera access was denied. Please allow camera access in your browser settings.";
} }
this.cameraState = 'error'; this.cameraState = "error";
this.cameraStateMessage = errorMessage; this.cameraStateMessage = errorMessage;
this.error = errorMessage; this.error = errorMessage;
this.$notify( this.$notify(
@ -487,8 +537,8 @@ export default class ImageMethodDialog extends Vue {
this.cameraStream = null; this.cameraStream = null;
} }
this.showCameraPreview = false; this.showCameraPreview = false;
this.cameraState = 'off'; this.cameraState = "off";
this.cameraStateMessage = 'Camera stopped'; this.cameraStateMessage = "Camera stopped";
this.error = null; this.error = null;
} }

5
src/router/index.ts

@ -243,11 +243,6 @@ const routes: Array<RouteRecordRaw> = [
name: "recent-offers-to-user-projects", name: "recent-offers-to-user-projects",
component: () => import("../views/RecentOffersToUserProjectsView.vue"), component: () => import("../views/RecentOffersToUserProjectsView.vue"),
}, },
{
path: "/scan-contact",
name: "scan-contact",
component: () => import("../views/ContactScanView.vue"),
},
{ {
path: "/search-area", path: "/search-area",
name: "search-area", name: "search-area",

93
src/services/QRScanner/WebInlineQRScanner.ts

@ -94,13 +94,13 @@ export class WebInlineQRScanner implements QRScannerService {
// First try the Permissions API if available // First try the Permissions API if available
if (navigator.permissions && navigator.permissions.query) { if (navigator.permissions && navigator.permissions.query) {
try { try {
const permissions = await navigator.permissions.query({ const permissions = await navigator.permissions.query({
name: "camera" as PermissionName, name: "camera" as PermissionName,
}); });
logger.error( logger.error(
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`, `[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
permissions.state, permissions.state,
); );
if (permissions.state === "granted") { if (permissions.state === "granted") {
this.updateCameraState("ready", "Camera permissions granted"); this.updateCameraState("ready", "Camera permissions granted");
return true; return true;
@ -121,7 +121,7 @@ export class WebInlineQRScanner implements QRScannerService {
video: true, video: true,
}); });
// If we get here, we have permission // If we get here, we have permission
testStream.getTracks().forEach(track => track.stop()); testStream.getTracks().forEach((track) => track.stop());
this.updateCameraState("ready", "Camera permissions granted"); this.updateCameraState("ready", "Camera permissions granted");
return true; return true;
} catch (mediaError) { } catch (mediaError) {
@ -133,8 +133,11 @@ export class WebInlineQRScanner implements QRScannerService {
message: error.message, message: error.message,
}, },
); );
if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") { if (
error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError"
) {
this.updateCameraState("permission_denied", "Camera access denied"); this.updateCameraState("permission_denied", "Camera access denied");
return false; return false;
} }
@ -193,26 +196,26 @@ export class WebInlineQRScanner implements QRScannerService {
); );
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: { video: {
facingMode: "environment", facingMode: "environment",
width: { ideal: 1280 }, width: { ideal: 1280 },
height: { ideal: 720 }, height: { ideal: 720 },
}, },
}); });
this.updateCameraState("ready", "Camera permissions granted"); this.updateCameraState("ready", "Camera permissions granted");
// Stop the test stream immediately // Stop the test stream immediately
stream.getTracks().forEach((track) => { stream.getTracks().forEach((track) => {
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, { logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
kind: track.kind, kind: track.kind,
label: track.label, label: track.label,
readyState: track.readyState, readyState: track.readyState,
});
track.stop();
}); });
track.stop(); return true;
});
return true;
} catch (mediaError) { } catch (mediaError) {
const error = mediaError as Error; const error = mediaError as Error;
logger.error( logger.error(
@ -224,31 +227,31 @@ export class WebInlineQRScanner implements QRScannerService {
}, },
); );
// Update state based on error type // Update state based on error type
if ( if (
error.name === "NotFoundError" || error.name === "NotFoundError" ||
error.name === "DevicesNotFoundError" error.name === "DevicesNotFoundError"
) { ) {
this.updateCameraState("not_found", "No camera found on this device"); this.updateCameraState("not_found", "No camera found on this device");
throw new Error("No camera found on this device"); throw new Error("No camera found on this device");
} else if ( } else if (
error.name === "NotAllowedError" || error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError" error.name === "PermissionDeniedError"
) { ) {
this.updateCameraState("permission_denied", "Camera access denied"); this.updateCameraState("permission_denied", "Camera access denied");
throw new Error( throw new Error(
"Camera access denied. Please grant camera permission and try again", "Camera access denied. Please grant camera permission and try again",
); );
} else if ( } else if (
error.name === "NotReadableError" || error.name === "NotReadableError" ||
error.name === "TrackStartError" error.name === "TrackStartError"
) { ) {
this.updateCameraState( this.updateCameraState(
"in_use", "in_use",
"Camera is in use by another application", "Camera is in use by another application",
); );
throw new Error("Camera is in use by another application"); throw new Error("Camera is in use by another application");
} else { } else {
this.updateCameraState("error", error.message); this.updateCameraState("error", error.message);
throw new Error(`Camera error: ${error.message}`); throw new Error(`Camera error: ${error.message}`);
} }

430
src/views/ContactQRScanFullView.vue

@ -0,0 +1,430 @@
<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>
Loading…
Cancel
Save