WIP: Unified contact QR code display + capture
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user