forked from trent_larson/crowd-funder-for-time-pwa
- Add status messages for different scanning states (initializing, scanning, error) - Add visual feedback with color-coded scanning frame and animations - Add camera switch button for toggling between front/back cameras - Add scanning instructions and tips in the footer - Add retry button for error recovery - Improve error handling and state management - Add browser compatibility message for unsupported browsers This change improves the user experience by providing clear visual feedback and guidance during the QR code scanning process.
332 lines
9.4 KiB
Vue
332 lines
9.4 KiB
Vue
<!-- QRScannerDialog.vue -->
|
|
<template>
|
|
<div
|
|
v-if="visible && !isNativePlatform"
|
|
class="dialog-overlay z-[60] fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
|
|
>
|
|
<div
|
|
class="dialog relative bg-white rounded-lg shadow-xl max-w-lg w-full mx-4"
|
|
>
|
|
<!-- Header -->
|
|
<div class="p-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Scan QR Code</h3>
|
|
<button
|
|
class="absolute top-4 right-4 text-gray-400 hover:text-gray-500"
|
|
aria-label="Close dialog"
|
|
@click="close"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Scanner -->
|
|
<div class="p-4">
|
|
<div
|
|
v-if="useQRReader && !isNativePlatform"
|
|
class="relative aspect-square"
|
|
>
|
|
<!-- Status Message -->
|
|
<div
|
|
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-center py-2 z-10"
|
|
>
|
|
<p v-if="isInitializing">Initializing camera...</p>
|
|
<p v-else-if="isScanning">Position QR code in the frame</p>
|
|
<p v-else-if="error" class="text-red-300">{{ error }}</p>
|
|
<p v-else>Ready to scan</p>
|
|
</div>
|
|
|
|
<qrcode-stream
|
|
:camera="options?.camera === 'front' ? 'user' : 'environment'"
|
|
@decode="onDecode"
|
|
@init="onInit"
|
|
@detect="onDetect"
|
|
@error="onError"
|
|
/>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Camera Switch Button -->
|
|
<button
|
|
@click="toggleCamera"
|
|
class="absolute bottom-4 right-4 bg-white rounded-full p-2 shadow-lg"
|
|
title="Switch camera"
|
|
>
|
|
<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 class="text-center py-8">
|
|
<p class="text-gray-500">
|
|
{{
|
|
isNativePlatform
|
|
? "Using native camera scanner..."
|
|
: "QR code scanning is not supported in this browser."
|
|
}}
|
|
</p>
|
|
<p v-if="!isNativePlatform" class="text-sm text-gray-400 mt-2">
|
|
Please ensure you're using a modern browser with camera access.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="p-4 border-t border-gray-200">
|
|
<div class="flex flex-col space-y-4">
|
|
<!-- Instructions -->
|
|
<div class="text-sm text-gray-600">
|
|
<ul class="list-disc list-inside space-y-1">
|
|
<li>Ensure the QR code is well-lit and in focus</li>
|
|
<li>Hold your device steady</li>
|
|
<li>The QR code should fit within the scanning frame</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<p v-if="error" class="text-red-500 text-sm">{{ error }}</p>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex justify-end space-x-2">
|
|
<button
|
|
v-if="error"
|
|
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
|
@click="retryScanning"
|
|
>
|
|
Retry
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200"
|
|
@click="close"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Prop, Vue } from "vue-facing-decorator";
|
|
import { QrcodeStream } from "vue-qrcode-reader";
|
|
import { QRScannerOptions } from "@/services/QRScanner/types";
|
|
import { logger } from "@/utils/logger";
|
|
import { Capacitor } from "@capacitor/core";
|
|
|
|
interface ScanProps {
|
|
onScan: (result: string) => void;
|
|
onError?: (error: Error) => void;
|
|
options?: QRScannerOptions;
|
|
}
|
|
|
|
@Component({
|
|
components: {
|
|
QrcodeStream,
|
|
},
|
|
})
|
|
export default class QRScannerDialog extends Vue {
|
|
@Prop({ type: Function, required: true }) onScan!: ScanProps["onScan"];
|
|
@Prop({ type: Function }) onError?: ScanProps["onError"];
|
|
@Prop({ type: Object }) options?: ScanProps["options"];
|
|
|
|
visible = true;
|
|
error: string | null = null;
|
|
useQRReader = __USE_QR_READER__;
|
|
isNativePlatform =
|
|
Capacitor.isNativePlatform() ||
|
|
__IS_MOBILE__ ||
|
|
Capacitor.getPlatform() === "android" ||
|
|
Capacitor.getPlatform() === "ios";
|
|
|
|
isInitializing = true;
|
|
isScanning = false;
|
|
preferredCamera: 'user' | 'environment' = 'environment';
|
|
|
|
created() {
|
|
logger.log("QRScannerDialog platform detection:", {
|
|
capacitorNative: Capacitor.isNativePlatform(),
|
|
isMobile: __IS_MOBILE__,
|
|
platform: Capacitor.getPlatform(),
|
|
useQRReader: this.useQRReader,
|
|
isNativePlatform: this.isNativePlatform,
|
|
userAgent: navigator.userAgent,
|
|
mediaDevices: !!navigator.mediaDevices,
|
|
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
|
});
|
|
|
|
// If on native platform, close immediately and don't initialize web scanner
|
|
if (this.isNativePlatform) {
|
|
logger.log("Closing QR dialog on native platform");
|
|
this.$nextTick(() => this.close());
|
|
}
|
|
}
|
|
|
|
async onInit(promise: Promise<void>): Promise<void> {
|
|
// Don't initialize on mobile platforms
|
|
if (this.isNativePlatform) {
|
|
logger.log("Skipping web scanner initialization on native platform");
|
|
return;
|
|
}
|
|
|
|
this.isInitializing = true;
|
|
this.error = null;
|
|
logger.log("Initializing QR scanner...");
|
|
|
|
try {
|
|
await promise;
|
|
this.isInitializing = false;
|
|
logger.log("QR scanner initialized successfully");
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
this.error = wrappedError.message;
|
|
if (this.onError) {
|
|
this.onError(wrappedError);
|
|
}
|
|
logger.error("Error initializing QR scanner:", {
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
name: wrappedError.name,
|
|
});
|
|
} finally {
|
|
this.isInitializing = false;
|
|
}
|
|
}
|
|
|
|
onDetect(promise: Promise<any>): void {
|
|
this.isScanning = true;
|
|
logger.log("QR code detected, processing...");
|
|
promise
|
|
.then((result) => {
|
|
logger.log("QR code processed successfully:", result);
|
|
})
|
|
.catch((error) => {
|
|
logger.error("Error processing QR code:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
})
|
|
.finally(() => {
|
|
this.isScanning = false;
|
|
});
|
|
}
|
|
|
|
onDecode(result: string): void {
|
|
logger.log("QR code decoded:", result);
|
|
try {
|
|
this.onScan(result);
|
|
this.close();
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
this.error = wrappedError.message;
|
|
if (this.onError) {
|
|
this.onError(wrappedError);
|
|
}
|
|
logger.error("Error handling QR scan result:", {
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
name: wrappedError.name,
|
|
});
|
|
}
|
|
}
|
|
|
|
onError(error: Error): void {
|
|
this.isScanning = false;
|
|
logger.error("QR scanner error:", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
name: error.name,
|
|
});
|
|
this.error = error.message;
|
|
if (this.onError) {
|
|
this.onError(error);
|
|
}
|
|
}
|
|
|
|
toggleCamera(): void {
|
|
this.preferredCamera = this.preferredCamera === 'user' ? 'environment' : 'user';
|
|
}
|
|
|
|
retryScanning(): void {
|
|
this.error = null;
|
|
this.isInitializing = true;
|
|
// The QR scanner component will automatically reinitialize
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
logger.log("Closing QR scanner dialog");
|
|
this.visible = false;
|
|
await this.$nextTick();
|
|
if (this.$el && this.$el.parentNode) {
|
|
this.$el.parentNode.removeChild(this.$el);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dialog-overlay {
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.qrcode-stream {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
opacity: 0.5;
|
|
}
|
|
50% {
|
|
opacity: 0.75;
|
|
}
|
|
100% {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.animate-pulse {
|
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
</style>
|