WIP: Unified contact QR code display + capture

This commit is contained in:
Jose Olarte III
2025-05-05 20:52:20 +08:00
parent 9b73e05bdb
commit 79707d2811
5 changed files with 447 additions and 51 deletions

View File

@@ -2,37 +2,35 @@
<QuickNav selected="Profile" />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="relative px-7">
<h1
<div class="mb-2">
<h1 class="text-2xl text-center font-light relative px-7">
<!-- Back -->
<a
class="text-lg text-center font-light px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</h1>
</div>
</a>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info
</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>
<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" />
<div
@@ -76,11 +74,114 @@
</div>
<div class="text-center">
<h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
<div v-if="isScanning" class="relative aspect-square">
<h1 class="text-2xl text-center font-light pt-6">Scan Contact Info</h1>
<div v-if="isScanning" class="relative aspect-square max-w-sm mx-auto">
<!-- Status Message -->
<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>
<!-- 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 v-else>
<button
@@ -104,14 +205,6 @@
permissions.
</span>
</div>
<!-- QR Scanner Dialog for Web -->
<QRScannerDialog
v-if="showScannerDialog"
:on-scan="onScanDetect"
:on-error="onScanError"
:on-close="closeScannerDialog"
/>
</section>
</template>
@@ -121,10 +214,10 @@ import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { Capacitor } from "@capacitor/core";
import { QrcodeStream } from "vue-qrcode-reader";
import QuickNav from "../components/QuickNav.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import QRScannerDialog from "../components/QRScanner/QRScannerDialog.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
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 { Router } from "vue-router";
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 {
rawValue?: string;
@@ -155,7 +249,7 @@ interface IUserNameDialog {
QRCodeVue3,
QuickNav,
UserNameDialog,
QRScannerDialog,
QrcodeStream,
},
})
export default class ContactQRScanShow extends Vue {
@@ -170,9 +264,15 @@ export default class ContactQRScanShow extends Vue {
qrValue = "";
isScanning = false;
error: string | null = null;
showScannerDialog = false;
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;
// Add new properties to track scanning state
@@ -230,6 +330,8 @@ export default class ContactQRScanShow extends Vue {
try {
this.error = null;
this.isScanning = true;
this.isInitializing = true;
this.initializationStatus = "Initializing camera...";
this.lastScannedValue = "";
this.lastScanTime = 0;
@@ -240,6 +342,7 @@ export default class ContactQRScanShow extends Vue {
this.error =
"Camera access requires HTTPS. Please use a secure connection.";
this.isScanning = false;
this.isInitializing = false;
this.$notify(
{
group: "alert",
@@ -254,10 +357,12 @@ export default class ContactQRScanShow extends Vue {
// Check permissions first
if (!(await scanner.checkPermissions())) {
this.initializationStatus = "Requesting camera permission...";
const granted = await scanner.requestPermissions();
if (!granted) {
this.error = "Camera permission denied";
this.isScanning = false;
this.isInitializing = false;
// Show notification for better visibility
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
scanner.addListener({
onScan: this.onScanDetect,
@@ -289,6 +388,7 @@ export default class ContactQRScanShow extends Vue {
} catch (error) {
this.error = error instanceof Error ? error.message : String(error);
this.isScanning = false;
this.isInitializing = false;
logger.error("Error starting scan:", {
error: error instanceof Error ? error.message : String(error),
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
mounted() {
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>