import { AlertTriangleIcon, ArrowLeftIcon, CopyIcon, LinkIcon, PlusIcon, ReceiptTextIcon, } from "lucide-react"; import React from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; import { CurrencyInputField } from "src/components/CurrencyInputField"; import ExternalLink from "src/components/ExternalLink"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import LottieSuccess from "src/components/LottieSuccess"; import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert"; import QRCode from "src/components/QRCode"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "src/components/ui/accordion"; import { Alert, AlertDescription } from "src/components/ui/alert"; import { Button } from "src/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "src/components/ui/card"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; import { LinkButton } from "src/components/ui/custom/link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useBalances } from "src/hooks/useBalances"; import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; import { useTransaction } from "src/hooks/useTransaction"; import { copyToClipboard } from "src/lib/clipboard"; import { cn } from "src/lib/utils"; import ReceiveToSelect from "src/screens/wallet/receive/ReceiveToSelect"; import { CreateInvoiceRequest, Transaction } from "src/types"; import { request } from "src/utils/request"; export default function ReceiveInvoice() { const { data: info, hasChannelManagement } = useInfo(); const { data: me } = useAlbyMe(); const { data: balances } = useBalances(); const { data: channels } = useChannels(); const [isLoading, setLoading] = React.useState(false); const [jitChannelRequestFailed, setJitChannelRequestFailed] = React.useState(false); const [amountSat, setAmountSat] = React.useState(""); const [description, setDescription] = React.useState(""); const [toAppId, setToAppId] = React.useState(); const [transaction, setTransaction] = React.useState( null ); const { data: invoiceData } = useTransaction( transaction ? transaction.paymentHash : "", true ); const paymentDone = !!invoiceData?.settledAt; const jitChannelsEnabled = !!info?.jitChannelsEnabled; const configuredLsps2Source = info?.jitChannelsLiquiditySource; const lsps2Source = jitChannelsEnabled ? configuredLsps2Source : undefined; const lsps2MinimumPaymentSizeSat = React.useMemo(() => { if (jitChannelsEnabled && info?.jitChannelsMinPaymentSizeMsat) { return Math.ceil(info.jitChannelsMinPaymentSizeMsat / 1000); } return undefined; }, [info?.jitChannelsMinPaymentSizeMsat, jitChannelsEnabled]); // only enforce the minimum on the input when the user has no channels yet - // their first channel must meet the minimum size. const jitMinimumReceiveSat = channels?.length ? undefined : lsps2MinimumPaymentSizeSat; const lsps2MaximumPaymentSizeSat = React.useMemo(() => { if (jitChannelsEnabled && info?.jitChannelsMaxPaymentSizeMsat) { return Math.floor(info.jitChannelsMaxPaymentSizeMsat / 1000); } return undefined; }, [info?.jitChannelsMaxPaymentSizeMsat, jitChannelsEnabled]); const jitMaximumReceiveSat = hasChannelManagement && lsps2Source ? lsps2MaximumPaymentSizeSat : !lsps2Source && hasChannelManagement ? balances?.lightning.totalReceivableSat : undefined; const totalReceivableMsat = balances?.lightning.totalReceivableMsat ?? 0; const requestedAmountMsat = +amountSat * 1000 || transaction?.amountMsat || 0; const isNearReceivingCapacity = !!hasChannelManagement && requestedAmountMsat >= 0.8 * totalReceivableMsat; const isJitReceiveInvoice = !!hasChannelManagement && !!lsps2Source && !!transaction && transaction.amountMsat > totalReceivableMsat; const displayedJitFeeMsat = paymentDone ? (invoiceData?.feesPaidMsat ?? 0) : (transaction?.feesPaidMsat ?? 0); if (!balances || !info || (info.albyAccountConnected && !me)) { return ; } const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); try { setLoading(true); setJitChannelRequestFailed(false); const invoice = await request("/api/invoices", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ amountMsat: (parseInt(amountSat) || 0) * 1000, description, toAppId, } as CreateInvoiceRequest), }); if (invoice) { setTransaction(invoice); setAmountSat(""); setDescription(""); toast("Successfully created invoice"); } } catch (e) { const requestedAmountSat = parseInt(amountSat) || 0; // the user already has channels but this amount exceeds their receiving // capacity (so a new channel is needed) and is below the LSP's minimum // channel size - the receive may have failed because the amount was too // small to open a second channel, so add a hint alongside the error. const likelyTooSmallForNewChannel = jitChannelsEnabled && !!channels?.length && !!lsps2MinimumPaymentSizeSat && requestedAmountSat < lsps2MinimumPaymentSizeSat && requestedAmountSat * 1000 > totalReceivableMsat; let description = "" + e; if (likelyTooSmallForNewChannel) { description += `\n\nThis amount is over your receiving capacity and may be too small to open a new Lightning channel. Try receiving at least ${new Intl.NumberFormat().format( lsps2MinimumPaymentSizeSat as number )} sats, or lower the amount to fit your current capacity.`; } toast.error("Failed to create invoice", { description, }); if (lsps2Source) { setJitChannelRequestFailed(true); } console.error(e); } finally { setLoading(false); } }; const copy = () => { copyToClipboard(transaction?.invoice as string); }; const newChannelFeeAlert = (

Includes a {" "} channel fee.{" "} Learn more

); return (
{!lsps2Source && !transaction && isNearReceivingCapacity && ( )}
{transaction ? ( {!paymentDone ? ( <>

Waiting for Payment...

{isJitReceiveInvoice && displayedJitFeeMsat >= 1000 && (
{newChannelFeeAlert}
)}
) : ( <> Payment Received

Back to Wallet )}
) : (
{ if ( jitMinimumReceiveSat && e.currentTarget.validity.rangeUnderflow ) { e.currentTarget.setCustomValidity( `You need to receive at least ${new Intl.NumberFormat().format( jitMinimumReceiveSat )} sats to open your first lightning channel` ); } else if ( jitMaximumReceiveSat && e.currentTarget.validity.rangeOverflow ) { e.currentTarget.setCustomValidity( lsps2Source ? `This JIT channel setup supports receiving at most ${new Intl.NumberFormat().format( jitMaximumReceiveSat )} sats in a single payment` : `You can receive at most ${new Intl.NumberFormat().format( jitMaximumReceiveSat )} sats with your current capacity` ); } else { e.currentTarget.setCustomValidity(""); } }} maxSat={jitMaximumReceiveSat} autoFocus contextRows={ hasChannelManagement && !lsps2Source && jitMaximumReceiveSat ? [ { label: "Receive limit", amountSat: jitMaximumReceiveSat, }, ] : undefined } />
{ setDescription(e.target.value); }} />
Create Invoice {(!info?.albyAccountConnected || !me?.lightning_address) && ( View other ways to receive {!info?.albyAccountConnected && info.supportsBolt12 && ( Lightning Offer )} Receive from On-chain / Other Cryptocurrency )} )}
{!transaction && jitChannelRequestFailed && ( Failed to request a just-in-time channel invoice.{" "} Manually open a channel. )}
{!transaction && (!info?.albyAccountConnected || !me?.lightning_address) && ( )}
); } function LightningAddressCard() { return ( Get Your Free Lightning Address

Create free Alby Account and link it with your Alby Hub to get a convenient @getalby.com{" "} lightning address and other perks:

  • • Lightning address & Nostr identifier,
  • • Personal tipping page,
  • • Access to podcasting 2.0 apps,
  • • Buy bitcoin directly to your wallet,
  • • Useful email Alby Hub notifications.
Get Alby Account
); }