story-research-zapwall/lib/nostrconnect.ts
2025-12-23 02:20:57 +01:00

187 lines
4.8 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) {
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<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 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<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()