import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { useToast } from '@/hooks/useToast'; import { useWallet } from '@/hooks/useWallet'; import { useNWC } from '@/hooks/useNWCContext'; import { PAYMENT_STATUS_UNKNOWN } from '@/hooks/useNWC'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { getInvoiceFromLightningAddress, getZapInvoiceFromLightningAddress, getZapProviderPubkey, } from '@/lib/lightningAddress'; import { usePlatformFee, type FeeType } from '@/hooks/usePlatformFee'; import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions'; import { useTranslation } from '@/contexts/language'; import { cn } from '@/lib/utils'; import { assertInvoiceAmount } from '@/lib/verifyInvoice'; import { Zap, Loader2, CheckCircle2, Copy, ExternalLink, Rocket, ShieldCheck, Clock, } from 'lucide-react'; interface PlatformFeePaymentProps { type: FeeType; /** Called after payment confirmed */ onSuccess: () => void; /** Optional extra context shown to user */ context?: string; /** * The addressable coordinate (`kind:pubkey:d`) of the entity being featured. * When provided and the provider supports NIP-57, the zap request tags this * `a` coordinate so the payment can later be verified against the entity. */ entityCoord?: string; } // Relays the provider should publish the zap receipt to (so we can verify it). const ZAP_RECEIPT_RELAYS = [ 'wss://goskatespots.nostr1.com', 'wss://relay.nostr.band', 'wss://relay.damus.io', ]; // Static maps to explicit `feesUi.*` literal keys (no template keys). const FEE_LABEL_KEYS: Record = { featuredEvent: 'feesUi.feeFeaturedEventLabel', labProfile: 'feesUi.feeLabProfileLabel', featuredShop: 'feesUi.feeFeaturedShopLabel', featuredTour: 'feesUi.feeFeaturedTourLabel', }; const FEE_DESC_KEYS: Record = { featuredEvent: 'feesUi.feeFeaturedEventDesc', labProfile: 'feesUi.feeLabProfileDesc', featuredShop: 'feesUi.feeFeaturedShopDesc', featuredTour: 'feesUi.feeFeaturedTourDesc', }; const FEE_BENEFIT_KEYS: Record = { featuredEvent: 'feesUi.feeFeaturedEventBenefit', labProfile: 'feesUi.feeLabProfileBenefit', featuredShop: 'feesUi.feeFeaturedShopBenefit', featuredTour: 'feesUi.feeFeaturedTourBenefit', }; const DURATION_LABEL_KEYS: Record = { '7d': 'feesUi.duration7d', '30d': 'feesUi.duration30d', '90d': 'feesUi.duration90d', '365d': 'feesUi.duration365d', }; type PaymentState = 'idle' | 'generating' | 'waiting' | 'paying' | 'success' | 'error'; export function PlatformFeePayment({ type, onSuccess, context, entityCoord }: PlatformFeePaymentProps) { const { amountSats, recipient, recipientName, meta, tier, tiers, setTierId, isActive, daysRemaining, recordPayment, tierPriceSats: feeTierPrice, tierPerDaySats: feeTierPerDay, } = usePlatformFee(type); const { toast } = useToast(); const { webln, activeNWC } = useWallet(); const { sendPayment } = useNWC(); const { user } = useCurrentUser(); const { t } = useTranslation(); // Localized fee metadata (labels/descriptions/benefits live in `feesUi.*`). const metaLabel = t(FEE_LABEL_KEYS[type]); const metaDesc = t(FEE_DESC_KEYS[type]); const metaBenefit = t(FEE_BENEFIT_KEYS[type]); const tierLabel = t(DURATION_LABEL_KEYS[tier.id] ?? 'feesUi.duration30d'); const [paymentState, setPaymentState] = useState('idle'); const [invoice, setInvoice] = useState(null); const [qrCodeUrl, setQrCodeUrl] = useState(null); const [error, setError] = useState(null); // Auto-pay when wallet connected and invoice ready useEffect(() => { if (invoice && paymentState === 'waiting' && (webln || activeNWC)) { handleAutoPay(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [invoice, paymentState]); // Already paid and active if (isActive) { return (

{t('feesUi.metaActive', { label: metaLabel })}

