}
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 (formValues, ...submitArgs) => {
let { cost, amount } = formValues
cost ??= amount
// 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))
}