import { useState, useEffect } from 'react' import { numWithUnits } from '@/lib/format' import AccordianItem from './accordian-item' import Qr, { QrSkeleton } from './qr' import { CompactLongCountdown } from './countdown' import PayerData from './payer-data' import Bolt11Info from './bolt11-info' import { useQuery } from '@apollo/client' import { INVOICE } from '@/fragments/wallet' import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants' import { NoAttachedWalletError } from './payment' import ItemJob from './item-job' import Item from './item' import { CommentFlat } from './comment' import classNames from 'classnames' export default function Invoice ({ id, query = INVOICE, modal, onPayment, onCanceled, info, successVerb, useWallet = true, walletError, poll, waitFor, ...props }) { const [expired, setExpired] = useState(false) const { data, error } = useQuery(query, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, variables: { id }, nextFetchPolicy: 'cache-and-network', skip: !poll }) const invoice = data?.invoice useEffect(() => { if (!invoice) { return } if (waitFor?.(invoice)) { onPayment?.(invoice) } if (invoice.cancelled || invoice.actionError) { onCanceled?.(invoice) } setExpired(new Date(invoice.expiredAt) <= new Date()) }, [invoice, onPayment, setExpired]) if (error) { return <div>{error.message}</div> } if (!invoice) { return <QrSkeleton {...props} /> } let variant = 'default' let status = 'waiting for you' if (invoice.cancelled) { variant = 'failed' status = 'cancelled' useWallet = false } else if (invoice.confirmedAt || (invoice.isHeld && invoice.satsReceived && !expired)) { variant = 'confirmed' status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} ${successVerb || 'deposited'}` useWallet = false } else if (expired) { variant = 'failed' status = 'expired' useWallet = false } else if (invoice.expiresAt) { variant = 'pending' status = ( <CompactLongCountdown date={invoice.expiresAt} onComplete={() => { setExpired(true) }} /> ) } const { nostr, comment, lud18Data, bolt11, confirmedPreimage } = invoice return ( <> {walletError && !(walletError instanceof NoAttachedWalletError) && <div className='text-center fw-bold text-info mb-3' style={{ lineHeight: 1.25 }}> Paying from attached wallet failed: <code> {walletError.message}</code> </div>} <Qr useWallet={useWallet} value={invoice.bolt11} description={numWithUnits(invoice.satsRequested, { abbreviate: false })} statusVariant={variant} status={status} /> {!modal && <> {info && <div className='text-muted fst-italic text-center'>{info}</div>} <div className='w-100'> {nostr ? <AccordianItem header='Nostr Zap Request' body={ <pre> <code> {JSON.stringify(nostr, null, 2)} </code> </pre> } /> : null} </div> {lud18Data && <div className='w-100'> <AccordianItem header='sender information' body={<PayerData data={lud18Data} className='text-muted ms-3 mb-3' />} /> </div>} {comment && <div className='w-100'> <AccordianItem header='sender comments' body={<span className='text-muted ms-3'>{comment}</span>} /> </div>} <Bolt11Info bolt11={bolt11} preimage={confirmedPreimage} /> {invoice?.item && <ActionInfo invoice={invoice} />} </>} </> ) } function ActionInfo ({ invoice }) { if (!invoice.actionType) return null let className = 'text-info' let actionString = '' switch (invoice.actionState) { case 'FAILED': case 'RETRYING': actionString += 'attempted ' className = 'text-warning' break case 'PAID': actionString += 'successful ' className = 'text-success' break default: actionString += 'pending ' } switch (invoice.actionType) { case 'ITEM_CREATE': actionString += 'item creation' break case 'ZAP': actionString += 'zap on item' break case 'DOWN_ZAP': actionString += 'downzap on item' break case 'POLL_VOTE': actionString += 'poll vote' break } return ( <div className='text-start w-100 my-3'> <div className={classNames('fw-bold', 'pb-1', className)}>{actionString}</div> {(invoice.item?.isJob && <ItemJob item={invoice?.item} />) || (invoice.item?.title && <Item item={invoice?.item} />) || <CommentFlat item={invoice.item} includeParent noReply truncate />} </div> ) }