import { 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 { WalletConfigurationError, WalletPaymentAggregateError } from '@/wallets/errors' import ItemJob from './item-job' import Item from './item' import { CommentFlat } from './comment' import classNames from 'classnames' import Moon from '@/svgs/moon-fill.svg' import { Badge } from 'react-bootstrap' import styles from './invoice.module.css' export default function Invoice ({ id, query = INVOICE, modal, onPayment, onExpired, onCanceled, info, successVerb = 'deposited', heldVerb = 'settling', walletError, poll, waitFor, ...props }) { const { data, error } = useQuery(query, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, variables: { id }, nextFetchPolicy: 'cache-and-network', skip: !poll }) const invoice = data?.invoice const expired = invoice?.cancelledAt && new Date(invoice.expiresAt) < new Date(invoice.cancelledAt) useEffect(() => { if (!invoice) { return } if (waitFor?.(invoice)) { onPayment?.(invoice) } if (expired) { onExpired?.(invoice) } else if (invoice.cancelled || invoice.actionError) { onCanceled?.(invoice) } }, [invoice, expired, onExpired, onCanceled, onPayment]) if (error) { return <div>{error.message}</div> } if (!invoice) { return <QrSkeleton {...props} /> } let variant = 'default' let status = 'waiting for you' let sats = invoice.satsRequested if (invoice.forwardedSats) { if (invoice.actionType === 'RECEIVE') { successVerb = 'forwarded' sats = invoice.forwardedSats } else { successVerb = 'zapped' } } if (invoice.confirmedAt) { variant = 'confirmed' status = ( <> {numWithUnits(sats, { abbreviate: false })} {' '} {successVerb} {' '} {invoice.forwardedSats && <Badge className={styles.badge} bg={null}>p2p</Badge>} </> ) } else if (expired) { variant = 'failed' status = 'expired' } else if (invoice.cancelled) { variant = 'failed' status = 'cancelled' } else if (invoice.isHeld) { variant = 'pending' status = ( <div className='d-flex justify-content-center'> <Moon className='spin fill-grey me-2' /> {heldVerb} </div> ) } else { variant = 'pending' status = ( <CompactLongCountdown date={invoice.expiresAt} /> ) } const { bolt11, confirmedPreimage } = invoice return ( <> <WalletError error={walletError} /> <Qr 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>} <InvoiceExtras {...invoice} /> <Bolt11Info bolt11={bolt11} preimage={confirmedPreimage} /> {invoice?.item && <ActionInfo invoice={invoice} />} </>} </> ) } export function InvoiceExtras ({ nostr, lud18Data, comment }) { return ( <> <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' />} className='mb-3' /> </div>} {comment && <div className='w-100'> <AccordianItem header='sender comments' body={<span className='text-muted ms-3'>{comment}</span>} className='mb-3' /> </div>} </> ) } 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> ) } function WalletError ({ error }) { if (!error || error instanceof WalletConfigurationError) return null if (!(error instanceof WalletPaymentAggregateError)) { console.error('unexpected wallet error:', error) return null } return ( <div className='text-center fw-bold text-info mb-3' style={{ lineHeight: 1.25 }}> <div className='text-info mb-2'>Paying from attached wallets failed:</div> {error.errors.map((e, i) => ( <div key={i}> <code>{e.wallet}: {e.reason || e.message}</code> </div> ))} </div> ) }