You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
5.4 KiB
184 lines
5.4 KiB
<script lang="ts"> |
|
import { sessionManager } from '../../services/auth/session-manager.js'; |
|
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
import { KIND } from '../../types/kind-lookup.js'; |
|
import ZapInvoiceModal from './ZapInvoiceModal.svelte'; |
|
import Icon from '../../components/ui/Icon.svelte'; |
|
|
|
interface Props { |
|
event: NostrEvent; |
|
pubkey?: string; // Optional: zap a specific pubkey (for profile zaps) |
|
} |
|
|
|
let { event, pubkey }: Props = $props(); |
|
|
|
let showInvoiceModal = $state(false); |
|
let invoice = $state<string | null>(null); |
|
let lnurl = $state<string | null>(null); |
|
let amount = $state<number>(1000); // Default 1000 sats |
|
|
|
const targetPubkey = $derived(pubkey || event.pubkey); |
|
|
|
async function handleZap() { |
|
if (!sessionManager.isLoggedIn()) { |
|
alert('Please log in to zap'); |
|
return; |
|
} |
|
|
|
try { |
|
// Fetch profile to get lud16 or lnurl |
|
const config = nostrClient.getConfig(); |
|
const profileEvents = await nostrClient.fetchEvents( |
|
[{ kinds: [KIND.METADATA], authors: [targetPubkey], limit: 1 }], |
|
[...config.defaultRelays, ...config.profileRelays], |
|
{ useCache: true } |
|
); |
|
|
|
let zapRequest: NostrEvent | null = null; |
|
|
|
if (profileEvents.length > 0) { |
|
const profile = profileEvents[0]; |
|
// Extract lud16 from profile tags |
|
const lud16Tag = profile.tags.find((t) => t[0] === 'lud16'); |
|
const lud16 = lud16Tag?.[1]; |
|
|
|
if (lud16) { |
|
// Create zap request |
|
const tags: string[][] = [ |
|
['relays', ...config.defaultRelays], |
|
['amount', amount.toString()], |
|
['lnurl', lud16], |
|
['p', targetPubkey] |
|
]; |
|
|
|
if (event.kind !== KIND.METADATA) { |
|
// Zap to an event, not just a profile |
|
tags.push(['e', event.id]); |
|
tags.push(['k', event.kind.toString()]); |
|
} |
|
|
|
const currentPubkey = sessionManager.getCurrentPubkey()!; |
|
const zapRequestEvent = { |
|
kind: 9734, |
|
pubkey: currentPubkey, |
|
created_at: Math.floor(Date.now() / 1000), |
|
tags, |
|
content: 'Zap!' |
|
}; |
|
|
|
const signed = await sessionManager.signEvent(zapRequestEvent); |
|
zapRequest = signed; |
|
|
|
// Try to send via lightning: URI (primary method) |
|
const lightningUri = `lightning:${lud16}?amount=${amount}&nostr=${encodeURIComponent(JSON.stringify(zapRequest))}`; |
|
|
|
try { |
|
window.location.href = lightningUri; |
|
return; // Success, wallet should handle it |
|
} catch (error) { |
|
console.log('lightning: URI not supported, falling back to lnurl'); |
|
} |
|
|
|
// Fallback: Fetch invoice from lnurl |
|
await fetchInvoiceFromLnurl(lud16, zapRequest); |
|
} else { |
|
alert('User has no lightning address configured'); |
|
} |
|
} else { |
|
alert('Could not fetch user profile'); |
|
} |
|
} catch (error) { |
|
console.error('Error creating zap:', error); |
|
alert('Error creating zap'); |
|
} |
|
} |
|
|
|
async function fetchInvoiceFromLnurl(lud16: string, zapRequest: NostrEvent) { |
|
try { |
|
// Parse lud16 (format: user@domain.com) |
|
const [username, domain] = lud16.split('@'); |
|
const callbackUrl = `https://${domain}/.well-known/lnurlp/${username}`; |
|
|
|
// Fetch lnurlp |
|
const response = await fetch(callbackUrl); |
|
const data = await response.json(); |
|
|
|
if (data.callback) { |
|
// Create zap request JSON |
|
const zapRequestJson = JSON.stringify(zapRequest); |
|
|
|
// Call callback with zap request |
|
const callbackUrlWithParams = `${data.callback}?amount=${amount * 1000}&nostr=${encodeURIComponent(zapRequestJson)}`; |
|
const invoiceResponse = await fetch(callbackUrlWithParams); |
|
const invoiceData = await invoiceResponse.json(); |
|
|
|
if (invoiceData.pr) { |
|
invoice = invoiceData.pr; |
|
lnurl = lud16; |
|
showInvoiceModal = true; |
|
} else { |
|
alert('Failed to get invoice from wallet'); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('Error fetching invoice:', error); |
|
alert('Error fetching invoice from wallet'); |
|
} |
|
} |
|
</script> |
|
|
|
<button |
|
onclick={handleZap} |
|
class="zap-button" |
|
data-zap-button |
|
title="Zap (z)" |
|
aria-label="Zap" |
|
> |
|
<Icon name="zap" size={16} /> |
|
<span>Zap</span> |
|
</button> |
|
|
|
{#if showInvoiceModal && invoice} |
|
<ZapInvoiceModal |
|
{invoice} |
|
{lnurl} |
|
{amount} |
|
onClose={() => { |
|
showInvoiceModal = false; |
|
invoice = null; |
|
}} |
|
/> |
|
{/if} |
|
|
|
<style> |
|
.zap-button { |
|
padding: 0.25rem 0.75rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
background: var(--fog-post, #ffffff); |
|
color: var(--fog-text, #1f2937); |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
font-size: 0.875rem; |
|
line-height: 1.5; |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 0.375rem; |
|
} |
|
|
|
:global(.dark) .zap-button { |
|
background: var(--fog-dark-post, #1f2937); |
|
border-color: var(--fog-dark-border, #374151); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.zap-button:hover { |
|
background: var(--fog-highlight, #f3f4f6); |
|
border-color: var(--fog-accent, #64748b); |
|
} |
|
|
|
:global(.dark) .zap-button:hover { |
|
background: var(--fog-dark-highlight, #374151); |
|
} |
|
</style>
|
|
|