story-research-zapwall/lib/nostrconnect.ts
2025-12-22 09:48:57 +01:00

179 lines
5.0 KiB
TypeScript

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<void> {
return new Promise((resolve, reject) => {
const appName = 'Nostr Paywall'
const appUrl = window.location.origin
// NostrConnect URI format: nostrconnect://<pubkey>?relay=<relay>&metadata=<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<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()