- **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.
175 lines
4.5 KiB
TypeScript
175 lines
4.5 KiB
TypeScript
import type { Event } from 'nostr-tools'
|
|
import { nostrService } from './nostr'
|
|
import { zapVerificationService } from './zapVerification'
|
|
import type { Notification } from '@/types/notifications'
|
|
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
|
|
|
|
const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io'
|
|
|
|
function createZapReceiptFilters(userPubkey: string) {
|
|
return [
|
|
{
|
|
kinds: [9735], // Zap receipt
|
|
'#p': [userPubkey], // Receipts targeting this user
|
|
},
|
|
]
|
|
}
|
|
|
|
async function buildPaymentNotification(event: Event, userPubkey: string): Promise<Notification | null> {
|
|
const paymentInfo = zapVerificationService.extractPaymentInfo(event)
|
|
|
|
if (paymentInfo?.recipient !== userPubkey) {
|
|
return null
|
|
}
|
|
|
|
let articleTitle: string | undefined
|
|
if (paymentInfo.articleId) {
|
|
try {
|
|
const article = await nostrService.getArticleById(paymentInfo.articleId)
|
|
articleTitle = article?.title
|
|
} catch (e) {
|
|
console.error('Error loading article for notification:', e)
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: event.id,
|
|
type: 'payment',
|
|
title: 'New Payment Received',
|
|
message: articleTitle
|
|
? `You received ${paymentInfo.amount} sats for "${articleTitle}"`
|
|
: `You received ${paymentInfo.amount} sats`,
|
|
timestamp: event.created_at,
|
|
read: false,
|
|
articleId: paymentInfo.articleId ?? undefined,
|
|
articleTitle,
|
|
amount: paymentInfo.amount,
|
|
fromPubkey: paymentInfo.payer,
|
|
}
|
|
}
|
|
|
|
function registerZapSubscription(
|
|
sub: ReturnType<SimplePoolWithSub['sub']>,
|
|
userPubkey: string,
|
|
onNotification: (notification: Notification) => void
|
|
) {
|
|
sub.on('event', (event: Event) => {
|
|
void buildPaymentNotification(event, userPubkey)
|
|
.then((notification) => {
|
|
if (notification) {
|
|
onNotification(notification)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error processing zap receipt notification:', error)
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Service for monitoring and managing notifications
|
|
*/
|
|
export class NotificationService {
|
|
private subscriptions: Map<string, () => void> = new Map()
|
|
|
|
/**
|
|
* Subscribe to zap receipts (payments) for a user's articles
|
|
*/
|
|
subscribeToPayments(
|
|
userPubkey: string,
|
|
onNotification: (notification: Notification) => void
|
|
): () => void {
|
|
const pool = nostrService.getPool()
|
|
if (!pool) {
|
|
return () => {}
|
|
}
|
|
|
|
const filters = createZapReceiptFilters(userPubkey)
|
|
const poolWithSub = pool as SimplePoolWithSub
|
|
const sub = poolWithSub.sub([RELAY_URL], filters)
|
|
|
|
registerZapSubscription(sub, userPubkey, onNotification)
|
|
|
|
const unsubscribe = () => {
|
|
sub.unsub()
|
|
}
|
|
|
|
return unsubscribe
|
|
}
|
|
|
|
/**
|
|
* Stop all subscriptions
|
|
*/
|
|
stopAll(): void {
|
|
this.subscriptions.forEach((unsubscribe) => unsubscribe())
|
|
this.subscriptions.clear()
|
|
}
|
|
}
|
|
|
|
export const notificationService = new NotificationService()
|
|
|
|
/**
|
|
* Load stored notifications from localStorage
|
|
*/
|
|
export function loadStoredNotifications(userPubkey: string): Notification[] {
|
|
try {
|
|
const key = `notifications_${userPubkey}`
|
|
const stored = localStorage.getItem(key)
|
|
if (stored) {
|
|
return JSON.parse(stored) as Notification[]
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading stored notifications:', error)
|
|
}
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Save notifications to localStorage
|
|
*/
|
|
export function saveNotifications(userPubkey: string, notifications: Notification[]): void {
|
|
try {
|
|
const key = `notifications_${userPubkey}`
|
|
localStorage.setItem(key, JSON.stringify(notifications))
|
|
} catch (error) {
|
|
console.error('Error saving notifications:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark notification as read
|
|
*/
|
|
export function markNotificationAsRead(
|
|
userPubkey: string,
|
|
notificationId: string,
|
|
notifications: Notification[]
|
|
): Notification[] {
|
|
const updated = notifications.map((n) =>
|
|
n.id === notificationId ? { ...n, read: true } : n
|
|
)
|
|
saveNotifications(userPubkey, updated)
|
|
return updated
|
|
}
|
|
|
|
/**
|
|
* Mark all notifications as read
|
|
*/
|
|
export function markAllAsRead(userPubkey: string, notifications: Notification[]): Notification[] {
|
|
const updated = notifications.map((n) => ({ ...n, read: true }))
|
|
saveNotifications(userPubkey, updated)
|
|
return updated
|
|
}
|
|
|
|
/**
|
|
* Delete a notification
|
|
*/
|
|
export function deleteNotification(
|
|
userPubkey: string,
|
|
notificationId: string,
|
|
notifications: Notification[]
|
|
): Notification[] {
|
|
const updated = notifications.filter((n) => n.id !== notificationId)
|
|
saveNotifications(userPubkey, updated)
|
|
return updated
|
|
}
|