import { useState, useCallback, useEffect } from 'react' import { useMutation, useQuery } from '@apollo/client' import { Button } from 'react-bootstrap' import { gql } from 'graphql-tag' import { numWithUnits } from '../lib/format' import AccordianItem from './accordian-item' import Qr, { QrSkeleton } from './qr' import { INVOICE } from '../fragments/wallet' import InvoiceStatus from './invoice-status' import { useMe } from './me' import { useShowModal } from './modal' import { sleep } from '../lib/time' import Countdown from './countdown' import PayerData from './payer-data' import Bolt11Info from './bolt11-info' export function Invoice ({ invoice, modal, onPayment, info, successVerb }) { const [expired, setExpired] = useState(new Date(invoice.expiredAt) <= new Date()) let variant = 'default' let status = 'waiting for you' let webLn = true if (invoice.confirmedAt || (invoice.isHeld && invoice.satsReceived && !expired)) { variant = 'confirmed' status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} ${successVerb || 'deposited'}` webLn = false } else if (expired) { variant = 'failed' status = 'expired' webLn = false } else if (invoice.cancelled) { variant = 'failed' status = 'cancelled' webLn = false } useEffect(() => { if (invoice.confirmedAt || (invoice.isHeld && invoice.satsReceived)) { onPayment?.(invoice) } }, [invoice.confirmedAt, invoice.isHeld, invoice.satsReceived]) const { nostr, comment, lud18Data, bolt11, confirmedPreimage } = invoice return ( <> {!invoice.confirmedAt &&
{ setExpired(true) }} />
} {!modal && <> {info &&
{info}
}
{nostr ? {JSON.stringify(nostr, null, 2)} } /> : null}
{lud18Data &&
} />
} {comment &&
{comment}} />
} } ) } const MutationInvoice = ({ id, hash, hmac, errorCount, repeat, onClose, expiresAt, ...props }) => { const { data, loading, error } = useQuery(INVOICE, { pollInterval: 1000, variables: { id } }) const [cancelInvoice] = useMutation(gql` mutation cancelInvoice($hash: String!, $hmac: String!) { cancelInvoice(hash: $hash, hmac: $hmac) { id } } `) if (error) { if (error.message?.includes('invoice not found')) { return } return
error
} if (!data || loading) { return } let errorStatus = 'Something went wrong trying to perform the action after payment.' if (errorCount > 1) { errorStatus = 'Something still went wrong.\nYou can retry or cancel the invoice to return your funds.' } return ( <> {errorCount > 0 ? ( <>
) : null} ) } const defaultOptions = { forceInvoice: false, requireSession: false, callback: null, // (formValues) => void replaceModal: false } // TODO: refactor this so it can be easily understood // there's lots of state cascading paired with logic // independent of the state, and it's hard to follow export const useInvoiceable = (onSubmit, options = defaultOptions) => { const me = useMe() const [createInvoice, { data }] = useMutation(gql` mutation createInvoice($amount: Int!) { createInvoice(amount: $amount, hodlInvoice: true, expireSecs: 180) { id hash hmac expiresAt } }`) const showModal = useShowModal() const [formValues, setFormValues] = useState() const [submitArgs, setSubmitArgs] = useState() let errorCount = 0 const onPayment = useCallback( (onClose, hmac) => { return async ({ id, satsReceived, expiresAt, hash }) => { await sleep(500) const repeat = () => { onClose() // call onSubmit handler and pass invoice data onSubmit({ satsReceived, hash, hmac, ...formValues }, ...submitArgs) .then(() => { options?.callback?.(formValues) }) .catch((error) => { // if error happened after payment, show repeat and cancel options // by passing `errorCount` and `repeat` console.error(error) errorCount++ showModal(onClose => ( ), { keepOpen: true }) }) } // prevents infinite loop of calling `onPayment` if (errorCount === 0) await repeat() } }, [onSubmit, submitArgs] ) const invoice = data?.createInvoice useEffect(() => { if (invoice) { showModal(onClose => ( ), { replaceModal: options.replaceModal, keepOpen: true } ) } }, [invoice?.id]) // this function will be called before the Form's onSubmit handler is called // and the form must include `cost` or `amount` as a value const onSubmitWrapper = useCallback(async ({ cost, ...formValues }, ...submitArgs) => { // action only allowed if logged in if (!me && options.requireSession) { throw new Error('you must be logged in') } // if no cost is passed, just try the action first if (!cost || (me && !options.forceInvoice)) { try { return await onSubmit(formValues, ...submitArgs) } catch (error) { if (!payOrLoginError(error)) { throw error } } } setFormValues(formValues) setSubmitArgs(submitArgs) await createInvoice({ variables: { amount: cost } }) // tell onSubmit handler that we want to keep local storage // even though the submit handler was "successful" return { keepLocalStorage: true } }, [onSubmit, setFormValues, setSubmitArgs, createInvoice, !!me]) return onSubmitWrapper } export const useInvoiceModal = (onPayment, deps) => { const onPaymentMemo = useCallback(onPayment, deps) return useInvoiceable(onPaymentMemo, { replaceModal: true }) } export const payOrLoginError = (error) => { const matches = ['insufficient funds', 'you must be logged in or pay'] if (Array.isArray(error)) { return error.some(({ message }) => matches.some(m => message.includes(m))) } return matches.some(m => error.toString().includes(m)) }