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

202 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useMemo, useState, useCallback } from 'react'
import QRCode from 'react-qr-code'
import type { AlbyInvoice } from '@/types/alby'
import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
import { AlbyInstaller } from './AlbyInstaller'
interface PaymentModalProps {
invoice: AlbyInvoice
onClose: () => void
onPaymentComplete: () => void
}
function useInvoiceTimer(expiresAt?: number) {
const [timeRemaining, setTimeRemaining] = useState<number | null>(null)
useEffect(() => {
if (!expiresAt) {
return
}
const updateTimeRemaining = () => {
const now = Math.floor(Date.now() / 1000)
const remaining = expiresAt - now
setTimeRemaining(remaining > 0 ? remaining : 0)
}
updateTimeRemaining()
const interval = setInterval(updateTimeRemaining, 1000)
return () => clearInterval(interval)
}, [expiresAt])
return timeRemaining
}
function PaymentHeader({
amount,
timeRemaining,
onClose,
}: {
amount: number
timeRemaining: number | null
onClose: () => void
}) {
const timeLabel = useMemo(() => {
if (timeRemaining === null) {
return null
}
if (timeRemaining <= 0) {
return 'Expired'
}
const minutes = Math.floor(timeRemaining / 60)
const secs = timeRemaining % 60
return `${minutes}:${secs.toString().padStart(2, '0')}`
}, [timeRemaining])
return (
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-xl font-bold">Pay {amount} sats</h2>
{timeLabel && (
<p className={`text-sm ${timeRemaining !== null && timeRemaining <= 60 ? 'text-red-600 font-semibold' : 'text-gray-600'}`}>
Time remaining: {timeLabel}
</p>
)}
</div>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 text-2xl">
×
</button>
</div>
)
}
function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paymentUrl: string }) {
return (
<div className="mb-4">
<p className="text-sm text-gray-600 mb-2">Lightning Invoice:</p>
<div className="bg-gray-100 p-3 rounded break-all text-sm font-mono mb-4">{invoiceText}</div>
<div className="flex justify-center mb-4">
<div className="bg-white p-4 rounded-lg border-2 border-gray-200">
<QRCode
value={paymentUrl}
size={200}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
viewBox="0 0 256 256"
/>
</div>
</div>
<p className="text-xs text-gray-500 text-center mb-2">Scan with your Lightning wallet to pay</p>
</div>
)
}
function PaymentActions({
copied,
onCopy,
onOpenWallet,
}: {
copied: boolean
onCopy: () => Promise<void>
onOpenWallet: () => void
}) {
return (
<div className="flex gap-2">
<button
onClick={() => {
void onCopy()
}}
className="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium transition-colors"
>
{copied ? 'Copied!' : 'Copy Invoice'}
</button>
<button
onClick={onOpenWallet}
className="flex-1 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg font-medium transition-colors"
>
Pay with Alby
</button>
</div>
)
}
function ExpiredNotice({ show }: { show: boolean }) {
if (!show) {
return null
}
return (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-700 font-semibold mb-2">This invoice has expired</p>
<p className="text-xs text-red-600">Please close this modal and try again to generate a new invoice.</p>
</div>
)
}
function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => void) {
const [copied, setCopied] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const paymentUrl = `lightning:${invoice.invoice}`
const timeRemaining = useInvoiceTimer(invoice.expiresAt)
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(invoice.invoice)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (e) {
console.error('Failed to copy:', e)
setErrorMessage('Failed to copy the invoice')
}
}, [invoice.invoice])
const handleOpenWallet = useCallback(async () => {
try {
const alby = getAlbyService()
if (!isWebLNAvailable()) {
throw new Error('WebLN is not available. Please install Alby or another Lightning wallet extension.')
}
await alby.enable()
await alby.sendPayment(invoice.invoice)
onPaymentComplete()
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e))
if (error.message.includes('user rejected') || error.message.includes('cancelled')) {
return
}
console.error('Payment failed:', error)
setErrorMessage(error.message)
}
}, [invoice.invoice, onPaymentComplete])
return { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet }
}
export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentModalProps) {
const { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet } =
usePaymentModalState(invoice, onPaymentComplete)
const handleOpenWalletSync = () => {
void handleOpenWallet()
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
<AlbyInstaller />
<PaymentHeader amount={invoice.amount} timeRemaining={timeRemaining} onClose={onClose} />
<InvoiceDisplay invoiceText={invoice.invoice} paymentUrl={paymentUrl} />
<PaymentActions
copied={copied}
onCopy={handleCopy}
onOpenWallet={handleOpenWalletSync}
/>
<ExpiredNotice show={timeRemaining !== null && timeRemaining <= 0} />
{errorMessage && (
<p className="text-xs text-red-600 mt-3 text-center" role="alert">
{errorMessage}
</p>
)}
<p className="text-xs text-gray-500 mt-4 text-center">
Payment will be automatically verified once completed
</p>
</div>
</div>
)
}