web-app/src/core/services/PaymentService.ts
padreug e5db949aae Refactor wallet balance handling and integrate PaymentService for centralized management
- Replaced direct wallet balance computation in Navbar and WalletPage with a centralized totalBalance property from PaymentService, improving code maintainability.
- Updated CreateProductDialog, CreateStoreDialog, and MerchantStore components to utilize PaymentService for retrieving wallet admin and invoice keys, enhancing consistency across the application.
- These changes streamline wallet management and improve the overall architecture of the wallet module.
2025-09-17 20:23:46 +02:00

329 lines
No EOL
8.8 KiB
TypeScript

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<string | null>(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<void> {
// 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 || []
}
/**
* Get total balance across all user wallets in millisatoshis
*/
get totalBalance(): number {
return this.userWallets.reduce((total: number, wallet: any) => {
return total + (wallet.balance_msat || 0)
}, 0)
}
/**
* 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<string> {
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<string> {
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<PaymentResult> {
// 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<PaymentResult | null> {
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<void> {
this.resetPaymentState()
this.debug('PaymentService disposed')
}
}
// Export singleton instance
export const paymentService = new PaymentService()