mirror of
https://github.com/dergigi/boris.git
synced 2026-01-31 12:44:37 +01:00
feat: add Login with Bunker authentication option
- Wire NostrConnectSigner to RelayPool in App.tsx - Create LoginOptions component with Extension and Bunker login flows - Show LoginOptions in BookmarkList when user is logged out - Add applesauce-accounts and applesauce-signers to vite optimizeDeps - Support NIP-46 bunker:// URI authentication alongside extension login
This commit is contained in:
@@ -7,6 +7,7 @@ import { EventStore } from 'applesauce-core'
|
||||
import { AccountManager } from 'applesauce-accounts'
|
||||
import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { NostrConnectSigner } from 'applesauce-signers'
|
||||
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
||||
import Bookmarks from './components/Bookmarks'
|
||||
import RouteDebug from './components/RouteDebug'
|
||||
@@ -219,6 +220,9 @@ function App() {
|
||||
|
||||
const pool = new RelayPool()
|
||||
|
||||
// Setup NostrConnectSigner to use the relay pool
|
||||
NostrConnectSigner.pool = pool
|
||||
|
||||
// Create a relay group for better event deduplication and management
|
||||
pool.group(RELAYS)
|
||||
console.log('Created relay group with', RELAYS.length, 'relays (including local)')
|
||||
|
||||
@@ -21,6 +21,7 @@ import { RELAYS } from '../config/relays'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
|
||||
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
|
||||
import LoginOptions from './LoginOptions'
|
||||
|
||||
interface BookmarkListProps {
|
||||
bookmarks: Bookmark[]
|
||||
@@ -153,7 +154,9 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredBookmarks.length === 0 && allIndividualBookmarks.length > 0 ? (
|
||||
{!activeAccount ? (
|
||||
<LoginOptions />
|
||||
) : filteredBookmarks.length === 0 && allIndividualBookmarks.length > 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>No bookmarks match this filter.</p>
|
||||
</div>
|
||||
@@ -170,7 +173,6 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
<div className="empty-state">
|
||||
<p>No bookmarks found.</p>
|
||||
<p>Add bookmarks using your nostr client to see them here.</p>
|
||||
<p>If you aren't on nostr yet, start here: <a href="https://nstart.me/" target="_blank" rel="noopener noreferrer">nstart.me</a></p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
|
||||
170
src/components/LoginOptions.tsx
Normal file
170
src/components/LoginOptions.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { Accounts } from 'applesauce-accounts'
|
||||
import { NostrConnectSigner } from 'applesauce-signers'
|
||||
|
||||
const LoginOptions: React.FC = () => {
|
||||
const accountManager = Hooks.useAccountManager()
|
||||
const [showBunkerInput, setShowBunkerInput] = useState(false)
|
||||
const [bunkerUri, setBunkerUri] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleExtensionLogin = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const account = await Accounts.ExtensionAccount.fromExtension()
|
||||
accountManager.addAccount(account)
|
||||
accountManager.setActive(account)
|
||||
} catch (err) {
|
||||
console.error('Extension login failed:', err)
|
||||
setError('Login failed. Please install a nostr browser extension and try again.')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBunkerLogin = async () => {
|
||||
if (!bunkerUri.trim()) {
|
||||
setError('Please enter a bunker URI')
|
||||
return
|
||||
}
|
||||
|
||||
if (!bunkerUri.startsWith('bunker://')) {
|
||||
setError('Invalid bunker URI. Must start with bunker://')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
// Create signer from bunker URI
|
||||
const signer = await NostrConnectSigner.fromBunkerURI(bunkerUri)
|
||||
|
||||
// Get pubkey from signer
|
||||
const pubkey = await signer.getPublicKey()
|
||||
|
||||
// Create account from signer
|
||||
const account = new Accounts.NostrConnectAccount(pubkey, signer)
|
||||
|
||||
// Add to account manager and set active
|
||||
accountManager.addAccount(account)
|
||||
accountManager.setActive(account)
|
||||
|
||||
// Clear input on success
|
||||
setBunkerUri('')
|
||||
setShowBunkerInput(false)
|
||||
} catch (err) {
|
||||
console.error('Bunker login failed:', err)
|
||||
setError(err instanceof Error ? err.message : 'Failed to connect to bunker')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<p style={{ marginBottom: '1rem' }}>Login with:</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', maxWidth: '300px', margin: '0 auto' }}>
|
||||
<button
|
||||
onClick={handleExtensionLogin}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
cursor: isLoading ? 'wait' : 'pointer',
|
||||
opacity: isLoading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{isLoading && !showBunkerInput ? 'Connecting...' : 'Extension'}
|
||||
</button>
|
||||
|
||||
{!showBunkerInput ? (
|
||||
<button
|
||||
onClick={() => setShowBunkerInput(true)}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
cursor: isLoading ? 'wait' : 'pointer',
|
||||
opacity: isLoading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
Bunker
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="bunker://..."
|
||||
value={bunkerUri}
|
||||
onChange={(e) => setBunkerUri(e.target.value)}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
fontSize: '0.9rem',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleBunkerLogin()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button
|
||||
onClick={handleBunkerLogin}
|
||||
disabled={isLoading || !bunkerUri.trim()}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
fontSize: '0.9rem',
|
||||
flex: 1,
|
||||
cursor: isLoading || !bunkerUri.trim() ? 'not-allowed' : 'pointer',
|
||||
opacity: isLoading || !bunkerUri.trim() ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{isLoading && showBunkerInput ? 'Connecting...' : 'Connect'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowBunkerInput(false)
|
||||
setBunkerUri('')
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
fontSize: '0.9rem',
|
||||
cursor: isLoading ? 'not-allowed' : 'pointer',
|
||||
opacity: isLoading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p style={{ color: 'var(--color-error, #ef4444)', marginTop: '1rem', fontSize: '0.9rem' }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p style={{ marginTop: '1.5rem', fontSize: '0.9rem' }}>
|
||||
If you aren't on nostr yet, start here:{' '}
|
||||
<a href="https://nstart.me/" target="_blank" rel="noopener noreferrer">
|
||||
nstart.me
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginOptions
|
||||
|
||||
@@ -141,7 +141,7 @@ export default defineConfig({
|
||||
mainFields: ['module', 'jsnext:main', 'jsnext', 'main']
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['applesauce-core', 'applesauce-factory', 'applesauce-relay', 'applesauce-react'],
|
||||
include: ['applesauce-core', 'applesauce-factory', 'applesauce-relay', 'applesauce-react', 'applesauce-accounts', 'applesauce-signers'],
|
||||
esbuildOptions: {
|
||||
resolveExtensions: ['.js', '.ts', '.tsx', '.json']
|
||||
}
|
||||
@@ -158,7 +158,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
ssr: {
|
||||
noExternal: ['applesauce-core', 'applesauce-factory', 'applesauce-relay']
|
||||
noExternal: ['applesauce-core', 'applesauce-factory', 'applesauce-relay', 'applesauce-accounts', 'applesauce-signers']
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user