import type { NostrConnectState } from '@/types/nostr' import { nostrService } from './nostr' import { handleNostrConnectMessage } from './nostrconnectHandler' // Support for nos2x extension (NIP-07) and NostrConnect (NIP-46) // nos2x uses window.nostr API directly // NostrConnect uses a bridge for remote signing const NOSTRCONNECT_BRIDGE = process.env.NEXT_PUBLIC_NOSTRCONNECT_BRIDGE ?? '' 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 { if (!NOSTRCONNECT_BRIDGE) { throw new Error('NostrConnect bridge not configured') } 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) { window.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 { 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 checkClosed = window.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) const messageHandler = this.createMessageHandler(resolve, reject, cleanup) window.addEventListener('message', messageHandler) }) } disconnect(): void { this.state = { connected: false, pubkey: null, profile: null, } this.saveStateToStorage() this.notifyListeners() } private async loadProfile(): Promise { 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()