{daysRemaining === 1 ? t('feesUi.daysRemainingOne', { count: daysRemaining }) : t('feesUi.daysRemainingMany', { count: daysRemaining })}

); } const generateInvoice = async () => { setPaymentState('generating'); setError(null); try { let bolt11: string | null = null; // Prefer a NIP-57 zap so the payment produces a verifiable zap receipt. // Falls back to a plain LNURL invoice if the provider doesn't support zaps // or the user can't sign. try { const providerPubkey = await getZapProviderPubkey(recipient); if (providerPubkey && user?.signer) { const platformPubkey = PLATFORM_OWNER_PUBKEYS[0]; const tags: string[][] = [ ['relays', ...ZAP_RECEIPT_RELAYS], ['amount', String(amountSats * 1000)], ['p', platformPubkey], ]; if (entityCoord) { tags.push(['a', entityCoord]); } const zapRequest = await user.signer.signEvent({ kind: 9734, content: t('feesUi.zapRequestContent', { label: metaLabel, tier: tierLabel }), created_at: Math.floor(Date.now() / 1000), tags, }); bolt11 = await getZapInvoiceFromLightningAddress({ lightningAddress: recipient, amountSats, zapRequest: JSON.stringify(zapRequest), }); } } catch { // Zap path failed — fall back to a normal invoice below. bolt11 = null; } if (!bolt11) { bolt11 = await getInvoiceFromLightningAddress({ lightningAddress: recipient, amountSats, comment: t('feesUi.zapRequestContent', { label: metaLabel, tier: tierLabel }), }); } // SECURITY: the provider-returned invoice must match the fee we asked for. assertInvoiceAmount(bolt11, amountSats); setInvoice(bolt11); const { default: QRCode } = await import('qrcode'); const qr = await QRCode.toDataURL(bolt11.toUpperCase(), { width: 256, margin: 2, color: { dark: '#000000', light: '#ffffff' }, }); setQrCodeUrl(qr); setPaymentState('waiting'); if (webln || activeNWC) { await handleAutoPay(); } } catch (err) { setError(err instanceof Error ? err.message : t('feesUi.errGenerate')); setPaymentState('error'); } }; const handleAutoPay = async () => { if (!invoice) return; setPaymentState('paying'); try { if (activeNWC?.isConnected) { await sendPayment(activeNWC, invoice); } else if (webln) { if (webln.enable) await webln.enable(); await webln.sendPayment(invoice); } else { setPaymentState('waiting'); return; } recordPayment(); setPaymentState('success'); toast({ title: t('feesUi.paymentReceived'), description: t('feesUi.paymentReceivedDesc', { amount: amountSats.toLocaleString(), label: metaLabel }) }); setTimeout(onSuccess, 1200); } catch (err) { setPaymentState('waiting'); if ((err as { code?: string })?.code === PAYMENT_STATUS_UNKNOWN) { toast({ title: t('feesUi.autoPayFailed'), description: t('feesUi.payStatusUnknown'), variant: 'destructive' }); return; } toast({ title: t('feesUi.autoPayFailed'), description: t('feesUi.autoPayFailedDesc'), variant: 'destructive' }); } }; const copyInvoice = async () => { if (!invoice) return; await navigator.clipboard.writeText(invoice); toast({ title: t('feesUi.invoiceCopied') }); }; // ── Success ── if (paymentState === 'success') { return (

{meta.emoji} {t('feesUi.activeBang', { label: metaLabel })}

{metaBenefit}

); } // ── Main payment UI ── return (
{/* Header */}

{meta.emoji} {metaLabel}

{metaDesc}

{/* Duration tier selector — only before payment has started */} {paymentState === 'idle' && (

{t('feesUi.chooseDuration')}

{tiers.map((tierOption) => { const price = feeTierPrice(tierOption); const perDay = feeTierPerDay(tierOption); const selected = tierOption.id === tier.id; return ( ); })}
)} {/* What you get */}
{metaBenefit}
{t('feesUi.validFor')} {tierLabel}
{context && (

{context}

)} {/* Amount */}

⚡ {amountSats.toLocaleString()} sats

{t('feesUi.amountLine', { usd: (amountSats * 0.0006).toFixed(2), name: recipientName, tier: tierLabel })}

{/* Actions */} {paymentState === 'idle' && ( )} {(paymentState === 'generating' || paymentState === 'paying') && (

{paymentState === 'generating' ? t('feesUi.generating') : t('feesUi.processing')}

)} {paymentState === 'waiting' && invoice && qrCodeUrl && (
{t('feesUi.invoiceQrAlt')}
{(webln || activeNWC) && ( )}

{t('feesUi.scanAny')}

)} {paymentState === 'error' && (

{error}

)}
); }