diff --git a/components/ImageUploadField.tsx b/components/ImageUploadField.tsx index 7c2e4b2..ccf91e3 100644 --- a/components/ImageUploadField.tsx +++ b/components/ImageUploadField.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { uploadNip95Media } from '@/lib/nip95' import { t } from '@/lib/i18n' import Image from 'next/image' +import { UnlockAccountModal } from './UnlockAccountModal' interface ImageUploadFieldProps { id: string @@ -94,6 +95,8 @@ async function processFileUpload(file: File, onChange: (url: string) => void, se function useImageUpload(onChange: (url: string) => void) { const [uploading, setUploading] = useState(false) const [error, setError] = useState(null) + const [showUnlockModal, setShowUnlockModal] = useState(false) + const [pendingFile, setPendingFile] = useState(null) const handleFileSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] @@ -107,35 +110,70 @@ function useImageUpload(onChange: (url: string) => void) { try { await processFileUpload(file, onChange, setError) } catch (e) { - setError(e instanceof Error ? e.message : t('presentation.field.picture.error.uploadFailed')) + const error = e instanceof Error ? e : new Error(String(e)) + // Check if unlock is required + if (error.message === 'UNLOCK_REQUIRED' || (error as any).unlockRequired) { + setPendingFile(file) + setShowUnlockModal(true) + setError(null) // Don't show error, show unlock modal instead + } else { + setError(error.message || t('presentation.field.picture.error.uploadFailed')) + } } finally { setUploading(false) } } - return { uploading, error, handleFileSelect } + const handleUnlockSuccess = async () => { + setShowUnlockModal(false) + if (pendingFile) { + // Retry upload after unlock + setUploading(true) + setError(null) + try { + await processFileUpload(pendingFile, onChange, setError) + setPendingFile(null) + } catch (e) { + setError(e instanceof Error ? e.message : t('presentation.field.picture.error.uploadFailed')) + } finally { + setUploading(false) + } + } + } + + return { uploading, error, handleFileSelect, showUnlockModal, setShowUnlockModal, handleUnlockSuccess } } export function ImageUploadField({ id, label, value, onChange, helpText }: ImageUploadFieldProps) { - const { uploading, error, handleFileSelect } = useImageUpload(onChange) + const { uploading, error, handleFileSelect, showUnlockModal, setShowUnlockModal, handleUnlockSuccess } = useImageUpload(onChange) const displayLabel = label ?? t('presentation.field.picture') const displayHelpText = helpText ?? t('presentation.field.picture.help') return ( -
- - {value && } - - {error &&

{error}

} - {displayHelpText &&

{displayHelpText}

} -
+ <> +
+ + {value && } + + {error &&

{error}

} + {displayHelpText &&

{displayHelpText}

} +
+ {showUnlockModal && ( + { + setShowUnlockModal(false) + }} + /> + )} + ) } diff --git a/components/KeyIndicator.tsx b/components/KeyIndicator.tsx new file mode 100644 index 0000000..1f53ab2 --- /dev/null +++ b/components/KeyIndicator.tsx @@ -0,0 +1,21 @@ +import { useNostrAuth } from '@/hooks/useNostrAuth' + +export function KeyIndicator() { + const { pubkey, isUnlocked } = useNostrAuth() + + // No indicator if no account + if (!pubkey) { + return null + } + + // Red if private key is accessible (unlocked) + // Green if only public key is accessible (connected but not unlocked) + const color = isUnlocked ? 'text-red-500' : 'text-green-500' + const title = isUnlocked ? 'Private key accessible' : 'Public key accessible' + + return ( + + 🔑 + + ) +} diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx index 3051d33..5383994 100644 --- a/components/PageHeader.tsx +++ b/components/PageHeader.tsx @@ -2,13 +2,15 @@ import Link from 'next/link' import { ConditionalPublishButton } from './ConditionalPublishButton' import { LanguageSelector } from './LanguageSelector' import { t } from '@/lib/i18n' +import { KeyIndicator } from './KeyIndicator' export function PageHeader() { return (
- + {t('home.title')} +
diff --git a/components/ProfileHeader.tsx b/components/ProfileHeader.tsx index 03b2d21..664ceac 100644 --- a/components/ProfileHeader.tsx +++ b/components/ProfileHeader.tsx @@ -1,11 +1,15 @@ import { ConnectButton } from '@/components/ConnectButton' import { ConditionalPublishButton } from './ConditionalPublishButton' +import { KeyIndicator } from './KeyIndicator' export function ProfileHeader() { return (
-

zapwall.fr

+

+ zapwall.fr + +

diff --git a/lib/configStorageTypes.ts b/lib/configStorageTypes.ts index 6e4d778..e04fc9a 100644 --- a/lib/configStorageTypes.ts +++ b/lib/configStorageTypes.ts @@ -42,7 +42,7 @@ export const DEFAULT_NIP95_APIS: Nip95Config[] = [ { id: 'nostrimg', url: 'https://nostrimg.com/api/upload', - enabled: true, // Temporarily enabled for diagnostic logging - disable if 500 errors persist + enabled: true, priority: 1, createdAt: Date.now(), }, diff --git a/lib/nip95.ts b/lib/nip95.ts index e9b7a2f..d44da18 100644 --- a/lib/nip95.ts +++ b/lib/nip95.ts @@ -137,12 +137,17 @@ export async function uploadNip95Media(file: File): Promise { const isUnlocked = nostrAuthService.isUnlocked() if (!pubkey) { console.warn('NIP-98 authentication required for nostrcheck.me but no account found. Please create or import an account.') + continue } else if (!isUnlocked) { - console.warn('NIP-98 authentication required for nostrcheck.me but account is not unlocked. Please unlock your account with your recovery phrase to use this endpoint.') + // Throw a special error that can be caught to trigger unlock modal + // This error should propagate to the caller, not be caught here + const unlockError = new Error('UNLOCK_REQUIRED') + ;(unlockError as any).unlockRequired = true + throw unlockError } else { console.warn('NIP-98 authentication required for nostrcheck.me but not available. Skipping endpoint.') + continue } - continue } try { // Generate NIP-98 token for the actual endpoint (not the proxy) @@ -170,6 +175,11 @@ export async function uploadNip95Media(file: File): Promise { const error = e instanceof Error ? e : new Error(String(e)) const errorMessage = error.message + // If unlock is required, propagate the error immediately + if (errorMessage === 'UNLOCK_REQUIRED' || (error as any).unlockRequired) { + throw error + } + console.error('NIP-95 upload endpoint error:', { endpoint, error: errorMessage, diff --git a/pages/api/nip95-upload.ts b/pages/api/nip95-upload.ts index b4bc657..fb3aa8a 100644 --- a/pages/api/nip95-upload.ts +++ b/pages/api/nip95-upload.ts @@ -209,6 +209,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }) requestFormData.on('error', (error) => { + console.error('NIP-95 proxy FormData error:', { + targetEndpoint, + hostname: url.hostname, + error: error instanceof Error ? error.message : 'Unknown FormData error', + }) + reject(error) + }) + + fileStream.on('error', (error) => { + console.error('NIP-95 proxy file stream error:', { + targetEndpoint, + hostname: url.hostname, + filepath: fileField.filepath, + error: error instanceof Error ? error.message : 'Unknown file stream error', + }) reject(error) })