import type { NostrConnectState, NostrProfile } 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 } } async connect(): Promise { return new Promise((resolve, reject) => { const appName = 'Nostr Paywall' const appUrl = window.location.origin // NostrConnect URI format: nostrconnect://?relay=&metadata= // use.nsec.app provides a web interface for this const params = new URLSearchParams({ origin: appUrl, name: appName, relay: this.relayUrl, }) const url = `${NOSTRCONNECT_BRIDGE}?${params.toString()}` // 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 = setInterval(() => { if (popup.closed) { clearInterval(checkClosed) window.removeEventListener('message', messageHandler) if (!this.state.connected) { reject(new Error('Connection cancelled')) } } }, 1000) const messageHandler = (event: MessageEvent) => { console.log('Message event received in connect:', event) handleNostrConnectMessage( event, this.state, (pubkey, privateKey) => { console.log('Connection successful, updating state') this.state = { connected: true, pubkey, profile: null, } this.saveStateToStorage() this.notifyListeners() this.loadProfile() clearInterval(checkClosed) window.removeEventListener('message', messageHandler) if (popup && !popup.closed) { popup.close() } resolve() }, (error) => { console.error('Connection error:', error) clearInterval(checkClosed) window.removeEventListener('message', messageHandler) if (popup && !popup.closed) { popup.close() } reject(error) } ) } // Listen for all messages to debug window.addEventListener('message', messageHandler) console.log('Listening for messages from:', NOSTRCONNECT_BRIDGE) }) } async disconnect(): Promise { 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()