lint fix wip
This commit is contained in:
parent
9e3d2c8742
commit
7c0dc68304
@ -60,10 +60,14 @@ export function LanguageSelector(): React.ReactElement {
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onLocaleClick = (locale: Locale): void => {
|
||||||
|
void handleLocaleChange(locale)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LocaleButton locale="fr" label="FR" currentLocale={currentLocale} onClick={handleLocaleChange} />
|
<LocaleButton locale="fr" label="FR" currentLocale={currentLocale} onClick={onLocaleClick} />
|
||||||
<LocaleButton locale="en" label="EN" currentLocale={currentLocale} onClick={handleLocaleChange} />
|
<LocaleButton locale="en" label="EN" currentLocale={currentLocale} onClick={onLocaleClick} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,10 @@ export function LanguageSettingsManager(): React.ReactElement {
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onLocaleClick = (locale: Locale): void => {
|
||||||
|
void handleLocaleChange(locale)
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
|
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
|
||||||
@ -76,8 +80,8 @@ export function LanguageSettingsManager(): React.ReactElement {
|
|||||||
<h2 className="text-2xl font-bold text-neon-cyan mb-4">{t('settings.language.title')}</h2>
|
<h2 className="text-2xl font-bold text-neon-cyan mb-4">{t('settings.language.title')}</h2>
|
||||||
<p className="text-cyber-accent mb-4 text-sm">{t('settings.language.description')}</p>
|
<p className="text-cyber-accent mb-4 text-sm">{t('settings.language.description')}</p>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<LocaleOption locale="fr" label={t('settings.language.french')} currentLocale={currentLocale} onClick={handleLocaleChange} />
|
<LocaleOption locale="fr" label={t('settings.language.french')} currentLocale={currentLocale} onClick={onLocaleClick} />
|
||||||
<LocaleOption locale="en" label={t('settings.language.english')} currentLocale={currentLocale} onClick={handleLocaleChange} />
|
<LocaleOption locale="en" label={t('settings.language.english')} currentLocale={currentLocale} onClick={onLocaleClick} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -277,13 +277,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps):
|
|||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
handleDrop(e, api.id)
|
handleDrop(e, api.id)
|
||||||
}}
|
}}
|
||||||
className={`bg-cyber-dark border rounded p-4 space-y-3 transition-all ${
|
className={`bg-cyber-dark border rounded p-4 space-y-3 transition-all ${getApiCardClassName(api.id, draggedId, dragOverId)}`}
|
||||||
draggedId === api.id
|
|
||||||
? 'opacity-50 border-neon-cyan'
|
|
||||||
: dragOverId === api.id
|
|
||||||
? 'border-neon-green shadow-lg'
|
|
||||||
: 'border-neon-cyan/30'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
@ -377,3 +371,13 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps):
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getApiCardClassName(apiId: string, draggedId: string | null, dragOverId: string | null): string {
|
||||||
|
if (draggedId === apiId) {
|
||||||
|
return 'opacity-50 border-neon-cyan'
|
||||||
|
}
|
||||||
|
if (dragOverId === apiId) {
|
||||||
|
return 'border-neon-green shadow-lg'
|
||||||
|
}
|
||||||
|
return 'border-neon-cyan/30'
|
||||||
|
}
|
||||||
|
|||||||
@ -143,7 +143,7 @@ function WordInputs({
|
|||||||
|
|
||||||
function useUnlockAccount(words: string[], setWords: (words: string[]) => void, setError: (error: string | null) => void): {
|
function useUnlockAccount(words: string[], setWords: (words: string[]) => void, setError: (error: string | null) => void): {
|
||||||
handleWordChange: (index: number, value: string) => void
|
handleWordChange: (index: number, value: string) => void
|
||||||
handlePaste: () => Promise<void>
|
handlePaste: () => void
|
||||||
} {
|
} {
|
||||||
const handleWordChange = (index: number, value: string): void => {
|
const handleWordChange = (index: number, value: string): void => {
|
||||||
const newWords = [...words]
|
const newWords = [...words]
|
||||||
@ -152,7 +152,7 @@ function useUnlockAccount(words: string[], setWords: (words: string[]) => void,
|
|||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = async (): Promise<void> => {
|
const handlePasteAsync = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
const pastedWords = text.trim().split(/\s+/).slice(0, 4)
|
const pastedWords = text.trim().split(/\s+/).slice(0, 4)
|
||||||
@ -165,6 +165,10 @@ function useUnlockAccount(words: string[], setWords: (words: string[]) => void,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePaste = (): void => {
|
||||||
|
void handlePasteAsync()
|
||||||
|
}
|
||||||
|
|
||||||
return { handleWordChange, handlePaste }
|
return { handleWordChange, handlePaste }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +255,10 @@ export function UnlockAccountModal({ onSuccess, onClose }: UnlockAccountModalPro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onUnlock = (): void => {
|
||||||
|
void handleUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||||
@ -261,7 +269,7 @@ export function UnlockAccountModal({ onSuccess, onClose }: UnlockAccountModalPro
|
|||||||
</p>
|
</p>
|
||||||
<UnlockAccountForm words={words} handleWordChange={handleWordChange} handlePaste={handlePaste} />
|
<UnlockAccountForm words={words} handleWordChange={handleWordChange} handlePaste={handlePaste} />
|
||||||
{error && <p className="text-sm text-red-600 mb-4">{error}</p>}
|
{error && <p className="text-sm text-red-600 mb-4">{error}</p>}
|
||||||
<UnlockAccountButtons loading={loading} words={words} onUnlock={handleUnlock} onClose={onClose} />
|
<UnlockAccountButtons loading={loading} words={words} onUnlock={onUnlock} onClose={onClose} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -48,11 +48,12 @@ export function useNotifications(userPubkey: string | null): {
|
|||||||
const unreadCount = notifications.filter((n) => !n.read).length
|
const unreadCount = notifications.filter((n) => !n.read).length
|
||||||
|
|
||||||
const markAsRead = useCallback(
|
const markAsRead = useCallback(
|
||||||
async (notificationId: string): Promise<void> => {
|
(notificationId: string): void => {
|
||||||
if (!userPubkey) {
|
if (!userPubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void (async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await notificationService.markAsRead(notificationId)
|
await notificationService.markAsRead(notificationId)
|
||||||
setNotifications((prev) =>
|
setNotifications((prev) =>
|
||||||
@ -61,35 +62,40 @@ export function useNotifications(userPubkey: string | null): {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useNotifications] Error marking notification as read:', error)
|
console.error('[useNotifications] Error marking notification as read:', error)
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
},
|
},
|
||||||
[userPubkey]
|
[userPubkey]
|
||||||
)
|
)
|
||||||
|
|
||||||
const markAllAsReadHandler = useCallback(async (): Promise<void> => {
|
const markAllAsReadHandler = useCallback((): void => {
|
||||||
if (!userPubkey) {
|
if (!userPubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void (async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await notificationService.markAllAsRead()
|
await notificationService.markAllAsRead()
|
||||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useNotifications] Error marking all as read:', error)
|
console.error('[useNotifications] Error marking all as read:', error)
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
}, [userPubkey])
|
}, [userPubkey])
|
||||||
|
|
||||||
const deleteNotificationHandler = useCallback(
|
const deleteNotificationHandler = useCallback(
|
||||||
async (notificationId: string): Promise<void> => {
|
(notificationId: string): void => {
|
||||||
if (!userPubkey) {
|
if (!userPubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void (async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await notificationService.deleteNotification(notificationId)
|
await notificationService.deleteNotification(notificationId)
|
||||||
setNotifications((prev) => prev.filter((n) => n.id !== notificationId))
|
setNotifications((prev) => prev.filter((n) => n.id !== notificationId))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useNotifications] Error deleting notification:', error)
|
console.error('[useNotifications] Error deleting notification:', error)
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
},
|
},
|
||||||
[userPubkey]
|
[userPubkey]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -140,11 +140,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
|
|||||||
} | null = null
|
} | null = null
|
||||||
|
|
||||||
if (tags.json) {
|
if (tags.json) {
|
||||||
try {
|
profileData = parsePresentationProfileJson(tags.json)
|
||||||
profileData = JSON.parse(tags.json)
|
|
||||||
} catch (jsonError) {
|
|
||||||
console.error('Error parsing JSON from tag:', jsonError)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to content format (for backward compatibility with old notes)
|
// Fallback to content format (for backward compatibility with old notes)
|
||||||
@ -155,7 +151,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
|
|||||||
try {
|
try {
|
||||||
// Remove zero-width characters from JSON
|
// Remove zero-width characters from JSON
|
||||||
const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim()
|
const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim()
|
||||||
profileData = JSON.parse(cleanedJson)
|
profileData = parsePresentationProfileJson(cleanedJson)
|
||||||
} catch (invisibleJsonError) {
|
} catch (invisibleJsonError) {
|
||||||
console.error('Error parsing profile JSON from invisible content:', invisibleJsonError)
|
console.error('Error parsing profile JSON from invisible content:', invisibleJsonError)
|
||||||
}
|
}
|
||||||
@ -165,11 +161,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
|
|||||||
if (!profileData) {
|
if (!profileData) {
|
||||||
const jsonMatch = event.content.match(/\[Metadata JSON\]\n(.+)$/s)
|
const jsonMatch = event.content.match(/\[Metadata JSON\]\n(.+)$/s)
|
||||||
if (jsonMatch?.[1]) {
|
if (jsonMatch?.[1]) {
|
||||||
try {
|
profileData = parsePresentationProfileJson(jsonMatch[1].trim())
|
||||||
profileData = JSON.parse(jsonMatch[1].trim())
|
|
||||||
} catch (contentJsonError) {
|
|
||||||
console.error('Error parsing profile JSON from content:', contentJsonError)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,6 +270,56 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parsePresentationProfileJson(json: string): {
|
||||||
|
authorName?: string
|
||||||
|
presentation?: string
|
||||||
|
contentDescription?: string
|
||||||
|
mainnetAddress?: string
|
||||||
|
pictureUrl?: string
|
||||||
|
category?: string
|
||||||
|
} | null {
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(json)
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = parsed as Record<string, unknown>
|
||||||
|
const result: {
|
||||||
|
authorName?: string
|
||||||
|
presentation?: string
|
||||||
|
contentDescription?: string
|
||||||
|
mainnetAddress?: string
|
||||||
|
pictureUrl?: string
|
||||||
|
category?: string
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
if (typeof obj.authorName === 'string') {
|
||||||
|
result.authorName = obj.authorName
|
||||||
|
}
|
||||||
|
if (typeof obj.presentation === 'string') {
|
||||||
|
result.presentation = obj.presentation
|
||||||
|
}
|
||||||
|
if (typeof obj.contentDescription === 'string') {
|
||||||
|
result.contentDescription = obj.contentDescription
|
||||||
|
}
|
||||||
|
if (typeof obj.mainnetAddress === 'string') {
|
||||||
|
result.mainnetAddress = obj.mainnetAddress
|
||||||
|
}
|
||||||
|
if (typeof obj.pictureUrl === 'string') {
|
||||||
|
result.pictureUrl = obj.pictureUrl
|
||||||
|
}
|
||||||
|
if (typeof obj.category === 'string') {
|
||||||
|
result.category = obj.category
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing presentation profile JSON:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAuthorPresentationFromPool(
|
export async function fetchAuthorPresentationFromPool(
|
||||||
pool: SimplePoolWithSub,
|
pool: SimplePoolWithSub,
|
||||||
pubkey: string
|
pubkey: string
|
||||||
|
|||||||
10
lib/nostr.ts
10
lib/nostr.ts
@ -113,9 +113,8 @@ class NostrService {
|
|||||||
success: true,
|
success: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const error = result.reason
|
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, result.reason)
|
||||||
console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, error)
|
|
||||||
relaySessionManager.markRelayFailed(relayUrl)
|
relaySessionManager.markRelayFailed(relayUrl)
|
||||||
// Log failed publication
|
// Log failed publication
|
||||||
void publishLog.logPublication({
|
void publishLog.logPublication({
|
||||||
@ -158,9 +157,8 @@ class NostrService {
|
|||||||
success: true,
|
success: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const error = result.reason
|
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, result.reason)
|
||||||
console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, error)
|
|
||||||
relaySessionManager.markRelayFailed(relayUrl)
|
relaySessionManager.markRelayFailed(relayUrl)
|
||||||
// Log failed publication
|
// Log failed publication
|
||||||
void publishLog.logPublication({
|
void publishLog.logPublication({
|
||||||
|
|||||||
@ -150,6 +150,7 @@ class NotificationDetector {
|
|||||||
try {
|
try {
|
||||||
// Get all object types that can be published
|
// Get all object types that can be published
|
||||||
const objectTypes = ['author', 'series', 'publication', 'review', 'purchase', 'sponsoring', 'review_tip', 'payment_note']
|
const objectTypes = ['author', 'series', 'publication', 'review', 'purchase', 'sponsoring', 'review_tip', 'payment_note']
|
||||||
|
const oneHourAgo = Date.now() - 60 * 60 * 1000
|
||||||
|
|
||||||
for (const objectType of objectTypes) {
|
for (const objectType of objectTypes) {
|
||||||
try {
|
try {
|
||||||
@ -161,24 +162,25 @@ class NotificationDetector {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for (const obj of userObjects) {
|
for (const obj of userObjects) {
|
||||||
// Check if object was recently published (published changed from false to array)
|
if (!Array.isArray(obj.published) || obj.published.length === 0) {
|
||||||
if (Array.isArray(obj.published) && obj.published.length > 0) {
|
continue
|
||||||
// Check if we already created a notification for this
|
}
|
||||||
|
|
||||||
const eventId = obj.id.split(':')[1] ?? obj.id
|
const eventId = obj.id.split(':')[1] ?? obj.id
|
||||||
const existing = await notificationService.getNotificationByEventId(eventId)
|
const existing = await notificationService.getNotificationByEventId(eventId)
|
||||||
|
if (existing?.type === 'published') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (existing?.type !== 'published') {
|
if (obj.createdAt * 1000 <= oneHourAgo) {
|
||||||
// Check if this is a recent change (within last 5 minutes)
|
continue
|
||||||
// We can't track old/new state easily, so we'll create notification
|
}
|
||||||
// if object was published recently (created in last hour and published)
|
|
||||||
const oneHourAgo = Date.now() - 60 * 60 * 1000
|
const relays = obj.published
|
||||||
const cachedObj = obj
|
|
||||||
if (cachedObj.createdAt * 1000 > oneHourAgo) {
|
|
||||||
const relays = cachedObj.published as string[]
|
|
||||||
await notificationService.createNotification({
|
await notificationService.createNotification({
|
||||||
type: 'published',
|
type: 'published',
|
||||||
objectType,
|
objectType,
|
||||||
objectId: cachedObj.id,
|
objectId: obj.id,
|
||||||
eventId,
|
eventId,
|
||||||
data: {
|
data: {
|
||||||
relays,
|
relays,
|
||||||
@ -188,9 +190,6 @@ class NotificationDetector {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[NotificationDetector] Error scanning published status for ${objectType}:`, error)
|
console.error(`[NotificationDetector] Error scanning published status for ${objectType}:`, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,23 +45,22 @@ export async function sendPrivateContentAfterPayment(
|
|||||||
// Publish explicit payment note (kind 1) with project tags
|
// Publish explicit payment note (kind 1) with project tags
|
||||||
try {
|
try {
|
||||||
const article = await nostrService.getArticleById(articleId)
|
const article = await nostrService.getArticleById(articleId)
|
||||||
if (article) {
|
if (!article) {
|
||||||
const payerPrivateKey = nostrService.getPrivateKey()
|
return logPaymentResult(result, articleId, recipientPubkey, amount)
|
||||||
if (payerPrivateKey) {
|
|
||||||
// Get zap receipt to extract payment hash and other info
|
|
||||||
let paymentHash = article.paymentHash ?? articleId
|
|
||||||
if (zapReceiptId) {
|
|
||||||
const { getZapReceiptById } = await import('./zapReceiptQueries')
|
|
||||||
const zapReceipt = await getZapReceiptById(zapReceiptId)
|
|
||||||
if (zapReceipt) {
|
|
||||||
const paymentHashTag = zapReceipt.tags.find((tag) => tag[0] === 'payment_hash')?.[1]
|
|
||||||
if (paymentHashTag) {
|
|
||||||
paymentHash = paymentHashTag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = article.category === 'author-presentation' ? undefined : (article.category === 'science-fiction' || article.category === 'scientific-research' ? article.category : undefined)
|
const payerPrivateKey = nostrService.getPrivateKey()
|
||||||
|
if (!payerPrivateKey) {
|
||||||
|
return logPaymentResult(result, articleId, recipientPubkey, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentHash = await resolvePaymentHashForPurchaseNote({
|
||||||
|
articlePaymentHash: article.paymentHash,
|
||||||
|
articleId,
|
||||||
|
zapReceiptId,
|
||||||
|
})
|
||||||
|
const category = normalizePurchaseNoteCategory(article.category)
|
||||||
|
|
||||||
await publishPurchaseNote({
|
await publishPurchaseNote({
|
||||||
articleId: article.id,
|
articleId: article.id,
|
||||||
authorPubkey: article.pubkey,
|
authorPubkey: article.pubkey,
|
||||||
@ -73,8 +72,6 @@ export async function sendPrivateContentAfterPayment(
|
|||||||
...(article.seriesId ? { seriesId: article.seriesId } : {}),
|
...(article.seriesId ? { seriesId: article.seriesId } : {}),
|
||||||
payerPrivateKey,
|
payerPrivateKey,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing purchase note:', error)
|
console.error('Error publishing purchase note:', error)
|
||||||
// Don't fail the payment if note publication fails
|
// Don't fail the payment if note publication fails
|
||||||
@ -85,3 +82,31 @@ export async function sendPrivateContentAfterPayment(
|
|||||||
|
|
||||||
return logPaymentResult(result, articleId, recipientPubkey, amount)
|
return logPaymentResult(result, articleId, recipientPubkey, amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePurchaseNoteCategory(
|
||||||
|
category: 'author-presentation' | 'science-fiction' | 'scientific-research' | string
|
||||||
|
): 'science-fiction' | 'scientific-research' | undefined {
|
||||||
|
if (category === 'science-fiction' || category === 'scientific-research') {
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvePaymentHashForPurchaseNote(params: {
|
||||||
|
articlePaymentHash: string | undefined
|
||||||
|
articleId: string
|
||||||
|
zapReceiptId?: string
|
||||||
|
}): Promise<string> {
|
||||||
|
let paymentHash = params.articlePaymentHash ?? params.articleId
|
||||||
|
if (!params.zapReceiptId) {
|
||||||
|
return paymentHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getZapReceiptById } = await import('./zapReceiptQueries')
|
||||||
|
const zapReceipt = await getZapReceiptById(params.zapReceiptId)
|
||||||
|
const paymentHashTag = zapReceipt?.tags.find((tag) => tag[0] === 'payment_hash')?.[1]
|
||||||
|
if (paymentHashTag) {
|
||||||
|
paymentHash = paymentHashTag
|
||||||
|
}
|
||||||
|
return paymentHash
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { IncomingForm, File as FormidableFile } from 'formidable'
|
import { IncomingForm, File as FormidableFile } from 'formidable'
|
||||||
|
import type { Fields, Files } from 'formidable'
|
||||||
import FormData from 'form-data'
|
import FormData from 'form-data'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
@ -15,8 +16,57 @@ export const config = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ParseResult {
|
interface ParseResult {
|
||||||
fields: Record<string, string[]>
|
fields: Fields
|
||||||
files: Record<string, FormidableFile[]>
|
files: Files
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(value: unknown): string | undefined {
|
||||||
|
return typeof value === 'string' ? value : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFirstFile(files: Files, fieldName: string): FormidableFile | null {
|
||||||
|
const value = files[fieldName]
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const first = value[0]
|
||||||
|
return first ?? null
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormDataHeaders(formData: FormData): http.OutgoingHttpHeaders {
|
||||||
|
const raw: unknown = formData.getHeaders()
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const headers: http.OutgoingHttpHeaders = {}
|
||||||
|
for (const [key, value] of Object.entries(raw)) {
|
||||||
|
const str = readString(value)
|
||||||
|
if (str !== undefined) {
|
||||||
|
headers[key] = str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRedirectLocation(headers: unknown): string | undefined {
|
||||||
|
if (!isRecord(headers)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const {location} = headers
|
||||||
|
if (typeof location === 'string') {
|
||||||
|
return location
|
||||||
|
}
|
||||||
|
if (Array.isArray(location) && typeof location[0] === 'string') {
|
||||||
|
return location[0]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
|
||||||
@ -37,13 +87,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
})
|
})
|
||||||
|
|
||||||
const parseResult = await new Promise<ParseResult>((resolve, reject) => {
|
const parseResult = await new Promise<ParseResult>((resolve, reject) => {
|
||||||
// Cast req to any to work with formidable - NextApiRequest extends IncomingMessage
|
form.parse(req, (err, fields, files) => {
|
||||||
form.parse(req as any, (err, fields, files) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Formidable parse error:', err)
|
console.error('Formidable parse error:', err)
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve({ fields: fields as Record<string, string[]>, files: files as Record<string, FormidableFile[]> })
|
resolve({ fields, files })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -51,7 +100,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const { files } = parseResult
|
const { files } = parseResult
|
||||||
|
|
||||||
// Get the file from the parsed form
|
// Get the file from the parsed form
|
||||||
const fileField = files.file?.[0]
|
const fileField = getFirstFile(files, 'file')
|
||||||
if (!fileField) {
|
if (!fileField) {
|
||||||
return res.status(400).json({ error: 'No file provided' })
|
return res.status(400).json({ error: 'No file provided' })
|
||||||
}
|
}
|
||||||
@ -84,7 +133,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const isHttps = url.protocol === 'https:'
|
const isHttps = url.protocol === 'https:'
|
||||||
const clientModule = isHttps ? https : http
|
const clientModule = isHttps ? https : http
|
||||||
const headers = requestFormData.getHeaders()
|
const headers = getFormDataHeaders(requestFormData)
|
||||||
|
|
||||||
// Add standard headers that some endpoints require
|
// Add standard headers that some endpoints require
|
||||||
headers['Accept'] = 'application/json'
|
headers['Accept'] = 'application/json'
|
||||||
@ -113,7 +162,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions: http.RequestOptions = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port ?? (isHttps ? 443 : 80),
|
port: url.port ?? (isHttps ? 443 : 80),
|
||||||
path: url.pathname + url.search,
|
path: url.pathname + url.search,
|
||||||
@ -122,11 +171,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
timeout: 30000, // 30 seconds timeout
|
timeout: 30000, // 30 seconds timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => {
|
const proxyRequest = clientModule.request(requestOptions, (proxyResponse: http.IncomingMessage) => {
|
||||||
// Handle redirects (301, 302, 307, 308)
|
// Handle redirects (301, 302, 307, 308)
|
||||||
const statusCode = proxyResponse.statusCode ?? 500
|
const statusCode = proxyResponse.statusCode ?? 500
|
||||||
if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) {
|
if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) {
|
||||||
const {location} = proxyResponse.headers
|
const location = getRedirectLocation(proxyResponse.headers as unknown)
|
||||||
|
if (!location) {
|
||||||
|
reject(new Error('Redirect response missing location header'))
|
||||||
|
return
|
||||||
|
}
|
||||||
let redirectUrl: URL
|
let redirectUrl: URL
|
||||||
try {
|
try {
|
||||||
// Handle relative and absolute URLs
|
// Handle relative and absolute URLs
|
||||||
|
|||||||
@ -295,6 +295,9 @@ export default function AuthorPage(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { presentation, series, totalSponsoring, loading, error, reload } = useAuthorData(hashIdOrPubkey ?? '')
|
const { presentation, series, totalSponsoring, loading, error, reload } = useAuthorData(hashIdOrPubkey ?? '')
|
||||||
|
const onSeriesCreated = (): void => {
|
||||||
|
void reload()
|
||||||
|
}
|
||||||
|
|
||||||
if (!hashIdOrPubkey) {
|
if (!hashIdOrPubkey) {
|
||||||
return <div />
|
return <div />
|
||||||
@ -320,7 +323,7 @@ export default function AuthorPage(): React.ReactElement {
|
|||||||
authorPubkey={actualAuthorPubkey}
|
authorPubkey={actualAuthorPubkey}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
onSeriesCreated={reload}
|
onSeriesCreated={onSeriesCreated}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user