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. 7
      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
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

126
src/components/ImageMethodDialog.vue

@ -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;
}

5
src/router/index.ts

@ -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",

7
src/services/QRScanner/WebInlineQRScanner.ts

@ -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;
}

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