Nicolas Cantu 90ff8282f1 feat: Implémentation système de commissions systématique et incontournable
- Création lib/platformCommissions.ts : configuration centralisée des commissions
  - Articles : 800 sats (700 auteur, 100 plateforme)
  - Avis : 70 sats (49 lecteur, 21 plateforme)
  - Sponsoring : 0.046 BTC (0.042 auteur, 0.004 plateforme)

- Validation des montants à chaque étape :
  - Publication : vérification du montant avant publication
  - Paiement : vérification du montant avant acceptation
  - Erreurs explicites si montant incorrect

- Tracking des commissions sur Nostr :
  - Tags author_amount et platform_commission dans événements
  - Interface ContentDeliveryTracking étendue
  - Traçabilité complète pour audit

- Logs structurés avec informations de commission
- Documentation complète du système

Les commissions sont maintenant systématiques, validées et traçables.
2025-12-27 21:11:09 +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">Zap de {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>
)
}