From a9c07f6af3308aeed2b1095db241020d5f040914 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 18 Sep 2025 22:09:46 +0200 Subject: [PATCH] Add QR code scanning functionality with new QRScanner component - Introduced a new QRScanner component to facilitate QR code scanning within the application. - Integrated QR code scanning capabilities into the SendDialog.vue, allowing users to scan QR codes for payment destinations. - Updated package.json and package-lock.json to include the qr-scanner library for QR code processing. - Enhanced user experience by providing visual feedback and error handling during the scanning process. These changes improve the payment workflow by enabling users to easily scan QR codes for transactions. Enhance QR code interaction in ReceiveDialog.vue - Updated the QR code display to include a clickable area that allows users to copy the invoice directly to the clipboard. - Added a descriptive message below the QR code to inform users about the copy functionality, improving usability and accessibility. These changes enhance the user experience by making it easier to interact with the QR code for invoice management. Fix QR scanner loading state condition and remove unused video element reference - Updated the loading state condition in qr-scanner.vue to check for camera permission correctly. - Removed the unused videoElement reference in useQRScanner.ts to clean up the code. These changes improve the functionality and clarity of the QR scanner component. --- package-lock.json | 16 +++ package.json | 1 + src/components/ui/qr-scanner.vue | 119 ++++++++++++++++++ src/composables/useQRScanner.ts | 109 ++++++++++++++++ .../wallet/components/ReceiveDialog.vue | 17 ++- src/modules/wallet/components/SendDialog.vue | 59 ++++++++- 6 files changed, 311 insertions(+), 10 deletions(-) create mode 100644 src/components/ui/qr-scanner.vue create mode 100644 src/composables/useQRScanner.ts diff --git a/package-lock.json b/package-lock.json index 351df0a..2250669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "lucide-vue-next": "^0.474.0", "nostr-tools": "^2.10.4", "pinia": "^2.3.1", + "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", "radix-vue": "^1.9.13", "reka-ui": "^2.5.0", @@ -5091,6 +5092,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/qrcode": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", @@ -11697,6 +11704,15 @@ "node": ">=6" } }, + "node_modules/qr-scanner": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/qr-scanner/-/qr-scanner-1.4.2.tgz", + "integrity": "sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==", + "license": "MIT", + "dependencies": { + "@types/offscreencanvas": "^2019.6.4" + } + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", diff --git a/package.json b/package.json index a6885cc..34de2b4 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lucide-vue-next": "^0.474.0", "nostr-tools": "^2.10.4", "pinia": "^2.3.1", + "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", "radix-vue": "^1.9.13", "reka-ui": "^2.5.0", diff --git a/src/components/ui/qr-scanner.vue b/src/components/ui/qr-scanner.vue new file mode 100644 index 0000000..ca9c861 --- /dev/null +++ b/src/components/ui/qr-scanner.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/composables/useQRScanner.ts b/src/composables/useQRScanner.ts new file mode 100644 index 0000000..aca5fda --- /dev/null +++ b/src/composables/useQRScanner.ts @@ -0,0 +1,109 @@ +import { ref, onUnmounted } from 'vue' +import QrScanner from 'qr-scanner' + +export function useQRScanner() { + const isScanning = ref(false) + const hasPermission = ref(null) + const error = ref(null) + const scanResult = ref(null) + + let qrScanner: QrScanner | null = null + + const startScanning = async (videoEl: HTMLVideoElement, onResult: (result: string) => void) => { + try { + error.value = null + + // Check if camera is available + const hasCamera = await QrScanner.hasCamera() + if (!hasCamera) { + throw new Error('No camera found') + } + + // Request camera permission + await navigator.mediaDevices.getUserMedia({ video: true }) + hasPermission.value = true + + // Create QR scanner instance + qrScanner = new QrScanner( + videoEl, + (result) => { + scanResult.value = result.data + onResult(result.data) + }, + { + highlightScanRegion: true, + highlightCodeOutline: true, + maxScansPerSecond: 5, + } + ) + + await qrScanner.start() + isScanning.value = true + + } catch (err) { + console.error('Failed to start QR scanner:', err) + hasPermission.value = false + + if (err instanceof Error) { + if (err.name === 'NotAllowedError') { + error.value = 'Camera permission denied. Please allow camera access and try again.' + } else if (err.name === 'NotFoundError') { + error.value = 'No camera found on this device.' + } else if (err.name === 'NotSupportedError') { + error.value = 'Camera not supported on this device.' + } else { + error.value = err.message + } + } else { + error.value = 'Failed to access camera' + } + } + } + + const stopScanning = () => { + if (qrScanner) { + qrScanner.stop() + qrScanner.destroy() + qrScanner = null + } + isScanning.value = false + scanResult.value = null + } + + const toggleFlash = async () => { + if (qrScanner) { + try { + await qrScanner.toggleFlash() + } catch (err) { + console.error('Failed to toggle flash:', err) + } + } + } + + const hasFlash = async (): Promise => { + if (qrScanner) { + try { + return await qrScanner.hasFlash() + } catch (err) { + return false + } + } + return false + } + + // Cleanup on unmount + onUnmounted(() => { + stopScanning() + }) + + return { + isScanning, + hasPermission, + error, + scanResult, + startScanning, + stopScanning, + toggleFlash, + hasFlash + } +} \ No newline at end of file diff --git a/src/modules/wallet/components/ReceiveDialog.vue b/src/modules/wallet/components/ReceiveDialog.vue index 10d10fa..a35f9af 100644 --- a/src/modules/wallet/components/ReceiveDialog.vue +++ b/src/modules/wallet/components/ReceiveDialog.vue @@ -286,12 +286,17 @@ function formatExpiry(seconds: number): string {
-
- Lightning Invoice QR Code +
+
+ Lightning Invoice QR Code +
+

+ Click QR code to copy invoice +

diff --git a/src/modules/wallet/components/SendDialog.vue b/src/modules/wallet/components/SendDialog.vue index 9614a1f..ea74808 100644 --- a/src/modules/wallet/components/SendDialog.vue +++ b/src/modules/wallet/components/SendDialog.vue @@ -1,5 +1,5 @@