Nicolas Cantu 3000872dbc refactoring
- **Motivations :** Assurer passage du lint strict et clarifier la logique paiements/publications.

- **Root causes :** Fonctions trop longues, promesses non gérées et typages WebLN/Nostr incomplets.

- **Correctifs :** Refactor PaymentModal (handlers void), extraction helpers articlePublisher, simplification polling sponsoring/zap, corrections curly et awaits.

- **Evolutions :** Nouveau module articlePublisherHelpers pour présentation/aiguillage contenu privé.

- **Page affectées :** components/PaymentModal.tsx, lib/articlePublisher.ts, lib/articlePublisherHelpers.ts, lib/paymentPolling.ts, lib/sponsoring.ts, lib/nostrZapVerification.ts et dépendances liées.
2025-12-22 17:56:00 +01:00

216 lines
6.0 KiB
TypeScript

import type {
AlbyInvoice,
AlbyPaymentStatus,
AlbyInvoiceRequest,
WebLNProvider,
} from '@/types/alby'
import { retryWithBackoff, isRetryableNetworkError } from './retry'
/**
* Check if WebLN (Lightning wallet) is available in the browser
*/
export function isWebLNAvailable(): boolean {
return typeof window !== 'undefined' && typeof window.webln !== 'undefined'
}
/**
* Get WebLN provider (Alby or other Lightning wallet)
*/
function getWebLN(): WebLNProvider {
if (typeof window === 'undefined') {
throw new Error('WebLN is only available in the browser')
}
if (!window.webln) {
throw new Error('WebLN provider not available')
}
return window.webln
}
/**
* Alby/WebLN Service
* Handles Lightning payments using the WebLN standard (works with Alby and other Lightning wallets)
*/
export class AlbyService {
/**
* Enable WebLN provider (request permission from user)
*/
async enable(): Promise<void> {
if (!isWebLNAvailable()) {
throw new Error('WebLN provider not available. Please install Alby or another Lightning wallet extension.')
}
const webln = getWebLN()
if (webln.enabled) {
return
}
try {
await webln.enable()
} catch (_error) {
throw new Error('User rejected WebLN permission request')
}
}
/**
* Check if WebLN is enabled
*/
isEnabled(): boolean {
if (!isWebLNAvailable()) {
return false
}
return getWebLN().enabled === true
}
/**
* Generate a Lightning invoice
* This will prompt the user's wallet to create an invoice
*/
async createInvoice(request: AlbyInvoiceRequest): Promise<AlbyInvoice> {
await this.enable()
const webln = getWebLN()
return retryWithBackoff(
async () => {
try {
// Use makeInvoice method from WebLN
const invoiceResponse = await webln.makeInvoice({
amount: request.amount,
defaultMemo: request.description ?? 'zapwall4Science Payment',
})
// Extract payment hash from invoice using a simple approach
// In production, decode BOLT11 properly
const paymentHash = this.generatePaymentHash(invoiceResponse.paymentRequest)
return {
invoice: invoiceResponse.paymentRequest,
paymentHash,
amount: request.amount,
expiresAt: Math.floor(Date.now() / 1000) + (request.expiry ?? 3600),
}
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
console.error('Alby createInvoice error:', err)
throw new Error(`Failed to create invoice: ${err.message}`)
}
},
{
maxRetries: 3,
initialDelay: 1000,
retryable: isRetryableNetworkError,
}
)
}
/**
* Request payment (user pays an invoice)
*/
async sendPayment(invoice: string): Promise<{ preimage: string }> {
await this.enable()
const webln = getWebLN()
return retryWithBackoff(
async () => {
try {
const response = await webln.sendPayment(invoice)
return { preimage: response.preimage }
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
console.error('Alby sendPayment error:', err)
throw new Error(`Failed to send payment: ${err.message}`)
}
},
{
maxRetries: 2, // Fewer retries for payment operations
initialDelay: 2000,
retryable: isRetryableNetworkError,
}
)
}
/**
* Check payment status by verifying if invoice is paid
* Note: With WebLN, we typically verify payment by checking zap receipts on Nostr
* since WebLN doesn't provide a direct way to check invoice status
*/
checkPaymentStatus(paymentHash: string): AlbyPaymentStatus {
// WebLN doesn't have a direct method to check payment status
// We'll rely on Nostr zap receipts for verification
return {
paid: false,
paymentHash,
}
}
/**
* Wait for payment completion
* Since WebLN doesn't provide payment status checking,
* we'll rely on zap receipt verification on Nostr
*/
waitForPayment(
paymentHash: string,
timeout: number = 300000, // 5 minutes default
interval: number = 2000 // 2 seconds default
): Promise<AlbyPaymentStatus> {
// With WebLN, payment verification is typically done via zap receipts
// This method is kept for API compatibility but should be used with zap receipt verification
const startTime = Date.now()
return new Promise((resolve) => {
const checkPayment = () => {
try {
const status = this.checkPaymentStatus(paymentHash)
if (status.paid) {
resolve(status)
return
}
if (Date.now() - startTime > timeout) {
resolve({ paid: false, paymentHash })
return
}
setTimeout(checkPayment, interval)
} catch (_error) {
resolve({ paid: false, paymentHash })
}
}
checkPayment()
})
}
/**
* Generate a simple payment hash identifier from invoice
* Note: This is not the actual payment hash from the invoice
* In production, decode BOLT11 properly or use zap receipts for verification
*/
private generatePaymentHash(invoice: string): string {
// Generate a simple hash-like identifier
// In practice, payment verification should be done via zap receipts
if (typeof window !== 'undefined' && window.crypto) {
// Use a simple hash for identification
let hash = 0
for (let i = 0; i < invoice.length; i++) {
const char = invoice.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // Convert to 32-bit integer
}
return Math.abs(hash).toString(16).padStart(16, '0')
}
return Date.now().toString(16)
}
}
// Singleton instance
let albyServiceInstance: AlbyService | null = null
export function getAlbyService(): AlbyService {
albyServiceInstance ??= new AlbyService()
return albyServiceInstance
}