import { useState, useEffect } from 'react' import { nostrAuthService } from '@/lib/nostrAuth' import { keyManagementService } from '@/lib/keyManagement' import { nip19 } from 'nostr-tools' import { t } from '@/lib/i18n' import { SyncProgressBar } from './SyncProgressBar' interface PublicKeys { publicKey: string npub: string } export function KeyManagementManager(): React.ReactElement { const [publicKeys, setPublicKeys] = useState(null) const [accountExists, setAccountExists] = useState(false) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [importKey, setImportKey] = useState('') const [importing, setImporting] = useState(false) const [showImportForm, setShowImportForm] = useState(false) const [showReplaceWarning, setShowReplaceWarning] = useState(false) const [recoveryPhrase, setRecoveryPhrase] = useState(null) const [newNpub, setNewNpub] = useState(null) const [copiedNpub, setCopiedNpub] = useState(false) const [copiedPublicKey, setCopiedPublicKey] = useState(false) const [copiedRecoveryPhrase, setCopiedRecoveryPhrase] = useState(false) useEffect(() => { void loadKeys() }, []) async function loadKeys(): Promise { try { setLoading(true) setError(null) const exists = await nostrAuthService.accountExists() setAccountExists(exists) if (exists) { const keys = await keyManagementService.getPublicKeys() if (keys) { setPublicKeys(keys) } } } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.loading') setError(errorMessage) console.error('Error loading keys:', e) } finally { setLoading(false) } } function extractKeyFromUrl(url: string): string | null { try { // Try to parse as URL const urlObj = new URL(url) // Check if it's a nostr:// URL with nsec if (urlObj.protocol === 'nostr:' || urlObj.protocol === 'nostr://') { const path = urlObj.pathname ?? urlObj.href.replace(/^nostr:?\/\//, '') if (path.startsWith('nsec')) { return path } } // Check if URL contains nsec const nsecMatch = url.match(/nsec1[a-z0-9]+/i) if (nsecMatch) { return nsecMatch[0] } return null } catch { return extractKeyFromText(url) } } function extractKeyFromText(text: string): string { const nsec = extractNsec(text) if (nsec) { return nsec } return text.trim() } function extractNsec(text: string): string | null { const nsecMatch = text.match(/nsec1[a-z0-9]+/i) return nsecMatch?.[0] ?? null } function isValidPrivateKeyFormat(key: string): boolean { try { const decoded = nip19.decode(key) if (decoded.type !== 'nsec') { return false } return typeof decoded.data === 'string' || decoded.data instanceof Uint8Array } catch { return /^[0-9a-f]{64}$/i.test(key) } } async function handleImport(): Promise { if (!importKey.trim()) { setError(t('settings.keyManagement.import.error.required')) return } // Extract key from URL or text const extractedKey = extractKeyFromUrl(importKey.trim()) if (!extractedKey) { setError(t('settings.keyManagement.import.error.invalid')) return } // Validate key format if (!isValidPrivateKeyFormat(extractedKey)) { setError(t('settings.keyManagement.import.error.invalid')) return } // If account exists, show warning if (accountExists) { setShowReplaceWarning(true) return } await performImport(extractedKey) } async function performImport(key: string): Promise { try { setImporting(true) setError(null) setShowReplaceWarning(false) // If account exists, delete it first if (accountExists) { await nostrAuthService.deleteAccount() } // Create new account with imported key const result = await nostrAuthService.createAccount(key) setRecoveryPhrase(result.recoveryPhrase) setNewNpub(result.npub) setImportKey('') setShowImportForm(false) await loadKeys() // Sync user content via Service Worker if (result.publicKey) { const { swClient } = await import('@/lib/swClient') const isReady = await swClient.isReady() if (isReady) { void swClient.startUserSync(result.publicKey) } } } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.import.error.failed') setError(errorMessage) console.error('Error importing key:', e) } finally { setImporting(false) } } async function handleCopyRecoveryPhrase(): Promise { if (!recoveryPhrase) { return } try { await navigator.clipboard.writeText(recoveryPhrase.join(' ')) setCopiedRecoveryPhrase(true) setTimeout(() => { setCopiedRecoveryPhrase(false) }, 2000) } catch (e) { console.error('Error copying recovery phrase:', e) } } async function handleCopyNpub(): Promise { if (!publicKeys?.npub) { return } try { await navigator.clipboard.writeText(publicKeys.npub) setCopiedNpub(true) setTimeout(() => { setCopiedNpub(false) }, 2000) } catch (e) { console.error('Error copying npub:', e) } } async function handleCopyPublicKey(): Promise { if (!publicKeys?.publicKey) { return } try { await navigator.clipboard.writeText(publicKeys.publicKey) setCopiedPublicKey(true) setTimeout(() => { setCopiedPublicKey(false) }, 2000) } catch (e) { console.error('Error copying public key:', e) } } if (loading) { return (

{t('settings.keyManagement.loading')}

) } return (

{t('settings.keyManagement.title')}

{/* Sync Progress Bar - Always show if connected, even if publicKeys not loaded yet */} { setShowImportForm(true) setError(null) }} /> { setImportKey(value) setError(null) }} onCancel={() => { setShowImportForm(false) setImportKey('') setError(null) }} onImport={() => { void handleImport() }} onDismissReplaceWarning={() => { setShowReplaceWarning(false) }} onConfirmReplace={() => { void performImport(extractKeyFromUrl(importKey.trim()) ?? importKey.trim()) }} /> {/* Recovery Phrase Display (after import) */} { setRecoveryPhrase(null) setNewNpub(null) void loadKeys() }} />
) } function KeyManagementErrorBanner(params: { error: string | null }): React.ReactElement | null { if (!params.error) { return null } return (

{params.error}

) } function KeyManagementPublicKeysPanel(params: { publicKeys: PublicKeys | null copiedNpub: boolean copiedPublicKey: boolean onCopyNpub: () => Promise onCopyPublicKey: () => Promise }): React.ReactElement | null { if (!params.publicKeys) { return null } return (
) } function KeyManagementKeyCard(params: { label: string value: string copied: boolean onCopy: () => Promise }): React.ReactElement { return (

{params.label}

{params.value}

) } function KeyManagementNoAccountBanner(params: { publicKeys: PublicKeys | null accountExists: boolean }): React.ReactElement | null { if (params.publicKeys || params.accountExists) { return null } return (

{t('settings.keyManagement.noAccount.title')}

{t('settings.keyManagement.noAccount.description')}

) } function KeyManagementImportButton(params: { accountExists: boolean showImportForm: boolean onClick: () => void }): React.ReactElement | null { if (params.showImportForm) { return null } return ( ) } function KeyManagementImportForm(params: { accountExists: boolean showImportForm: boolean showReplaceWarning: boolean importing: boolean importKey: string onChangeImportKey: (value: string) => void onCancel: () => void onImport: () => void onDismissReplaceWarning: () => void onConfirmReplace: () => void }): React.ReactElement | null { if (!params.showImportForm) { return null } return (

{t('settings.keyManagement.import.warning.title')}

{params.accountExists && (

)}