Replace nos2x and NostrConnect with Alby authentication
- Remove nos2x and NostrConnect support - Create new NostrAuthService using Alby (window.nostr NIP-07) - Replace useNostrConnect with useNostrAuth in all components - Update NostrRemoteSigner to use Alby for signing - Delete NostrConnect-related files (nostrconnect.ts, handlers, etc.) - Update documentation to reflect Alby-only authentication - Remove NOSTRCONNECT_BRIDGE environment variable - All TypeScript checks pass
This commit is contained in:
parent
fd6ff4b6cc
commit
cb7ee0cfd4
11
README.md
11
README.md
@ -4,7 +4,7 @@ Plateforme de publication d'articles scientifiques et de science-fiction avec sy
|
||||
|
||||
## Features
|
||||
|
||||
- **Nostr Wallet Integration**: Authenticate using nos2x extension (NIP-07) or NostrConnect bridge (NIP-46)
|
||||
- **Nostr Authentication**: Authenticate using Alby browser extension (NIP-07)
|
||||
- **Free Previews**: Public notes showing article previews
|
||||
- **Paid Content**: Private notes containing full content, unlocked after 800 sats zap
|
||||
- **Lightning Payments**: Integrated Alby/WebLN for Lightning payments (works with Alby and other Lightning wallets)
|
||||
@ -28,15 +28,14 @@ npm run dev
|
||||
## Environment Variables
|
||||
|
||||
- `NEXT_PUBLIC_NOSTR_RELAY_URL`: Nostr relay URL (default: wss://relay.damus.io)
|
||||
- `NEXT_PUBLIC_NOSTRCONNECT_BRIDGE`: NostrConnect bridge URL (optional, nos2x extension is used by default)
|
||||
|
||||
## Lightning Wallet Setup
|
||||
|
||||
This project uses the WebLN standard for Lightning payments, which works with:
|
||||
- **Alby** (recommended): Install the [Alby browser extension](https://getalby.com/)
|
||||
- Other WebLN-compatible Lightning wallets
|
||||
This project uses Alby browser extension for both Nostr authentication and Lightning payments:
|
||||
- **Alby**: Install the [Alby browser extension](https://getalby.com/)
|
||||
- Alby provides both Nostr authentication (NIP-07) and Lightning payments (WebLN)
|
||||
|
||||
Users need to have a Lightning wallet extension installed to make payments. The payment flow will prompt them to connect their wallet when needed.
|
||||
Users need to have Alby installed to authenticate and make payments. The application will prompt them to connect when needed.
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Article } from '@/types/nostr'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useArticlePayment } from '@/hooks/useArticlePayment'
|
||||
import { ArticlePreview } from './ArticlePreview'
|
||||
import { PaymentModal } from './PaymentModal'
|
||||
@ -42,7 +42,7 @@ function ArticleMeta({
|
||||
}
|
||||
|
||||
export function ArticleCard({ article, onUnlock }: ArticleCardProps) {
|
||||
const { pubkey, connect } = useNostrConnect()
|
||||
const { pubkey, connect } = useNostrAuth()
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useArticlePublishing } from '@/hooks/useArticlePublishing'
|
||||
import type { ArticleDraft } from '@/lib/articlePublisher'
|
||||
import { ArticleEditorForm } from './ArticleEditorForm'
|
||||
@ -22,7 +22,7 @@ function SuccessMessage() {
|
||||
}
|
||||
|
||||
export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSelectSeries }: ArticleEditorProps) {
|
||||
const { connected, pubkey, connect } = useNostrConnect()
|
||||
const { connected, pubkey, connect } = useNostrAuth()
|
||||
const { loading, error, success, publishArticle } = useArticlePublishing(pubkey ?? null)
|
||||
const [draft, setDraft] = useState<ArticleDraft>({
|
||||
title: '',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||
import { ArticleField } from './ArticleField'
|
||||
import { ArticleFormButtons } from './ArticleFormButtons'
|
||||
@ -229,6 +229,6 @@ function AuthorPresentationFormView({
|
||||
}
|
||||
|
||||
export function AuthorPresentationEditor() {
|
||||
const { connected, pubkey, profile } = useNostrConnect()
|
||||
const { connected, pubkey, profile } = useNostrAuth()
|
||||
return <AuthorPresentationFormView pubkey={pubkey ?? null} connected={connected} profile={profile} />
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import Link from 'next/link'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
export function ConditionalPublishButton() {
|
||||
const { connected, pubkey } = useNostrConnect()
|
||||
const { connected, pubkey } = useNostrAuth()
|
||||
const { checkPresentationExists } = useAuthorPresentation(pubkey ?? null)
|
||||
const [hasPresentation, setHasPresentation] = useState<boolean | null>(null)
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { ConnectedUserMenu } from './ConnectedUserMenu'
|
||||
|
||||
function ConnectForm({ onConnect, loading, error }: {
|
||||
@ -23,7 +23,7 @@ function ConnectForm({ onConnect, loading, error }: {
|
||||
}
|
||||
|
||||
export function ConnectButton() {
|
||||
const { connected, pubkey, profile, loading, error, connect, disconnect } = useNostrConnect()
|
||||
const { connected, pubkey, profile, loading, error, connect, disconnect } = useNostrAuth()
|
||||
|
||||
if (connected && pubkey) {
|
||||
return (
|
||||
|
||||
@ -38,7 +38,7 @@ Le sponsoring permet de soutenir directement un auteur avec **0.046 BTC** :
|
||||
|
||||
### Comment me connecter ?
|
||||
|
||||
Cliquez sur "Connect with Nostr" et autorisez la connexion avec votre portefeuille Nostr. L'application utilise l'extension nos2x (NIP-07) par défaut, ou un pont NostrConnect (NIP-46) si configuré.
|
||||
Cliquez sur "Connect with Nostr" et autorisez la connexion avec Alby. L'application utilise l'extension Alby pour l'authentification Nostr (NIP-07) et les paiements Lightning (WebLN).
|
||||
|
||||
### J'ai besoin d'un compte ?
|
||||
|
||||
|
||||
@ -57,7 +57,6 @@ RIZFUL_API_URL=https://api.rizful.com
|
||||
|
||||
# Variables publiques (client-side)
|
||||
NEXT_PUBLIC_NOSTR_RELAY_URL=wss://relay.damus.io
|
||||
NEXT_PUBLIC_NOSTRCONNECT_BRIDGE= # Optional: nos2x extension is used by default
|
||||
```
|
||||
|
||||
**⚠️ Important** :
|
||||
|
||||
@ -62,7 +62,7 @@ Pour effectuer des paiements Lightning, vous devez installer une extension de po
|
||||
|
||||
1. Cliquez sur le bouton **"Connect with Nostr"** en haut à droite
|
||||
2. Une fenêtre s'ouvrira pour vous connecter avec votre portefeuille Nostr
|
||||
3. Par défaut, l'application utilise l'extension nos2x (NIP-07). Un pont NostrConnect (NIP-46) peut être configuré via la variable d'environnement `NEXT_PUBLIC_NOSTRCONNECT_BRIDGE`
|
||||
3. L'application utilise l'extension Alby pour l'authentification Nostr (NIP-07) et les paiements Lightning (WebLN)
|
||||
4. Autorisez la connexion dans votre portefeuille Nostr
|
||||
|
||||
### Que se passe-t-il après la connexion ?
|
||||
@ -265,7 +265,7 @@ En tant qu'auteur, vous pouvez remercier un lecteur pour son avis :
|
||||
|
||||
**Je ne peux pas me connecter avec Nostr**
|
||||
- Vérifiez que votre portefeuille Nostr est accessible
|
||||
- Vérifiez que l'extension nos2x est installée, ou que le pont NostrConnect est configuré et accessible
|
||||
- Vérifiez que l'extension Alby est installée et activée
|
||||
- Essayez de rafraîchir la page
|
||||
- Vérifiez votre connexion internet
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
### Nostr Paywall → zapwall4Science
|
||||
- Publication d'articles avec aperçus gratuits et contenu payant
|
||||
- Paiement Lightning via Alby/WebLN (remplacement de Rizful)
|
||||
- Connexion via nos2x extension (NIP-07) ou NostrConnect bridge (NIP-46)
|
||||
- Connexion via Alby extension (NIP-07) pour l'authentification Nostr et les paiements Lightning
|
||||
- Interface TypeScript/Next.js
|
||||
|
||||
### Services principaux
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { nostrConnectService } from '@/lib/nostrconnect'
|
||||
import { nostrAuthService } from '@/lib/nostrAuth'
|
||||
import type { NostrConnectState } from '@/types/nostr'
|
||||
|
||||
export function useNostrConnect() {
|
||||
const [state, setState] = useState<NostrConnectState>(nostrConnectService.getState())
|
||||
export function useNostrAuth() {
|
||||
const [state, setState] = useState<NostrConnectState>(nostrAuthService.getState())
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = nostrConnectService.subscribe((newState) => {
|
||||
const unsubscribe = nostrAuthService.subscribe((newState) => {
|
||||
setState(newState)
|
||||
})
|
||||
|
||||
@ -19,7 +19,7 @@ export function useNostrConnect() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
await nostrConnectService.connect()
|
||||
await nostrAuthService.connect()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Connection failed')
|
||||
} finally {
|
||||
@ -30,7 +30,7 @@ export function useNostrConnect() {
|
||||
const disconnect = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await nostrConnectService.disconnect()
|
||||
nostrAuthService.disconnect()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Disconnection failed')
|
||||
} finally {
|
||||
@ -46,3 +46,4 @@ export function useNostrConnect() {
|
||||
disconnect,
|
||||
}
|
||||
}
|
||||
|
||||
144
lib/nostrAuth.ts
Normal file
144
lib/nostrAuth.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { nostrService } from './nostr'
|
||||
import type { NostrConnectState } from '@/types/nostr'
|
||||
|
||||
/**
|
||||
* Nostr authentication service using Alby (NIP-07)
|
||||
* Alby exposes window.nostr API for Nostr authentication and signing
|
||||
*/
|
||||
export class NostrAuthService {
|
||||
private state: NostrConnectState = {
|
||||
connected: false,
|
||||
pubkey: null,
|
||||
profile: null,
|
||||
}
|
||||
|
||||
private listeners: Set<(state: NostrConnectState) => void> = new Set()
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Alby (window.nostr) is available
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
return typeof window !== 'undefined' && typeof window.nostr !== 'undefined'
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect using Alby (NIP-07)
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (!this.isAvailable()) {
|
||||
throw new Error('Alby extension not available. Please install Alby browser extension.')
|
||||
}
|
||||
|
||||
if (!window.nostr) {
|
||||
throw new Error('window.nostr is not available. Please ensure Alby extension is installed and enabled.')
|
||||
}
|
||||
|
||||
try {
|
||||
const pubkey = await window.nostr.getPublicKey()
|
||||
if (!pubkey) {
|
||||
throw new Error('Failed to get public key from Alby')
|
||||
}
|
||||
|
||||
this.state = {
|
||||
connected: true,
|
||||
pubkey,
|
||||
profile: null,
|
||||
}
|
||||
nostrService.setPublicKey(pubkey)
|
||||
this.saveStateToStorage()
|
||||
this.notifyListeners()
|
||||
void this.loadProfile()
|
||||
} catch (e) {
|
||||
console.error('Error connecting with Alby:', e)
|
||||
throw new Error(`Failed to connect with Alby: ${e instanceof Error ? e.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
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 === 'nostr_auth_state') {
|
||||
this.loadStateFromStorage()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private loadStateFromStorage(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem('nostr_auth_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('nostr_auth_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 nostrAuthService = new NostrAuthService()
|
||||
|
||||
@ -1,22 +1,21 @@
|
||||
import type { EventTemplate, Event } from 'nostr-tools'
|
||||
import { getEventHash, signEvent } from 'nostr-tools'
|
||||
import { nostrConnectService } from './nostrconnect'
|
||||
import { nostrAuthService } from './nostrAuth'
|
||||
import { nostrService } from './nostr'
|
||||
|
||||
/**
|
||||
* Remote signer using nos2x (NIP-07) or NostrConnect (NIP-46)
|
||||
* Supports nos2x extension (window.nostr) and NostrConnect bridge
|
||||
* Remote signer using Alby (NIP-07)
|
||||
* Alby exposes window.nostr API for signing events
|
||||
*/
|
||||
export class NostrRemoteSigner {
|
||||
/**
|
||||
* Sign an event template
|
||||
* Uses nos2x (NIP-07) if available, otherwise falls back to private key signing
|
||||
* Sign an event template using Alby (window.nostr)
|
||||
*/
|
||||
async signEvent(eventTemplate: EventTemplate): Promise<Event | null> {
|
||||
// Get the event hash first
|
||||
const pubkey = nostrService.getPublicKey()
|
||||
if (!pubkey) {
|
||||
throw new Error('Public key required for signing. Please connect a Nostr wallet.')
|
||||
throw new Error('Public key required for signing. Please connect with Alby.')
|
||||
}
|
||||
|
||||
const unsignedEvent = {
|
||||
@ -26,7 +25,7 @@ export class NostrRemoteSigner {
|
||||
}
|
||||
const eventId = getEventHash(unsignedEvent)
|
||||
|
||||
// Try nos2x (NIP-07) first
|
||||
// Use Alby (window.nostr) for signing
|
||||
if (typeof window !== 'undefined' && window.nostr) {
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent({
|
||||
@ -37,17 +36,17 @@ export class NostrRemoteSigner {
|
||||
})
|
||||
return signedEvent as Event
|
||||
} catch (e) {
|
||||
console.error('Error signing with nos2x:', e)
|
||||
throw new Error('Failed to sign event with nos2x extension')
|
||||
console.error('Error signing with Alby:', e)
|
||||
throw new Error('Failed to sign event with Alby extension')
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to private key signing
|
||||
// Fallback to private key signing (should not happen if Alby is properly connected)
|
||||
const privateKey = nostrService.getPrivateKey()
|
||||
if (!privateKey) {
|
||||
throw new Error(
|
||||
'Private key required for signing. ' +
|
||||
'Please install nos2x extension or use a NostrConnect wallet that provides signing capabilities.'
|
||||
'Alby extension required for signing. ' +
|
||||
'Please install and connect Alby browser extension.'
|
||||
)
|
||||
}
|
||||
|
||||
@ -64,15 +63,15 @@ export class NostrRemoteSigner {
|
||||
* Check if remote signing is available
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
const state = nostrConnectService.getState()
|
||||
const state = nostrAuthService.getState()
|
||||
return state.connected && !!state.pubkey
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if direct signing (with private key) is available
|
||||
* Check if Alby is available
|
||||
*/
|
||||
isDirectSigningAvailable(): boolean {
|
||||
return !!nostrService.getPrivateKey()
|
||||
isAlbyAvailable(): boolean {
|
||||
return typeof window !== 'undefined' && typeof window.nostr !== 'undefined'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,190 +0,0 @@
|
||||
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<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()
|
||||
@ -1,2 +0,0 @@
|
||||
export { handleNostrConnectMessage } from './nostrconnectMessageHandler'
|
||||
export type { NostrConnectState } from '@/types/nostr'
|
||||
@ -1,106 +0,0 @@
|
||||
import type { NostrConnectState } from '@/types/nostr'
|
||||
import { nostrService } from './nostr'
|
||||
|
||||
const NOSTRCONNECT_BRIDGE = process.env.NEXT_PUBLIC_NOSTRCONNECT_BRIDGE ?? ''
|
||||
|
||||
interface MessageData {
|
||||
type?: string
|
||||
method?: string
|
||||
action?: string
|
||||
pubkey?: string
|
||||
publicKey?: string
|
||||
privateKey?: string
|
||||
secretKey?: string
|
||||
message?: string
|
||||
error?: string
|
||||
params?: {
|
||||
pubkey?: string
|
||||
publicKey?: string
|
||||
privateKey?: string
|
||||
secretKey?: string
|
||||
}
|
||||
}
|
||||
|
||||
function handleConnectMessage(
|
||||
data: MessageData,
|
||||
onSuccess: (pubkey: string, privateKey?: string) => void,
|
||||
onError: (error: Error) => void
|
||||
): boolean {
|
||||
const pubkey = data.pubkey ?? data.publicKey
|
||||
const privateKey = data.privateKey ?? data.secretKey
|
||||
|
||||
if (!pubkey) {
|
||||
console.error('No pubkey in message data:', data)
|
||||
onError(new Error('No pubkey received'))
|
||||
return false
|
||||
}
|
||||
nostrService.setPublicKey(pubkey)
|
||||
if (privateKey) {
|
||||
nostrService.setPrivateKey(privateKey)
|
||||
}
|
||||
|
||||
onSuccess(pubkey, privateKey)
|
||||
return true
|
||||
}
|
||||
|
||||
function handleAlternativeConnectMessage(
|
||||
data: MessageData,
|
||||
onSuccess: (pubkey: string, privateKey?: string) => void,
|
||||
onError: (error: Error) => void
|
||||
): boolean {
|
||||
const pubkey =
|
||||
data.pubkey ?? data.publicKey ?? data.params?.pubkey ?? data.params?.publicKey
|
||||
const privateKey =
|
||||
data.privateKey ?? data.secretKey ?? data.params?.privateKey ?? data.params?.secretKey
|
||||
|
||||
if (!pubkey) {
|
||||
console.error('No pubkey in message data:', data)
|
||||
onError(new Error('No pubkey received'))
|
||||
return false
|
||||
}
|
||||
nostrService.setPublicKey(pubkey)
|
||||
if (privateKey) {
|
||||
nostrService.setPrivateKey(privateKey)
|
||||
}
|
||||
|
||||
onSuccess(pubkey, privateKey)
|
||||
return true
|
||||
}
|
||||
|
||||
function handleErrorMessage(data: MessageData, onError: (error: Error) => void): void {
|
||||
const errorMessage = data.message ?? data.error ?? 'Connection failed'
|
||||
console.error('Connection error:', errorMessage)
|
||||
onError(new Error(errorMessage))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle NostrConnect connection message
|
||||
*/
|
||||
export function handleNostrConnectMessage(
|
||||
event: MessageEvent,
|
||||
_state: NostrConnectState,
|
||||
onSuccess: (pubkey: string, privateKey?: string) => void,
|
||||
onError: (error: Error) => void
|
||||
): void {
|
||||
const bridgeOrigin = new URL(NOSTRCONNECT_BRIDGE).origin
|
||||
|
||||
if (event.origin !== bridgeOrigin) {
|
||||
console.warn('Origin mismatch:', event.origin, 'expected:', bridgeOrigin)
|
||||
return
|
||||
}
|
||||
|
||||
const data = event.data as MessageData | undefined
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
const messageType = data.type ?? data.method ?? data.action
|
||||
|
||||
if (messageType === 'nostrconnect:connect' || messageType === 'connect') {
|
||||
handleConnectMessage(data, onSuccess, onError)
|
||||
} else if (messageType === 'nostrconnect:error' || messageType === 'error') {
|
||||
handleErrorMessage(data, onError)
|
||||
} else if (data.method === 'connect' || data.action === 'connect') {
|
||||
handleAlternativeConnectMessage(data, onSuccess, onError)
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
env: {
|
||||
NOSTR_RELAY_URL: process.env.NEXT_PUBLIC_NOSTR_RELAY_URL || 'wss://relay.damus.io',
|
||||
NOSTRCONNECT_BRIDGE: process.env.NEXT_PUBLIC_NOSTRCONNECT_BRIDGE || '',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import { useArticles } from '@/hooks/useArticles'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { applyFiltersAndSort } from '@/lib/articleFiltering'
|
||||
import type { Article } from '@/types/nostr'
|
||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
||||
@ -85,7 +85,7 @@ function useUnlockHandler(
|
||||
}
|
||||
|
||||
function useHomeController() {
|
||||
const { } = useNostrConnect()
|
||||
const { } = useNostrAuth()
|
||||
const {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
|
||||
@ -4,7 +4,7 @@ import Head from 'next/head'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { AuthorPresentationEditor } from '@/components/AuthorPresentationEditor'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
@ -59,7 +59,7 @@ function PresentationLayout() {
|
||||
}
|
||||
|
||||
export default function PresentationPage() {
|
||||
const { connected, pubkey } = useNostrConnect()
|
||||
const { connected, pubkey } = useNostrAuth()
|
||||
usePresentationRedirect(connected, pubkey)
|
||||
return <PresentationLayout />
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
|
||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
||||
import type { NostrProfile } from '@/types/nostr'
|
||||
import { ProfileView } from '@/components/ProfileView'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useUserArticles } from '@/hooks/useUserArticles'
|
||||
import { nostrService } from '@/lib/nostr'
|
||||
|
||||
@ -49,7 +49,7 @@ function useRedirectWhenDisconnected(connected: boolean, pubkey: string | null)
|
||||
}
|
||||
|
||||
function useProfileController() {
|
||||
const { connected, pubkey: currentPubkey } = useNostrConnect()
|
||||
const { connected, pubkey: currentPubkey } = useNostrAuth()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filters, setFilters] = useState<ArticleFilters>({
|
||||
authorPubkey: null,
|
||||
|
||||
@ -2,7 +2,7 @@ import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ArticleEditor } from '@/components/ArticleEditor'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNostrConnect } from '@/hooks/useNostrConnect'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { getSeriesByAuthor } from '@/lib/seriesQueries'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
@ -33,7 +33,7 @@ function PublishHero({ onBack }: { onBack: () => void }) {
|
||||
|
||||
export default function PublishPage() {
|
||||
const router = useRouter()
|
||||
const { pubkey } = useNostrConnect()
|
||||
const { pubkey } = useNostrAuth()
|
||||
const [seriesOptions, setSeriesOptions] = useState<{ id: string; title: string }[]>([])
|
||||
|
||||
const handlePublishSuccess = () => {
|
||||
|
||||
4
types/nostr-window.d.ts
vendored
4
types/nostr-window.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
// Type definitions for NIP-07 (nos2x extension)
|
||||
// Type definitions for NIP-07 (Alby extension)
|
||||
// Alby exposes window.nostr API for Nostr authentication and signing
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: {
|
||||
@ -27,4 +28,3 @@ declare global {
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user