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.
This commit is contained in:
@@ -3,13 +3,7 @@ FROM node:22-alpine3.20 AS builder
|
||||
|
||||
# Install build dependencies
|
||||
|
||||
RUN apk add --no-cache \
|
||||
python3 \
|
||||
py3-pip \
|
||||
py3-setuptools \
|
||||
make \
|
||||
g++ \
|
||||
gcc
|
||||
RUN apk add --no-cache bash git python3 py3-pip py3-setuptools make g++ gcc
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -17,24 +17,32 @@
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<div v-if="cameraState === 'off'">
|
||||
<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>Tip:</b> Try using a desktop browser, or check if your browser 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.
|
||||
<b>Note:</b> This mobile browser may not support direct camera
|
||||
access, or the app is treating it as a native app.<br />
|
||||
<b>Tip:</b> Try using a desktop browser, or check if your browser
|
||||
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 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>
|
||||
</div>
|
||||
<div v-else-if="cameraState === 'error'">
|
||||
<b>Error:</b> {{ error || cameraStateMessage }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<b>Status:</b> {{ cameraStateMessage || 'Unknown reason.' }}
|
||||
<b>Status:</b> {{ cameraStateMessage || "Unknown reason." }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,27 +68,48 @@
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<p><strong>Camera State:</strong> {{ cameraState }}</p>
|
||||
<p><strong>State Message:</strong> {{ cameraStateMessage || 'None' }}</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>
|
||||
<p>
|
||||
<strong>State Message:</strong>
|
||||
{{ cameraStateMessage || "None" }}
|
||||
</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>
|
||||
<p><strong>Browser:</strong> {{ userAgent }}</p>
|
||||
<p><strong>HTTPS:</strong> {{ isSecureContext ? 'Yes' : 'No' }}</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>
|
||||
<p>
|
||||
<strong>HTTPS:</strong>
|
||||
{{ isSecureContext ? "Yes" : "No" }}
|
||||
</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>
|
||||
|
||||
<!-- Toggle Diagnostics Button -->
|
||||
<button
|
||||
@click="toggleDiagnostics"
|
||||
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>
|
||||
<div class="camera-container w-full h-full relative">
|
||||
<video
|
||||
@@ -284,8 +313,18 @@ export default class ImageMethodDialog extends Vue {
|
||||
userAgent = navigator.userAgent;
|
||||
isSecureContext = window.isSecureContext;
|
||||
hasMediaDevices = !!navigator.mediaDevices;
|
||||
hasGetUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
|
||||
cameraState: 'off' | 'initializing' | 'ready' | 'active' | 'error' | 'permission_denied' | 'not_found' | 'in_use' = 'off';
|
||||
hasGetUserMedia = !!(
|
||||
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
|
||||
);
|
||||
cameraState:
|
||||
| "off"
|
||||
| "initializing"
|
||||
| "ready"
|
||||
| "active"
|
||||
| "error"
|
||||
| "permission_denied"
|
||||
| "not_found"
|
||||
| "in_use" = "off";
|
||||
cameraStateMessage?: string;
|
||||
error: string | null = null;
|
||||
|
||||
@@ -403,19 +442,21 @@ export default class ImageMethodDialog extends Vue {
|
||||
|
||||
if (this.platformCapabilities.isNativeApp) {
|
||||
logger.debug("Using platform service for mobile device");
|
||||
this.cameraState = 'initializing';
|
||||
this.cameraStateMessage = 'Using platform camera service...';
|
||||
this.cameraState = "initializing";
|
||||
this.cameraStateMessage = "Using platform camera service...";
|
||||
try {
|
||||
const result = await this.platformService.takePicture();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
this.cameraState = 'ready';
|
||||
this.cameraStateMessage = 'Photo captured successfully';
|
||||
this.cameraState = "ready";
|
||||
this.cameraStateMessage = "Photo captured successfully";
|
||||
} catch (error) {
|
||||
logger.error("Error taking picture:", error);
|
||||
this.cameraState = 'error';
|
||||
this.cameraStateMessage = error instanceof Error ? error.message : 'Failed to take picture';
|
||||
this.error = error instanceof Error ? error.message : 'Failed to take picture';
|
||||
this.cameraState = "error";
|
||||
this.cameraStateMessage =
|
||||
error instanceof Error ? error.message : "Failed to take picture";
|
||||
this.error =
|
||||
error instanceof Error ? error.message : "Failed to take picture";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -431,8 +472,8 @@ export default class ImageMethodDialog extends Vue {
|
||||
|
||||
logger.debug("Starting camera preview for desktop browser");
|
||||
try {
|
||||
this.cameraState = 'initializing';
|
||||
this.cameraStateMessage = 'Requesting camera access...';
|
||||
this.cameraState = "initializing";
|
||||
this.cameraStateMessage = "Requesting camera access...";
|
||||
this.showCameraPreview = true;
|
||||
await this.$nextTick();
|
||||
|
||||
@@ -441,8 +482,8 @@ export default class ImageMethodDialog extends Vue {
|
||||
});
|
||||
logger.debug("Camera access granted");
|
||||
this.cameraStream = stream;
|
||||
this.cameraState = 'active';
|
||||
this.cameraStateMessage = 'Camera is active';
|
||||
this.cameraState = "active";
|
||||
this.cameraStateMessage = "Camera is active";
|
||||
|
||||
await this.$nextTick();
|
||||
|
||||
@@ -459,13 +500,22 @@ export default class ImageMethodDialog extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error starting camera preview:", error);
|
||||
let errorMessage = error instanceof Error ? error.message : 'Failed to access camera';
|
||||
if (error.name === 'NotReadableError' || 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.';
|
||||
let errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to access camera";
|
||||
if (
|
||||
error.name === "NotReadableError" ||
|
||||
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.error = errorMessage;
|
||||
this.$notify(
|
||||
@@ -487,8 +537,8 @@ export default class ImageMethodDialog extends Vue {
|
||||
this.cameraStream = null;
|
||||
}
|
||||
this.showCameraPreview = false;
|
||||
this.cameraState = 'off';
|
||||
this.cameraStateMessage = 'Camera stopped';
|
||||
this.cameraState = "off";
|
||||
this.cameraStateMessage = "Camera stopped";
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -243,11 +243,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "recent-offers-to-user-projects",
|
||||
component: () => import("../views/RecentOffersToUserProjectsView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/scan-contact",
|
||||
name: "scan-contact",
|
||||
component: () => import("../views/ContactScanView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/search-area",
|
||||
name: "search-area",
|
||||
|
||||
@@ -94,13 +94,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
// First try the Permissions API if available
|
||||
if (navigator.permissions && navigator.permissions.query) {
|
||||
try {
|
||||
const permissions = await navigator.permissions.query({
|
||||
name: "camera" as PermissionName,
|
||||
});
|
||||
logger.error(
|
||||
const permissions = await navigator.permissions.query({
|
||||
name: "camera" as PermissionName,
|
||||
});
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
||||
permissions.state,
|
||||
);
|
||||
permissions.state,
|
||||
);
|
||||
if (permissions.state === "granted") {
|
||||
this.updateCameraState("ready", "Camera permissions granted");
|
||||
return true;
|
||||
@@ -121,7 +121,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
video: true,
|
||||
});
|
||||
// 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");
|
||||
return true;
|
||||
} catch (mediaError) {
|
||||
@@ -134,7 +134,10 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
},
|
||||
);
|
||||
|
||||
if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") {
|
||||
if (
|
||||
error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError"
|
||||
) {
|
||||
this.updateCameraState("permission_denied", "Camera access denied");
|
||||
return false;
|
||||
}
|
||||
@@ -193,26 +196,26 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
);
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: "environment",
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
},
|
||||
});
|
||||
|
||||
this.updateCameraState("ready", "Camera permissions granted");
|
||||
|
||||
// Stop the test stream immediately
|
||||
stream.getTracks().forEach((track) => {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||
kind: track.kind,
|
||||
label: track.label,
|
||||
readyState: track.readyState,
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: "environment",
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
},
|
||||
});
|
||||
track.stop();
|
||||
});
|
||||
return true;
|
||||
|
||||
this.updateCameraState("ready", "Camera permissions granted");
|
||||
|
||||
// Stop the test stream immediately
|
||||
stream.getTracks().forEach((track) => {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||
kind: track.kind,
|
||||
label: track.label,
|
||||
readyState: track.readyState,
|
||||
});
|
||||
track.stop();
|
||||
});
|
||||
return true;
|
||||
} catch (mediaError) {
|
||||
const error = mediaError as Error;
|
||||
logger.error(
|
||||
@@ -224,31 +227,31 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
},
|
||||
);
|
||||
|
||||
// Update state based on error type
|
||||
if (
|
||||
// Update state based on error type
|
||||
if (
|
||||
error.name === "NotFoundError" ||
|
||||
error.name === "DevicesNotFoundError"
|
||||
) {
|
||||
this.updateCameraState("not_found", "No camera found on this device");
|
||||
throw new Error("No camera found on this device");
|
||||
} else if (
|
||||
) {
|
||||
this.updateCameraState("not_found", "No camera found on this device");
|
||||
throw new Error("No camera found on this device");
|
||||
} else if (
|
||||
error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError"
|
||||
) {
|
||||
this.updateCameraState("permission_denied", "Camera access denied");
|
||||
throw new Error(
|
||||
"Camera access denied. Please grant camera permission and try again",
|
||||
);
|
||||
} else if (
|
||||
) {
|
||||
this.updateCameraState("permission_denied", "Camera access denied");
|
||||
throw new Error(
|
||||
"Camera access denied. Please grant camera permission and try again",
|
||||
);
|
||||
} else if (
|
||||
error.name === "NotReadableError" ||
|
||||
error.name === "TrackStartError"
|
||||
) {
|
||||
this.updateCameraState(
|
||||
"in_use",
|
||||
"Camera is in use by another application",
|
||||
);
|
||||
throw new Error("Camera is in use by another application");
|
||||
} else {
|
||||
) {
|
||||
this.updateCameraState(
|
||||
"in_use",
|
||||
"Camera is in use by another application",
|
||||
);
|
||||
throw new Error("Camera is in use by another application");
|
||||
} else {
|
||||
this.updateCameraState("error", error.message);
|
||||
throw new Error(`Camera error: ${error.message}`);
|
||||
}
|
||||
|
||||
430
src/views/ContactQRScanFullView.vue
Normal file
430
src/views/ContactQRScanFullView.vue
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user