story-research-zapwall/lib/nostrconnect.ts
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

191 lines
4.9 KiB
TypeScript

import type { NostrConnectState } from '@/types/nostr'
import { nostrService } from './nostr'
import { handleNostrConnectMessage } from './nostrconnectHandler'
// NostrConnect uses NIP-46 protocol
// use.nsec.app provides a bridge for remote signing
const NOSTRCONNECT_BRIDGE = process.env.NEXT_PUBLIC_NOSTRCONNECT_BRIDGE ?? 'https://use.nsec.app'
export class NostrConnectService {
private state: NostrConnectState = {
connected: false,
pubkey: null,
profile: null,
}
private listeners: Set<(state: NostrConnectState) => void> = new Set()
private relayUrl: string = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io'
constructor() {
if (typeof window !== 'undefined') {
this.loadStateFromStorage()
this.setupMessageListener()
}
}
subscribe(callback: (state: NostrConnectState) => void): () => void {
this.listeners.add(callback)
callback(this.state)
return () => {
this.listeners.delete(callback)
}
}
getState(): NostrConnectState {
return { ...this.state }
}
private createConnectUrl(): string {
const appName = 'zapwall4Science'
const appUrl = window.location.origin
const params = new URLSearchParams({
origin: appUrl,
name: appName,
relay: this.relayUrl,
})
return `${NOSTRCONNECT_BRIDGE}?${params.toString()}`
}
private cleanupPopup(popup: Window | null, checkClosed: number, messageHandler: (event: MessageEvent) => void) {
clearInterval(checkClosed)
window.removeEventListener('message', messageHandler)
if (popup && !popup.closed) {
popup.close()
}
}
private createMessageHandler(
resolve: () => void,
reject: (error: Error) => void,
cleanup: () => void
): (event: MessageEvent) => void {
return (event: MessageEvent) => {
handleNostrConnectMessage(
event,
this.state,
(pubkey, _privateKey) => {
this.state = {
connected: true,
pubkey,
profile: null,
}
this.saveStateToStorage()
this.notifyListeners()
void this.loadProfile()
cleanup()
resolve()
},
(error) => {
console.error('Connection error:', error)
cleanup()
reject(error)
}
)
}
}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
const url = this.createConnectUrl()
// Open NostrConnect bridge in popup
const popup = window.open(url, 'nostrconnect', 'width=400,height=600,scrollbars=yes,resizable=yes')
if (!popup) {
reject(new Error('Popup blocked. Please allow popups for this site.'))
return
}
const messageHandler: (event: MessageEvent) => void
const checkClosed = setInterval(() => {
if (popup.closed) {
this.cleanupPopup(popup, checkClosed, messageHandler)
if (!this.state.connected) {
reject(new Error('Connection cancelled'))
}
}
}, 1000)
const cleanup = () => {
this.cleanupPopup(popup, checkClosed, messageHandler)
}
messageHandler = this.createMessageHandler(resolve, reject, cleanup)
window.addEventListener('message', messageHandler)
})
}
disconnect(): Promise<void> {
this.state = {
connected: false,
pubkey: null,
profile: null,
}
this.saveStateToStorage()
this.notifyListeners()
}
private async loadProfile(): Promise<void> {
if (!this.state.pubkey) {
return
}
try {
const profile = await nostrService.getProfile(this.state.pubkey)
if (profile) {
this.state.profile = profile
this.saveStateToStorage()
this.notifyListeners()
}
} catch (e) {
console.error('Error loading profile:', e)
}
}
private setupMessageListener(): void {
window.addEventListener('storage', (e) => {
if (e.key === 'nostrconnect_state') {
this.loadStateFromStorage()
}
})
}
private loadStateFromStorage(): void {
try {
const stored = localStorage.getItem('nostrconnect_state')
if (stored) {
const parsed = JSON.parse(stored)
this.state = {
connected: parsed.connected ?? false,
pubkey: parsed.pubkey ?? null,
profile: parsed.profile ?? null,
}
if (this.state.pubkey) {
nostrService.setPublicKey(this.state.pubkey)
}
}
} catch (e) {
console.error('Error loading state from storage:', e)
}
}
private saveStateToStorage(): void {
try {
localStorage.setItem('nostrconnect_state', JSON.stringify(this.state))
} catch (e) {
console.error('Error saving state to storage:', e)
}
}
private notifyListeners(): void {
this.listeners.forEach((callback) => callback({ ...this.state }))
}
}
export const nostrConnectService = new NostrConnectService()