forked from jsnbuchanan/crowd-funder-for-time-pwa
WIP: Unified contact QR code display + capture
This commit is contained in:
@@ -202,4 +202,9 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
return this.cleanupPromise;
|
return this.cleanupPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onStream(callback: (stream: MediaStream | null) => void): void {
|
||||||
|
// No-op for native scanner
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
import { QRScannerService } from "./types";
|
import { QRScannerService } from "./types";
|
||||||
import { CapacitorQRScanner } from "./CapacitorQRScanner";
|
import { CapacitorQRScanner } from "./CapacitorQRScanner";
|
||||||
import { WebDialogQRScanner } from "./WebDialogQRScanner";
|
import { WebInlineQRScanner } from "./WebInlineQRScanner";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +69,7 @@ export class QRScannerFactory {
|
|||||||
: !isNative
|
: !isNative
|
||||||
) {
|
) {
|
||||||
logger.log("Using web QR scanner");
|
logger.log("Using web QR scanner");
|
||||||
this.instance = new WebDialogQRScanner();
|
this.instance = new WebInlineQRScanner();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No QR scanner implementation available for this platform",
|
"No QR scanner implementation available for this platform",
|
||||||
|
|||||||
17
src/services/QRScanner/QRScannerService.ts
Normal file
17
src/services/QRScanner/QRScannerService.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
export interface QRScannerListener {
|
||||||
|
onScan: (result: string) => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QRScannerService {
|
||||||
|
checkPermissions(): Promise<boolean>;
|
||||||
|
requestPermissions(): Promise<boolean>;
|
||||||
|
isSupported(): Promise<boolean>;
|
||||||
|
startScan(): Promise<void>;
|
||||||
|
stopScan(): Promise<void>;
|
||||||
|
addListener(listener: QRScannerListener): void;
|
||||||
|
cleanup(): Promise<void>;
|
||||||
|
onStream(callback: (stream: MediaStream | null) => void): void;
|
||||||
|
}
|
||||||
195
src/services/QRScanner/WebInlineQRScanner.ts
Normal file
195
src/services/QRScanner/WebInlineQRScanner.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
|
||||||
|
import { logger } from "@/utils/logger";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
export class WebInlineQRScanner implements QRScannerService {
|
||||||
|
private scanListener: ScanListener | null = null;
|
||||||
|
private isScanning = false;
|
||||||
|
private stream: MediaStream | null = null;
|
||||||
|
private events = new EventEmitter();
|
||||||
|
|
||||||
|
constructor(private options?: QRScannerOptions) {}
|
||||||
|
|
||||||
|
async checkPermissions(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
logger.log("[QRScanner] Checking camera permissions...");
|
||||||
|
const permissions = await navigator.permissions.query({
|
||||||
|
name: "camera" as PermissionName,
|
||||||
|
});
|
||||||
|
logger.log("[QRScanner] Permission state:", permissions.state);
|
||||||
|
return permissions.state === "granted";
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[QRScanner] Error checking camera permissions:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPermissions(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// First check if we have any video devices
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(
|
||||||
|
(device) => device.kind === "videoinput",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (videoDevices.length === 0) {
|
||||||
|
logger.error("No video devices found");
|
||||||
|
throw new Error("No camera found on this device");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get a stream with specific constraints
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
facingMode: "environment",
|
||||||
|
width: { ideal: 1280 },
|
||||||
|
height: { ideal: 720 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop the test stream immediately
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error requesting camera permissions:", {
|
||||||
|
error: wrappedError.message,
|
||||||
|
stack: wrappedError.stack,
|
||||||
|
name: wrappedError.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provide more specific error messages
|
||||||
|
if (
|
||||||
|
wrappedError.name === "NotFoundError" ||
|
||||||
|
wrappedError.name === "DevicesNotFoundError"
|
||||||
|
) {
|
||||||
|
throw new Error("No camera found on this device");
|
||||||
|
} else if (
|
||||||
|
wrappedError.name === "NotAllowedError" ||
|
||||||
|
wrappedError.name === "PermissionDeniedError"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Camera access denied. Please grant camera permission and try again",
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
wrappedError.name === "NotReadableError" ||
|
||||||
|
wrappedError.name === "TrackStartError"
|
||||||
|
) {
|
||||||
|
throw new Error("Camera is in use by another application");
|
||||||
|
} else {
|
||||||
|
throw new Error(`Camera error: ${wrappedError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isSupported(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Check for secure context first
|
||||||
|
if (!window.isSecureContext) {
|
||||||
|
logger.warn("Camera access requires HTTPS (secure context)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for camera API support
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
logger.warn("Camera API not supported in this browser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have any video devices
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const hasVideoDevices = devices.some(
|
||||||
|
(device) => device.kind === "videoinput",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasVideoDevices) {
|
||||||
|
logger.warn("No video devices found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error checking camera support:", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startScan(): Promise<void> {
|
||||||
|
if (this.isScanning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isScanning = true;
|
||||||
|
logger.log("[WebInlineQRScanner] Starting scan");
|
||||||
|
|
||||||
|
// Get camera stream
|
||||||
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
facingMode: "environment",
|
||||||
|
width: { ideal: 1280 },
|
||||||
|
height: { ideal: 720 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit stream to component
|
||||||
|
this.events.emit("stream", this.stream);
|
||||||
|
} catch (error) {
|
||||||
|
this.isScanning = false;
|
||||||
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
if (this.scanListener?.onError) {
|
||||||
|
this.scanListener.onError(wrappedError);
|
||||||
|
}
|
||||||
|
logger.error("Error starting scan:", wrappedError);
|
||||||
|
throw wrappedError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopScan(): Promise<void> {
|
||||||
|
if (!this.isScanning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.log("[WebInlineQRScanner] Stopping scan");
|
||||||
|
|
||||||
|
// Stop all tracks in the stream
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
|
this.stream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit stream stopped event
|
||||||
|
this.events.emit("stream", null);
|
||||||
|
} catch (error) {
|
||||||
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error stopping scan:", wrappedError);
|
||||||
|
throw wrappedError;
|
||||||
|
} finally {
|
||||||
|
this.isScanning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener: ScanListener): void {
|
||||||
|
this.scanListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to get stream events
|
||||||
|
onStream(callback: (stream: MediaStream | null) => void): void {
|
||||||
|
this.events.on("stream", callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.stopScan();
|
||||||
|
this.events.removeAllListeners();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error during cleanup:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,37 +2,35 @@
|
|||||||
<QuickNav selected="Profile" />
|
<QuickNav selected="Profile" />
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<div class="mb-2">
|
||||||
<div class="mb-8">
|
<h1 class="text-2xl text-center font-light relative px-7">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<div class="relative px-7">
|
<a
|
||||||
<h1
|
|
||||||
class="text-lg text-center font-light px-2 py-1 absolute -left-2 -top-1"
|
class="text-lg text-center font-light px-2 py-1 absolute -left-2 -top-1"
|
||||||
@click="$router.back()"
|
@click="$router.back()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||||
</h1>
|
</a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
|
||||||
Your Contact Info
|
Your Contact Info
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
|
||||||
v-if="!givenName"
|
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
|
||||||
>
|
|
||||||
<span class="text-red">Beware!</span>
|
|
||||||
You aren't sharing your name, so quickly
|
|
||||||
<br />
|
|
||||||
<span
|
|
||||||
class="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-1.5 py-1 rounded-md"
|
|
||||||
@click="openUserNameDialog"
|
|
||||||
>
|
|
||||||
click here to set it for them.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="!givenName"
|
||||||
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
|
>
|
||||||
|
<span class="text-red">Beware!</span>
|
||||||
|
You aren't sharing your name, so quickly
|
||||||
|
<br />
|
||||||
|
<span
|
||||||
|
class="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-1.5 py-1 rounded-md"
|
||||||
|
@click="openUserNameDialog"
|
||||||
|
>
|
||||||
|
click here to set it for them.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
<UserNameDialog ref="userNameDialog" />
|
<UserNameDialog ref="userNameDialog" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -76,11 +74,114 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
|
<h1 class="text-2xl text-center font-light pt-6">Scan Contact Info</h1>
|
||||||
<div v-if="isScanning" class="relative aspect-square">
|
<div v-if="isScanning" class="relative aspect-square max-w-sm mx-auto">
|
||||||
|
<!-- Status Message -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 border-2 border-blue-500 opacity-50 pointer-events-none"
|
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-center py-2 z-10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isInitializing"
|
||||||
|
class="flex items-center justify-center space-x-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span>{{ initializationStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-else-if="isScanning"
|
||||||
|
class="flex items-center justify-center space-x-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-block w-2 h-2 bg-green-500 rounded-full animate-pulse"
|
||||||
|
></span>
|
||||||
|
<span>Position QR code in the frame</span>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="error" class="text-red-300">
|
||||||
|
<span class="font-medium">Error:</span> {{ error }}
|
||||||
|
</p>
|
||||||
|
<p v-else class="flex items-center justify-center space-x-2">
|
||||||
|
<span
|
||||||
|
class="inline-block w-2 h-2 bg-blue-500 rounded-full"
|
||||||
|
></span>
|
||||||
|
<span>Ready to scan</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<qrcode-stream
|
||||||
|
v-if="useQRReader && !isNativePlatform"
|
||||||
|
:camera="preferredCamera"
|
||||||
|
@decode="onDecode"
|
||||||
|
@init="onInit"
|
||||||
|
@detect="onDetect"
|
||||||
|
@error="onError"
|
||||||
|
@camera-on="onCameraOn"
|
||||||
|
@camera-off="onCameraOff"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Scanning Frame -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 border-2"
|
||||||
|
:class="{
|
||||||
|
'border-blue-500': !error && !isScanning,
|
||||||
|
'border-green-500 animate-pulse': isScanning,
|
||||||
|
'border-red-500': error,
|
||||||
|
}"
|
||||||
|
style="opacity: 0.5; pointer-events: none"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<!-- Debug Info -->
|
||||||
|
<div
|
||||||
|
class="absolute bottom-16 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center py-1"
|
||||||
|
>
|
||||||
|
Camera: {{ preferredCamera === "user" ? "Front" : "Back" }} |
|
||||||
|
Status: {{ cameraStatus }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Camera Switch Button -->
|
||||||
|
<button
|
||||||
|
class="absolute bottom-4 right-4 bg-white rounded-full p-2 shadow-lg"
|
||||||
|
title="Switch camera"
|
||||||
|
@click="toggleCamera"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-6 w-6 text-gray-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<button
|
<button
|
||||||
@@ -104,14 +205,6 @@
|
|||||||
permissions.
|
permissions.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Scanner Dialog for Web -->
|
|
||||||
<QRScannerDialog
|
|
||||||
v-if="showScannerDialog"
|
|
||||||
:on-scan="onScanDetect"
|
|
||||||
:on-error="onScanError"
|
|
||||||
:on-close="closeScannerDialog"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -121,10 +214,10 @@ import QRCodeVue3 from "qr-code-generator-vue3";
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import { QrcodeStream } from "vue-qrcode-reader";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import QRScannerDialog from "../components/QRScanner/QRScannerDialog.vue";
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
@@ -139,7 +232,8 @@ import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
|||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveAccountMetadata } from "../libs/util";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
||||||
|
import { WebInlineQRScanner } from "@/services/QRScanner/WebInlineQRScanner";
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -155,7 +249,7 @@ interface IUserNameDialog {
|
|||||||
QRCodeVue3,
|
QRCodeVue3,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
UserNameDialog,
|
UserNameDialog,
|
||||||
QRScannerDialog,
|
QrcodeStream,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ContactQRScanShow extends Vue {
|
export default class ContactQRScanShow extends Vue {
|
||||||
@@ -170,9 +264,15 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
qrValue = "";
|
qrValue = "";
|
||||||
isScanning = false;
|
isScanning = false;
|
||||||
error: string | null = null;
|
error: string | null = null;
|
||||||
showScannerDialog = false;
|
|
||||||
isNativePlatform = Capacitor.isNativePlatform();
|
isNativePlatform = Capacitor.isNativePlatform();
|
||||||
|
|
||||||
|
// QR Scanner properties
|
||||||
|
isInitializing = true;
|
||||||
|
initializationStatus = "Initializing camera...";
|
||||||
|
useQRReader = __USE_QR_READER__;
|
||||||
|
preferredCamera: "user" | "environment" = "environment";
|
||||||
|
cameraStatus = "Initializing";
|
||||||
|
|
||||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||||
|
|
||||||
// Add new properties to track scanning state
|
// Add new properties to track scanning state
|
||||||
@@ -230,6 +330,8 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
try {
|
try {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
|
this.isInitializing = true;
|
||||||
|
this.initializationStatus = "Initializing camera...";
|
||||||
this.lastScannedValue = "";
|
this.lastScannedValue = "";
|
||||||
this.lastScanTime = 0;
|
this.lastScanTime = 0;
|
||||||
|
|
||||||
@@ -240,6 +342,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.error =
|
this.error =
|
||||||
"Camera access requires HTTPS. Please use a secure connection.";
|
"Camera access requires HTTPS. Please use a secure connection.";
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
|
this.isInitializing = false;
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -254,10 +357,12 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
// Check permissions first
|
// Check permissions first
|
||||||
if (!(await scanner.checkPermissions())) {
|
if (!(await scanner.checkPermissions())) {
|
||||||
|
this.initializationStatus = "Requesting camera permission...";
|
||||||
const granted = await scanner.requestPermissions();
|
const granted = await scanner.requestPermissions();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
this.error = "Camera permission denied";
|
this.error = "Camera permission denied";
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
|
this.isInitializing = false;
|
||||||
// Show notification for better visibility
|
// Show notification for better visibility
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -272,12 +377,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the scanner dialog for web
|
|
||||||
if (!this.isNativePlatform) {
|
|
||||||
this.showScannerDialog = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For native platforms, use the scanner service
|
// For native platforms, use the scanner service
|
||||||
scanner.addListener({
|
scanner.addListener({
|
||||||
onScan: this.onScanDetect,
|
onScan: this.onScanDetect,
|
||||||
@@ -289,6 +388,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error instanceof Error ? error.message : String(error);
|
this.error = error instanceof Error ? error.message : String(error);
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
|
this.isInitializing = false;
|
||||||
logger.error("Error starting scan:", {
|
logger.error("Error starting scan:", {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
@@ -601,11 +701,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
closeScannerDialog() {
|
|
||||||
this.showScannerDialog = false;
|
|
||||||
this.isScanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle hooks
|
// Lifecycle hooks
|
||||||
mounted() {
|
mounted() {
|
||||||
this.isMounted = true;
|
this.isMounted = true;
|
||||||
@@ -734,6 +829,90 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onInit(promise: Promise<void>): Promise<void> {
|
||||||
|
logger.log("[QRScanner] onInit called");
|
||||||
|
if (this.isNativePlatform) {
|
||||||
|
logger.log("Skipping QR scanner initialization on native platform");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
this.isInitializing = false;
|
||||||
|
this.cameraStatus = "Ready";
|
||||||
|
} catch (error) {
|
||||||
|
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
this.error = wrappedError.message;
|
||||||
|
this.cameraStatus = "Error";
|
||||||
|
this.isInitializing = false;
|
||||||
|
logger.error("Error during QR scanner initialization:", {
|
||||||
|
error: wrappedError.message,
|
||||||
|
stack: wrappedError.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCameraOn(): void {
|
||||||
|
this.cameraStatus = "Active";
|
||||||
|
this.isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCameraOff(): void {
|
||||||
|
this.cameraStatus = "Off";
|
||||||
|
}
|
||||||
|
|
||||||
|
onDetect(result: any): void {
|
||||||
|
this.isScanning = true;
|
||||||
|
this.cameraStatus = "Detecting";
|
||||||
|
try {
|
||||||
|
let rawValue: string | undefined;
|
||||||
|
if (Array.isArray(result) && result.length > 0 && "rawValue" in result[0]) {
|
||||||
|
rawValue = result[0].rawValue;
|
||||||
|
} else if (result && typeof result === "object" && "rawValue" in result) {
|
||||||
|
rawValue = result.rawValue;
|
||||||
|
}
|
||||||
|
if (rawValue) {
|
||||||
|
this.isInitializing = false;
|
||||||
|
this.initializationStatus = "QR code captured!";
|
||||||
|
this.onScanDetect(rawValue);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error);
|
||||||
|
} finally {
|
||||||
|
this.isScanning = false;
|
||||||
|
this.cameraStatus = "Active";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDecode(result: string): void {
|
||||||
|
try {
|
||||||
|
this.isInitializing = false;
|
||||||
|
this.initializationStatus = "QR code captured!";
|
||||||
|
this.onScanDetect(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCamera(): void {
|
||||||
|
this.preferredCamera = this.preferredCamera === "user" ? "environment" : "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown): void {
|
||||||
|
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
this.error = wrappedError.message;
|
||||||
|
this.cameraStatus = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(error: Error): void {
|
||||||
|
this.error = error.message;
|
||||||
|
this.cameraStatus = "Error";
|
||||||
|
logger.error("QR code scan error:", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user