import GroupedSatsInput from '@/components/GroupedSatsInput' import QrCode from '@/components/QrCode' import SatsAmountEquivalents from '@/components/SatsAmountEquivalents' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { Textarea } from '@/components/ui/textarea' import { LIGHTNING_WALLET_PAY_ENABLED } from '@/constants' import { clampZapSats, formatAmount, formatSatsGrouped, getAmountFromInvoice, LN_INVOICE_COFFEE_PRESET_SATS, LN_INVOICE_PRESET_SATS, MIN_ZAP_SATS, } from '@/lib/lightning' import { buildPaytoUri, formatPaytoTagValue } from '@/lib/payto' import { superchatLightningAccentClass } from '@/lib/superchat-ui' import { cn } from '@/lib/utils' import { useZap } from '@/providers/ZapProvider' import lightning from '@/services/lightning.service' import { Check, Copy, Wallet, Zap } from 'lucide-react' import { closeModal } from '@getalby/bitcoin-connect-react' import { releaseBodyScrollLocks } from '@/lib/react-remove-scroll-body-cleanup' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' function normalizeInvoiceZapSats(amount: number): number { const clamped = clampZapSats(amount) return clamped > 0 && clamped < MIN_ZAP_SATS ? MIN_ZAP_SATS : clamped } function invoiceQrPayload(pr: string): string { const trimmed = pr.trim() if (trimmed.toLowerCase().startsWith('lightning:')) return trimmed return `lightning:${trimmed}` } export default function LightningInvoiceSection({ lightningAddress, paytoUri, onBolt11InvoiceChange, onPaymentFlowComplete }: { lightningAddress: string paytoUri: string /** Fired when a BOLT11 invoice is created or cleared (for Phoenix / external wallet links). */ onBolt11InvoiceChange?: (invoice: string | null) => void /** After a wallet payment succeeds (dialog stays open for the user to choose next steps). */ onPaymentFlowComplete?: (details?: { amountMsat: number payto: string messageDraft?: string }) => void }) { const { t } = useTranslation() const { defaultZapSats, isWalletConnected } = useZap() const [sats, setSats] = useState(() => normalizeInvoiceZapSats(defaultZapSats)) const [description, setDescription] = useState('') const [commentMax, setCommentMax] = useState(null) const [lnurlMetadataState, setLnurlMetadataState] = useState<'loading' | 'ready' | 'error'>( 'loading' ) const [invoice, setInvoice] = useState(null) const [invoiceDescription, setInvoiceDescription] = useState(null) const [creating, setCreating] = useState(false) const [paying, setPaying] = useState(false) const mountedRef = useRef(true) useEffect(() => { mountedRef.current = true return () => { mountedRef.current = false closeModal() releaseBodyScrollLocks() } }, []) useEffect(() => { setSats(normalizeInvoiceZapSats(defaultZapSats)) setDescription('') setInvoice(null) setInvoiceDescription(null) setCommentMax(null) setLnurlMetadataState('loading') let cancelled = false void lightning.getLnurlPayInvoiceOptions(lightningAddress).then((opts) => { if (!cancelled && mountedRef.current) { if (opts) { setCommentMax(opts.commentAllowed) setLnurlMetadataState('ready') } else { setCommentMax(0) setLnurlMetadataState('error') } } }) return () => { cancelled = true } }, [lightningAddress, defaultZapSats]) useEffect(() => { setInvoice(null) setInvoiceDescription(null) }, [sats, description]) useEffect(() => { onBolt11InvoiceChange?.(invoice) }, [invoice, onBolt11InvoiceChange]) const invoiceSats = useMemo(() => { if (!invoice) return null try { return getAmountFromInvoice(invoice) } catch { return null } }, [invoice]) const invoiceQrValue = useMemo( () => (invoice ? invoiceQrPayload(invoice) : ''), [invoice] ) const handleCreateInvoice = async () => { try { setCreating(true) const trimmedDesc = description.trim() const pr = await lightning.createLnurlInvoice(lightningAddress, sats, { description: trimmedDesc || undefined }) if (!mountedRef.current) return setInvoice(pr) setInvoiceDescription(trimmedDesc || null) } catch (error) { if (mountedRef.current) { toast.error(`${t('Failed to create invoice')}: ${(error as Error).message}`) } } finally { if (mountedRef.current) setCreating(false) } } const buildPaymentDetails = (pr: string) => { let amountMsat = clampZapSats(sats) * 1000 try { amountMsat = getAmountFromInvoice(pr) * 1000 } catch { /* use form amount */ } return { amountMsat, payto: formatPaytoTagValue(buildPaytoUri('lightning', lightningAddress)), messageDraft: invoiceDescription?.trim() || description.trim() || undefined } } const handlePay = async () => { if (!invoice) return try { setPaying(true) const result = await lightning.payInvoice(invoice, undefined) if (!mountedRef.current) return if (result) { toast.success(t('Payment sent')) onPaymentFlowComplete?.(buildPaymentDetails(invoice)) setInvoice(null) setInvoiceDescription(null) } } catch (error) { if (mountedRef.current) { toast.error(`${t('Lightning payment failed')}: ${(error as Error).message}`) } } finally { if (mountedRef.current) setPaying(false) } } const copyText = (text: string, message?: string) => { navigator.clipboard.writeText(text) toast.success(message ?? t('Copied to clipboard')) } return (

{t('Pay to')}

{lightningAddress}

{t( 'Plain Lightning payments do not create NIP-57 zap receipts on Nostr. Use the zap button when available.' )}

{t('sats')}
{LN_INVOICE_PRESET_SATS.map((preset) => { const active = sats === preset const isCoffeePreset = preset === LN_INVOICE_COFFEE_PRESET_SATS return ( ) })}
{lnurlMetadataState === 'loading' ? ( ) : lnurlMetadataState === 'error' ? (

{t( 'Could not read this Lightning address (network or browser block). Descriptions need LNURL-pay support on the recipient side.' )}

) : (commentMax ?? 0) > 0 ? (
{description.length}/{commentMax ?? 0}