import { ref, computed } from 'vue' import { BaseService } from '@/core/base/BaseService' import { payInvoiceWithWallet } from '@/lib/api/events' import { toast } from 'vue-sonner' export interface PaymentResult { payment_hash: string fee_msat: number preimage: string } export interface QRCodeOptions { width?: number margin?: number darkColor?: string lightColor?: string } export interface PaymentOptions { successMessage?: string errorMessage?: string showToast?: boolean } /** * Centralized Payment Service * Handles Lightning payments, QR code generation, and wallet management */ export class PaymentService extends BaseService { // Service metadata protected readonly metadata = { name: 'PaymentService', version: '1.0.0', dependencies: ['AuthService'] } // Payment state private _isProcessingPayment = ref(false) private _paymentError = ref(null) // Public reactive state public readonly isProcessingPayment = computed(() => this._isProcessingPayment.value) public readonly paymentError = computed(() => this._paymentError.value) /** * Service-specific initialization (called by BaseService) */ protected async onInitialize(): Promise { // Reset payment state on initialization to prevent stuck states this.resetPaymentState() this.debug('PaymentService initialized with clean state') } /** * Get user wallets from authenticated user */ get userWallets() { return this.authService?.user?.value?.wallets || [] } /** * Check if user has any wallet with balance */ get hasWalletWithBalance(): boolean { return this.userWallets.some((wallet: any) => wallet.balance_msat > 0) } /** * Find wallet with sufficient balance for payment */ getWalletWithBalance(requiredAmountSats?: number): any | null { const wallets = this.userWallets if (!wallets.length) return null if (requiredAmountSats) { // Convert sats to msat for comparison const requiredMsat = requiredAmountSats * 1000 return wallets.find((wallet: any) => wallet.balance_msat >= requiredMsat) } return wallets.find((wallet: any) => wallet.balance_msat > 0) } /** * Get the preferred wallet for operations (always use wallets[0] for consistency) * This ensures consistent wallet selection across all modules */ getPreferredWallet(): any | null { const wallets = this.userWallets if (!wallets.length) return null return wallets[0] } /** * Get the preferred wallet's admin key for administrative operations */ getPreferredWalletAdminKey(): string | null { const wallet = this.getPreferredWallet() return wallet?.adminkey || null } /** * Get the preferred wallet's invoice key for read-only operations */ getPreferredWalletInvoiceKey(): string | null { const wallet = this.getPreferredWallet() return wallet?.inkey || null } /** * Generate QR code for Lightning payment request */ async generateQRCode( paymentRequest: string, options: QRCodeOptions = {} ): Promise { try { const qrcode = await import('qrcode') const dataUrl = await qrcode.toDataURL(paymentRequest, { width: options.width || 256, margin: options.margin || 2, color: { dark: options.darkColor || '#000000', light: options.lightColor || '#FFFFFF' } }) return dataUrl } catch (error) { const err = this.handleError(error, 'generateQRCode') throw err } } /** * Generate QR code for ticket or other custom data */ async generateCustomQRCode( data: string, options: QRCodeOptions = {} ): Promise { try { const qrcode = await import('qrcode') const dataUrl = await qrcode.toDataURL(data, { width: options.width || 128, margin: options.margin || 2, color: { dark: options.darkColor || '#000000', light: options.lightColor || '#FFFFFF' } }) return dataUrl } catch (error) { const err = this.handleError(error, 'generateCustomQRCode') throw err } } /** * Pay Lightning invoice with user's wallet */ async payWithWallet( paymentRequest: string, requiredAmountSats?: number, options: PaymentOptions = {} ): Promise { // Check authentication if (!this.authService?.isAuthenticated?.value || !this.authService?.user?.value) { throw new Error('User must be authenticated to pay with wallet') } // Find suitable wallet const wallet = this.getWalletWithBalance(requiredAmountSats) if (!wallet) { throw new Error('No wallet with sufficient balance found') } try { this._isProcessingPayment.value = true this._paymentError.value = null this.debug(`Paying invoice with wallet: ${wallet.id.slice(0, 8)}`) // Make payment const paymentResult = await payInvoiceWithWallet( paymentRequest, wallet.id, wallet.adminkey ) this.debug('Payment successful', { paymentHash: paymentResult.payment_hash, feeMsat: paymentResult.fee_msat }) // Show success notification if enabled if (options.showToast !== false) { const message = options.successMessage || 'Payment successful!' toast.success(message) } return paymentResult } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Payment failed' this._paymentError.value = errorMessage this.debug('Payment failed', error) // Show error notification if enabled if (options.showToast !== false) { const message = options.errorMessage || errorMessage toast.error('Payment failed', { description: message }) } throw error } finally { this._isProcessingPayment.value = false } } /** * Open external Lightning wallet */ openExternalWallet(paymentRequest: string): void { if (!paymentRequest) { toast.error('No payment request available') return } try { const lightningUrl = `lightning:${paymentRequest}` window.open(lightningUrl, '_blank') toast.info('Opening Lightning wallet...', { description: 'If no wallet opens, copy the payment request manually' }) } catch (error) { console.warn('Failed to open lightning: URL:', error) // Fallback: copy to clipboard navigator.clipboard?.writeText(paymentRequest).then(() => { toast.success('Payment request copied to clipboard') }).catch(() => { toast.info('Please copy the payment request manually') }) } } /** * Handle payment with automatic fallback * Tries wallet payment first, falls back to external wallet */ async handlePayment( paymentRequest: string, requiredAmountSats?: number, options: PaymentOptions = {} ): Promise { if (!paymentRequest) { toast.error('No payment request available') return null } // Try wallet payment first if user has balance if (this.hasWalletWithBalance) { try { return await this.payWithWallet( paymentRequest, requiredAmountSats, options ) } catch (error) { this.debug('Wallet payment failed, offering external wallet option:', error) // Don't throw here, continue to external wallet option } } // Fallback to external wallet this.openExternalWallet(paymentRequest) return null // External wallet payment status unknown } /** * Clear payment error state */ clearPaymentError(): void { this._paymentError.value = null } /** * Reset payment state */ resetPaymentState(): void { this._isProcessingPayment.value = false this._paymentError.value = null this.debug('Payment state reset to clean state') } /** * Force reset payment state (public method for debugging) */ forceResetPaymentState(): void { console.log('Force resetting payment state from:', this._isProcessingPayment.value) this._isProcessingPayment.value = false this._paymentError.value = null console.log('Payment state after force reset:', this._isProcessingPayment.value) } /** * Cleanup when service is disposed (called by BaseService) */ protected async onDispose(): Promise { this.resetPaymentState() this.debug('PaymentService disposed') } } // Export singleton instance export const paymentService = new PaymentService()