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 @@