From a9c07f6af3308aeed2b1095db241020d5f040914 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 18 Sep 2025 22:09:46 +0200 Subject: [PATCH 1/8] 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 @@ @@ -163,7 +304,21 @@ const destinationType = computed(() => { - + +
+
+

Lightning Invoice

+
+ {{ displayAmount.toLocaleString() }} sats +
+
+
+ Description: {{ displayDescription }} +
+
+ + + Amount (sats) * @@ -174,7 +329,11 @@ const destinationType = computed(() => { v-bind="componentField" /> - Amount to send in satoshis + + Amount to send to Lightning address + Amount to send via LNURL + Amount to send in satoshis + From 0b98b29198e80ba6ebc26b511508c8cd03145ddd Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 18 Sep 2025 22:54:46 +0200 Subject: [PATCH 5/8] Implement payment request normalization in SendDialog - Added a new function to normalize payment requests by stripping URI prefixes (lightning: and lnurl:) and handling BIP-21 Bitcoin URIs with Lightning fallback. - Updated the destination parsing logic to utilize the normalization function, ensuring consistent handling of various payment formats. - Enhanced the payment submission process by using the normalized destination for sending payments. These changes improve the robustness of payment handling in the SendDialog, providing better support for different payment request formats. --- src/modules/wallet/components/SendDialog.vue | 32 ++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/modules/wallet/components/SendDialog.vue b/src/modules/wallet/components/SendDialog.vue index 20631e3..b963988 100644 --- a/src/modules/wallet/components/SendDialog.vue +++ b/src/modules/wallet/components/SendDialog.vue @@ -93,6 +93,30 @@ function parseBolt11Invoice(bolt11: string): ParsedInvoice | null { } } +function normalizePaymentRequest(request: string): string { + let normalized = request.trim() + const req = normalized.toLowerCase() + + // Handle Lightning URI prefix stripping (following LNbits implementation) + if (req.startsWith('lightning:')) { + normalized = normalized.slice(10) // Remove "lightning:" (10 chars) + } else if (req.startsWith('lnurl:')) { + normalized = normalized.slice(6) // Remove "lnurl:" (6 chars) + } else if (req.includes('lightning=lnurl1')) { + // Extract LNURL from lightning parameter + normalized = normalized.split('lightning=')[1].split('&')[0] + } else if (req.includes('lightning=')) { + // Handle BIP-21 Bitcoin URIs with Lightning fallback + normalized = normalized.split('lightning=')[1] + // Remove any additional query parameters + if (normalized.includes('&')) { + normalized = normalized.split('&')[0] + } + } + + return normalized.trim() +} + function parsePaymentDestination(destination: string) { // Clear previous parsing results parsedInvoice.value = null @@ -101,7 +125,8 @@ function parsePaymentDestination(destination: string) { if (!destination.trim()) return - const cleanDest = destination.trim() + // Normalize the destination by stripping URI prefixes + const cleanDest = normalizePaymentRequest(destination) if (isBolt11(cleanDest)) { paymentType.value = 'bolt11' @@ -198,8 +223,11 @@ const effectiveAmount = computed(() => { // Methods const onSubmit = form.handleSubmit(async (formValues) => { try { + // Use normalized destination (stripped of URI prefixes) + const normalizedDestination = normalizePaymentRequest(formValues.destination) + const success = await walletService.sendPayment({ - destination: formValues.destination, + destination: normalizedDestination, amount: effectiveAmount.value, // Use computed effective amount comment: formValues.comment || undefined }) From d19f6ac685e61c9df4e3756023fb829a6f592559 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 18 Sep 2025 22:57:20 +0200 Subject: [PATCH 6/8] Refactor SendDialog form validation schema for improved clarity - Simplified the form validation schema to always include an optional amount field, enhancing consistency across payment types. - Updated the custom validation logic to ensure proper handling of required fields based on payment type, improving user experience. - Adjusted the effective amount computation to default to zero when no amount is provided, ensuring accurate state management. These changes streamline the form validation process, making it more intuitive for users while maintaining robust payment handling. --- src/modules/wallet/components/SendDialog.vue | 51 ++++++++++---------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/modules/wallet/components/SendDialog.vue b/src/modules/wallet/components/SendDialog.vue index b963988..937e393 100644 --- a/src/modules/wallet/components/SendDialog.vue +++ b/src/modules/wallet/components/SendDialog.vue @@ -142,27 +142,12 @@ function parsePaymentDestination(destination: string) { } } -// Dynamic form validation schema based on payment type -const formSchema = computed(() => { - const baseSchema = { - destination: z.string().min(1, "Destination is required"), - comment: z.string().optional() - } - - // Only require amount for LNURL, Lightning addresses, or zero-amount invoices - const needsAmountInput = paymentType.value === 'lnurl' || - paymentType.value === 'lightning-address' || - (paymentType.value === 'bolt11' && parsedInvoice.value?.amount === 0) - - if (needsAmountInput) { - return toTypedSchema(z.object({ - ...baseSchema, - amount: z.number().min(1, "Amount must be at least 1 sat").max(1000000, "Amount too large") - })) - } else { - return toTypedSchema(z.object(baseSchema)) - } -}) +// Form validation schema - always include amount but make it optional +const formSchema = toTypedSchema(z.object({ + destination: z.string().min(1, "Destination is required"), + amount: z.number().min(1, "Amount must be at least 1 sat").max(1000000, "Amount too large").optional(), + comment: z.string().optional() +})) // Form setup with dynamic schema const form = useForm({ @@ -174,8 +159,24 @@ const form = useForm({ } }) -const { resetForm, values, meta, setFieldValue } = form -const isFormValid = computed(() => meta.value.valid) +const { resetForm, values, setFieldValue } = form +// Custom validation logic considering payment type +const isFormValid = computed(() => { + // Check if destination is valid + if (!values.destination || values.destination.trim() === '') return false + + // Check if amount is required and valid + const needsAmountInput = paymentType.value === 'lnurl' || + paymentType.value === 'lightning-address' || + (paymentType.value === 'bolt11' && parsedInvoice.value?.amount === 0) + + if (needsAmountInput) { + return !!(values.amount && values.amount > 0 && values.amount <= 1000000) + } + + // For BOLT11 invoices with fixed amounts, no amount input needed + return true +}) // Watch for prop changes watch(() => props.initialDestination, (newDestination) => { @@ -186,7 +187,7 @@ watch(() => props.initialDestination, (newDestination) => { // Watch destination changes to parse payment type watch(() => values.destination, (newDestination) => { - parsePaymentDestination(newDestination) + parsePaymentDestination(newDestination || '') }, { immediate: true }) // State @@ -217,7 +218,7 @@ const effectiveAmount = computed(() => { if (parsedInvoice.value && parsedInvoice.value.amount > 0) { return parsedInvoice.value.amount } - return values.amount + return values.amount || 0 }) // Methods From 95324c1260a202facfffb68561385a16dfad27c7 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 18 Sep 2025 23:07:23 +0200 Subject: [PATCH 7/8] Fix handleQRClick function closure in WalletPage.vue - Closed the handleQRClick function properly by adding a closing brace, ensuring correct function definition and preventing potential runtime errors. This change enhances the code structure and maintains the integrity of the QR code handling functionality. --- src/modules/wallet/views/WalletPage.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/wallet/views/WalletPage.vue b/src/modules/wallet/views/WalletPage.vue index ef733d5..b285d14 100644 --- a/src/modules/wallet/views/WalletPage.vue +++ b/src/modules/wallet/views/WalletPage.vue @@ -148,6 +148,7 @@ function handleQRClick() { const encodedLNURL = encodeLNURL(firstPayLink.value.lnurl) copyToClipboard(encodedLNURL, 'qr-lnurl') } +} // QR Scanner functions function closeQRScanner() { From de9a8efc2d32bf805f6dd451c18236c9b5378595 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 18 Sep 2025 23:23:30 +0200 Subject: [PATCH 8/8] Enhance README and wallet module documentation with new payment features - Updated README.md to include new capabilities for Lightning invoice creation, smart payment scanning, universal payment support, and smart amount fields. - Enhanced wallet-module documentation to reflect comprehensive Lightning Network wallet functionality, including detailed descriptions of payment processing, QR code scanning, and invoice management features. - Added new sections for QR scanner component and payment flow architecture, improving clarity and usability for developers. These changes provide clearer guidance on the application's features and improve the overall documentation quality. --- README.md | 4 + docs/02-modules/wallet-module/index.md | 205 +++++++++++++++++++------ 2 files changed, 160 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index d595888..f423590 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ A modular Vue 3 + TypeScript application with Nostr protocol integration and Lig The application provides seamless Lightning Network wallet integration: +- **Lightning Invoice Creation**: Create BOLT11 invoices for receiving payments with QR codes +- **Smart Payment Scanning**: QR code scanner for Lightning invoices, LNURL, and Lightning addresses +- **Universal Payment Support**: Send to Lightning invoices, Lightning addresses (user@domain.com), and LNURL +- **Smart Amount Fields**: Amount input only appears when needed (LNURL, Lightning addresses, or zero-amount invoices) - **Instant Balance Updates**: WebSocket connection provides real-time balance updates when payments are sent or received - **Live Notifications**: Toast notifications for incoming payments - **Connection Management**: Automatic reconnection with exponential backoff diff --git a/docs/02-modules/wallet-module/index.md b/docs/02-modules/wallet-module/index.md index 84de301..5b700bf 100644 --- a/docs/02-modules/wallet-module/index.md +++ b/docs/02-modules/wallet-module/index.md @@ -19,10 +19,12 @@ The Wallet Module provides comprehensive Lightning Network wallet functionality for the Ario application. It integrates with LNbits to handle payments, manage wallet balances, and provide real-time updates through WebSocket connections. ### **Key Capabilities** +- **Lightning invoice creation** (BOLT11) for receiving payments with QR codes +- **QR code scanning** for Lightning invoices, LNURL, and Lightning addresses +- **Smart payment processing** with automatic payment type detection +- **Universal payment support** for Lightning invoices, Lightning addresses, and LNURL - **Real-time balance updates** via WebSocket connection to LNbits -- **Payment processing** for Lightning invoices, Lightning addresses, and LNURL - **Transaction management** with comprehensive history and status tracking -- **Pay link creation** for receiving payments with LNURL and Lightning addresses - **Multi-wallet support** with automatic wallet selection - **Battery optimization** through VisibilityService integration @@ -43,11 +45,14 @@ Automatic wallet balance synchronization without page refreshes: - **Connection Management** - Automatic reconnection with exponential backoff ### **💸 Payment Processing** -Comprehensive Lightning payment handling: +Comprehensive Lightning payment handling with smart detection: -- **Lightning Invoices** - Pay BOLT11 invoices directly +- **Lightning Invoices** - Pay BOLT11 invoices directly with amount detection - **Lightning Addresses** - Send to Lightning addresses (user@domain.com) - **LNURL Pay** - Support for LNURL payment requests +- **QR Code Scanning** - Camera-based scanning for all payment types +- **Smart Amount Fields** - Amount input only appears when needed (LNURL, Lightning addresses, zero-amount invoices) +- **Lightning URI Support** - Handles "lightning:" prefixes and BIP-21 URIs - **Payment Validation** - Pre-flight checks for sufficient balance - **Error Handling** - User-friendly error messages and recovery @@ -60,14 +65,16 @@ Complete transaction history and tracking: - **Transaction Types** - Sent, received, pending, confirmed, failed - **Search and Filter** - Find specific transactions quickly -### **🔗 Payment Links (LNURL)** -Create reusable payment endpoints: +### **⚡ Lightning Invoice Creation** +Create BOLT11 invoices for receiving payments: -- **LNURL Generation** - Create payment links for receiving -- **Lightning Addresses** - Generate username@domain addresses -- **Amount Ranges** - Set minimum and maximum payment amounts -- **Comments Support** - Allow payment comments -- **Link Management** - Create, view, and delete payment links +- **BOLT11 Invoice Generation** - Create Lightning invoices with specified amounts +- **QR Code Generation** - Automatic QR code creation for easy sharing +- **Expiry Management** - Configurable invoice expiration times +- **Description Support** - Add payment descriptions and memos +- **Real-time Status** - Live payment status updates (pending, paid, expired) +- **Mobile Optimization** - Responsive design for mobile devices +- **Copy Functionality** - Easy copying of invoice strings and payment hashes ## Architecture @@ -82,13 +89,25 @@ src/modules/wallet/services/ ### **Component Layer** ``` src/modules/wallet/components/ -├── SendDialog.vue # Payment sending interface -├── ReceiveDialog.vue # Payment receiving interface +├── SendDialog.vue # Payment sending interface with QR scanning +├── ReceiveDialog.vue # Lightning invoice creation interface ├── TransactionList.vue # Transaction history display -├── PayLinkManager.vue # LNURL payment link management └── BalanceDisplay.vue # Wallet balance component ``` +### **UI Components** +``` +src/components/ui/ +├── qr-scanner.vue # Camera-based QR code scanner +└── ... # Other Shadcn/UI components +``` + +### **Composables** +``` +src/composables/ +└── useQRScanner.ts # QR scanner functionality with camera access +``` + ### **Views Layer** ``` src/modules/wallet/views/ @@ -150,27 +169,25 @@ if (payment.amount < 0) { **Token:** `SERVICE_TOKENS.WALLET_SERVICE` **Key Features:** -- Send Lightning payments (invoices, addresses, LNURL) -- Create and manage LNURL payment links -- Transaction history management -- Payment link generation with Lightning addresses +- Send Lightning payments (invoices, addresses, LNURL) with smart type detection +- Create Lightning invoices (BOLT11) for receiving payments +- Transaction history management with real-time updates +- QR code generation for payment sharing **Reactive State:** ```typescript interface WalletService { transactions: ComputedRef // Transaction history - payLinks: ComputedRef // Created payment links - isCreatingPayLink: ComputedRef // Pay link creation state + isCreatingInvoice: ComputedRef // Invoice creation state isSendingPayment: ComputedRef // Payment sending state error: ComputedRef // Error state } ``` **Key Methods:** -- `sendPayment(request: SendPaymentRequest)` - Send Lightning payment -- `createReceiveAddress(params)` - Create LNURL payment link -- `deletePayLink(linkId: string)` - Remove payment link -- `refresh()` - Reload transactions and payment links +- `sendPayment(request: SendPaymentRequest)` - Send Lightning payment with type detection +- `createInvoice(params: CreateInvoiceRequest)` - Create BOLT11 Lightning invoice +- `refresh()` - Reload transactions and balance data ### **WalletWebSocketService** ⚡ **Purpose:** Real-time wallet balance updates via WebSocket @@ -217,36 +234,37 @@ The wallet module integrates with the core PaymentService: ## Components ### **SendDialog.vue** 📤 -Payment sending interface with comprehensive validation: +Payment sending interface with smart payment detection and QR scanning: **Features:** -- Support for Lightning invoices, addresses, and LNURL -- Amount validation against available balance +- QR code scanner for Lightning invoices, LNURL, and Lightning addresses +- Automatic payment type detection (BOLT11, LNURL, Lightning address) +- Smart amount field visibility (only shows when needed) +- Lightning URI prefix handling ("lightning:" prefixes, BIP-21 URIs) - Payment request parsing and validation -- Real-time fee estimation +- Amount validation against available balance - Error handling with user-friendly messages **Form Fields:** -- Destination (invoice, address, or LNURL) -- Amount (when not specified in invoice) -- Comment (for Lightning addresses) +- Destination (invoice, address, or LNURL) with QR scanner button +- Amount (conditionally visible based on payment type) +- Comment (for Lightning addresses and LNURL) ### **ReceiveDialog.vue** 📥 -Payment receiving interface for creating payment requests: +Lightning invoice creation interface for receiving payments: **Features:** -- LNURL payment link creation -- Lightning address generation -- Customizable amount ranges -- Comment support configuration -- QR code generation for sharing +- BOLT11 Lightning invoice generation +- QR code generation for easy sharing +- Real-time payment status tracking (pending, paid, expired) +- Mobile-responsive design with optimized layouts +- Copy functionality for invoice string and payment hash +- Payment success/failure indicators +- Configurable invoice expiry times **Form Fields:** -- Description -- Minimum amount -- Maximum amount -- Username (for Lightning address) -- Allow comments toggle +- Amount (required, in satoshis) +- Description/Memo (optional payment description) ### **TransactionList.vue** 📋 Comprehensive transaction history display: @@ -268,17 +286,48 @@ Wallet balance component with real-time updates: - Loading state indicators - Connection status awareness +### **QRScanner Component** 📷 +Camera-based QR code scanning for payments: + +**Features:** +- WebRTC camera access with permission handling +- Real-time QR code detection using qr-scanner library +- Support for all Lightning payment types (BOLT11, LNURL, Lightning addresses) +- Lightning URI prefix cleaning ("lightning:" removal) +- Professional camera interface with scanning overlays +- Flash/torch support for low-light conditions +- Error handling for camera access issues +- Mobile-optimized scanning experience + +**Technical Details:** +- Uses `qr-scanner` library for robust QR detection +- Handles camera permissions with user-friendly error messages +- Automatic cleanup on component unmount +- Configurable scan rate and highlighting options + ## Payment Processing ### **Payment Flow Architecture** ``` -User Action ──→ Validation ──→ Payment ──→ Confirmation ──→ UI Update - │ │ │ │ │ - │ │ │ │ └─ Real-time via WebSocket - │ │ │ └─ Transaction recorded - │ │ └─ LNbits API call - │ └─ Balance check, format validation - └─ Send/Receive dialog interaction +User Action ──→ Type Detection ──→ Validation ──→ Payment ──→ Confirmation ──→ UI Update + │ │ │ │ │ │ + │ │ │ │ │ └─ Real-time via WebSocket + │ │ │ │ └─ Transaction recorded + │ │ │ └─ LNbits API call + │ │ └─ Balance check, format validation + │ └─ BOLT11/LNURL/Lightning address detection + └─ Send/Receive dialog or QR scan +``` + +### **QR Code Scanning Flow** +``` +Camera Access ──→ QR Detection ──→ URI Cleaning ──→ Type Detection ──→ Form Population + │ │ │ │ │ + │ │ │ │ └─ Auto-fill destination field + │ │ │ └─ BOLT11/LNURL/Lightning address + │ │ └─ Remove "lightning:" prefixes + │ └─ Real-time scanning with qr-scanner + └─ Permission handling with user-friendly errors ``` ### **Payment Types Supported** @@ -311,6 +360,33 @@ await walletService.sendPayment({ }) ``` +### **Lightning Invoice Creation** +Create BOLT11 invoices for receiving payments: + +#### **Invoice Creation** +```typescript +// Create Lightning invoice +const invoice = await walletService.createInvoice({ + amount: 1000, // Amount in satoshis + memo: "Payment for services", // Optional description + expiry: 3600 // Optional expiry in seconds (default: 1 hour) +}) + +// Returns invoice object with: +// - payment_request: BOLT11 invoice string +// - payment_hash: Payment hash for tracking +// - amount: Invoice amount in sats +// - memo: Invoice description +// - expiry: Expiration time +``` + +#### **QR Code Generation** +```typescript +// QR code is automatically generated for created invoices +// Available in the UI for easy sharing +const qrCodeDataUrl = await paymentService.generateQRCode(invoice.payment_request) +``` + ### **Error Handling** Comprehensive error handling for payment failures: @@ -468,6 +544,37 @@ describe('WalletService', () => { const result = await service.sendPayment(request) expect(result).toBe(true) }) + + it('should create Lightning invoice', async () => { + const invoiceData = { + amount: 1000, + memo: 'Test payment' + } + + const invoice = await service.createInvoice(invoiceData) + expect(invoice.payment_request).toMatch(/^lnbc/) + expect(invoice.amount).toBe(1000) + }) +}) +``` + +#### **QR Scanner Tests** +```typescript +describe('QRScanner', () => { + it('should detect BOLT11 invoices', async () => { + const invoice = 'lnbc1000n1p3xh4v...' + const result = parsePaymentDestination(invoice) + + expect(result.type).toBe('bolt11') + expect(result.amount).toBe(100) + }) + + it('should clean Lightning URIs', () => { + const uri = 'lightning:lnbc1000n1p3xh4v...' + const cleaned = normalizePaymentRequest(uri) + + expect(cleaned).toBe('lnbc1000n1p3xh4v...') + }) }) ```