Rename to useInvoiceable
This commit is contained in:
parent
38dbbd5a4f
commit
318088179a
|
@ -17,7 +17,6 @@ import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema,
|
|||
import { sendUserNotification } from '../webPush'
|
||||
import { proxyImages } from './imgproxy'
|
||||
import { defaultCommentSort } from '../../lib/item'
|
||||
import { checkInvoice } from '../../lib/anonymous'
|
||||
|
||||
export async function commentFilterClause (me, models) {
|
||||
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
|
||||
|
@ -38,6 +37,25 @@ export async function commentFilterClause (me, models) {
|
|||
return clause
|
||||
}
|
||||
|
||||
async function checkInvoice (models, invoiceHash, fee) {
|
||||
const invoice = await models.invoice.findUnique({
|
||||
where: { hash: invoiceHash },
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
})
|
||||
if (!invoice) {
|
||||
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
if (!invoice.msatsReceived) {
|
||||
throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
if (msatsToSats(invoice.msatsReceived) < fee) {
|
||||
throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
return invoice
|
||||
}
|
||||
|
||||
async function comments (me, models, id, sort) {
|
||||
let orderBy
|
||||
switch (sort) {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { bountySchema } from '../lib/validate'
|
|||
import { SubSelectInitial } from './sub-select-form'
|
||||
import CancelButton from './cancel-button'
|
||||
import { useCallback } from 'react'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
export function BountyForm ({
|
||||
item,
|
||||
|
@ -75,7 +75,7 @@ export function BountyForm ({
|
|||
}
|
||||
}, [upsertBounty, router])
|
||||
|
||||
const anonUpsertBounty = useAnonymous(submitUpsertBounty, { requireSession: true })
|
||||
const invoiceableUpsertBounty = useInvoiceable(submitUpsertBounty, { requireSession: true })
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
@ -90,7 +90,7 @@ export function BountyForm ({
|
|||
onSubmit={
|
||||
handleSubmit ||
|
||||
(async ({ boost, bounty, cost, ...values }) => {
|
||||
await anonUpsertBounty(cost, boost, bounty, values)
|
||||
await invoiceableUpsertBounty(cost, boost, bounty, values)
|
||||
})
|
||||
}
|
||||
storageKeyPrefix={item ? undefined : 'bounty'}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { discussionSchema } from '../lib/validate'
|
|||
import { SubSelectInitial } from './sub-select-form'
|
||||
import CancelButton from './cancel-button'
|
||||
import { useCallback } from 'react'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
export function DiscussionForm ({
|
||||
item, sub, editThreshold, titleLabel = 'title',
|
||||
|
@ -53,7 +53,7 @@ export function DiscussionForm ({
|
|||
}
|
||||
}, [upsertDiscussion, router])
|
||||
|
||||
const anonUpsertDiscussion = useAnonymous(submitUpsertDiscussion)
|
||||
const invoiceableUpsertDiscussion = useInvoiceable(submitUpsertDiscussion)
|
||||
|
||||
const [getRelated, { data: relatedData }] = useLazyQuery(gql`
|
||||
${ITEM_FIELDS}
|
||||
|
@ -79,7 +79,7 @@ export function DiscussionForm ({
|
|||
}}
|
||||
schema={schema}
|
||||
onSubmit={handleSubmit || (async ({ boost, cost, ...values }) => {
|
||||
await anonUpsertDiscussion(cost, boost, values)
|
||||
await invoiceableUpsertDiscussion(cost, boost, values)
|
||||
})}
|
||||
storageKeyPrefix={item ? undefined : 'discussion'}
|
||||
>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Link from 'next/link'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
export default function FundError ({ onClose, amount, onPayment }) {
|
||||
const anonPayment = useAnonymous(onPayment, { forceInvoice: true })
|
||||
const createInvoice = useInvoiceable(onPayment, { forceInvoice: true })
|
||||
return (
|
||||
<>
|
||||
<p className='fw-bolder'>you need more sats</p>
|
||||
|
@ -12,8 +12,15 @@ export default function FundError ({ onClose, amount, onPayment }) {
|
|||
<Button variant='success' onClick={onClose}>fund wallet</Button>
|
||||
</Link>
|
||||
<span className='d-flex mx-3 font-weight-bold text-muted align-items-center'>or</span>
|
||||
<Button variant='success' onClick={() => anonPayment(amount)}>pay invoice</Button>
|
||||
<Button variant='success' onClick={() => createInvoice(amount)}>pay invoice</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const isInsufficientFundsError = (error) => {
|
||||
if (Array.isArray(error)) {
|
||||
return error.some(({ message }) => message.includes('insufficient funds'))
|
||||
}
|
||||
return error.toString().includes('insufficient funds')
|
||||
}
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
import AccordianItem from './accordian-item'
|
||||
import Qr from './qr'
|
||||
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 { CopyInput } from './form'
|
||||
import { INVOICE } from '../fragments/wallet'
|
||||
import InvoiceStatus from './invoice-status'
|
||||
import { useMe } from './me'
|
||||
import { useShowModal } from './modal'
|
||||
import { sleep } from '../lib/time'
|
||||
import FundError, { isInsufficientFundsError } from './fund-error'
|
||||
|
||||
export function Invoice ({ invoice, onConfirmation, successVerb }) {
|
||||
let variant = 'default'
|
||||
|
@ -43,3 +54,176 @@ export function Invoice ({ invoice, onConfirmation, successVerb }) {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Contacts = ({ invoiceHash }) => {
|
||||
const subject = `Support request for payment hash: ${invoiceHash}`
|
||||
const body = 'Hi, I successfully paid for <insert action> but the action did not work.'
|
||||
return (
|
||||
<div className='d-flex flex-column justify-content-center'>
|
||||
<span>Payment hash</span>
|
||||
<div className='w-100'>
|
||||
<CopyInput type='text' placeholder={invoiceHash} readOnly noForm />
|
||||
</div>
|
||||
<div className='d-flex flex-row justify-content-center'>
|
||||
<a
|
||||
href={`mailto:kk@stacker.news?subject=${subject}&body=${body}`} className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
e-mail
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span>
|
||||
<a
|
||||
href='https://tribes.sphinx.chat/t/stackerzchat' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
sphinx
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span>
|
||||
<a
|
||||
href='https://t.me/k00bideh' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
telegram
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span>
|
||||
<a
|
||||
href='https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2F6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE%3D%40smp10.simplex.im%2FebLYaEFGjsD3uK4fpE326c5QI1RZSxau%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAV086Oj5yCsavWzIbRMCVuF6jq793Tt__rWvCec__viI%253D%26srv%3Drb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22cZwSGoQhyOUulzp7rwCdWQ%3D%3D%22%7D' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
simplex
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ActionInvoice = ({ id, hash, errorCount, repeat, ...props }) => {
|
||||
const { data, loading, error } = useQuery(INVOICE, {
|
||||
pollInterval: 1000,
|
||||
variables: { id }
|
||||
})
|
||||
if (error) {
|
||||
if (error.message?.includes('invoice not found')) {
|
||||
return
|
||||
}
|
||||
return <div>error</div>
|
||||
}
|
||||
if (!data || loading) {
|
||||
return <QrSkeleton status='loading' />
|
||||
}
|
||||
|
||||
let errorStatus = 'Something went wrong. Please try again.'
|
||||
if (errorCount > 1) {
|
||||
errorStatus = 'Something still went wrong.\nPlease contact admins for support or to request a refund.'
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Invoice invoice={data.invoice} {...props} />
|
||||
{errorCount > 0
|
||||
? (
|
||||
<>
|
||||
<InvoiceStatus variant='failed' status={errorStatus} />
|
||||
{errorCount === 1
|
||||
? <div className='d-flex flex-row mt-3 justify-content-center'><Button variant='info' onClick={repeat}>Retry</Button></div>
|
||||
: <Contacts invoiceHash={hash} />}
|
||||
</>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
forceInvoice: false,
|
||||
requireSession: false
|
||||
}
|
||||
export const useInvoiceable = (fn, options = defaultOptions) => {
|
||||
const me = useMe()
|
||||
const [createInvoice, { data }] = useMutation(gql`
|
||||
mutation createInvoice($amount: Int!) {
|
||||
createInvoice(amount: $amount) {
|
||||
id
|
||||
hash
|
||||
}
|
||||
}`)
|
||||
const showModal = useShowModal()
|
||||
const [fnArgs, setFnArgs] = useState()
|
||||
|
||||
// fix for bug where `showModal` runs the code for two modals and thus executes `onConfirmation` twice
|
||||
let called = false
|
||||
let errorCount = 0
|
||||
const onConfirmation = useCallback(
|
||||
onClose => {
|
||||
called = false
|
||||
return async ({ id, satsReceived, hash }) => {
|
||||
if (called) return
|
||||
called = true
|
||||
await sleep(2000)
|
||||
const repeat = () =>
|
||||
fn(satsReceived, ...fnArgs, hash)
|
||||
.then(onClose)
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
errorCount++
|
||||
onClose()
|
||||
showModal(onClose => (
|
||||
<ActionInvoice
|
||||
id={id}
|
||||
hash={hash}
|
||||
onConfirmation={onConfirmation(onClose)}
|
||||
successVerb='received'
|
||||
errorCount={errorCount}
|
||||
repeat={repeat}
|
||||
/>
|
||||
), { keepOpen: true })
|
||||
})
|
||||
// prevents infinite loop of calling `onConfirmation`
|
||||
if (errorCount === 0) await repeat()
|
||||
}
|
||||
}, [fn, fnArgs]
|
||||
)
|
||||
|
||||
const invoice = data?.createInvoice
|
||||
useEffect(() => {
|
||||
if (invoice) {
|
||||
showModal(onClose => (
|
||||
<ActionInvoice
|
||||
id={invoice.id}
|
||||
hash={invoice.hash}
|
||||
onConfirmation={onConfirmation(onClose)}
|
||||
successVerb='received'
|
||||
/>
|
||||
), { keepOpen: true }
|
||||
)
|
||||
}
|
||||
}, [invoice?.id])
|
||||
|
||||
const actionFn = useCallback(async (amount, ...args) => {
|
||||
if (!me && options.requireSession) {
|
||||
throw new Error('you must be logged in')
|
||||
}
|
||||
if (me && !options.forceInvoice) {
|
||||
try {
|
||||
return await fn(amount, ...args)
|
||||
} catch (error) {
|
||||
if (isInsufficientFundsError(error)) {
|
||||
showModal(onClose => {
|
||||
return (
|
||||
<FundError
|
||||
onClose={onClose}
|
||||
amount={amount}
|
||||
onPayment={async (_, invoiceHash) => { await fn(amount, ...args, invoiceHash) }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
}
|
||||
setFnArgs(args)
|
||||
return createInvoice({ variables: { amount } })
|
||||
}, [fn, setFnArgs, createInvoice])
|
||||
|
||||
return actionFn
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Form, Input, SubmitButton } from './form'
|
|||
import { useMe } from './me'
|
||||
import UpBolt from '../svgs/bolt.svg'
|
||||
import { amountSchema } from '../lib/validate'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
const defaultTips = [100, 1000, 10000, 100000]
|
||||
|
||||
|
@ -65,7 +65,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
|||
onClose()
|
||||
}, [act, onClose, strike, itemId])
|
||||
|
||||
const anonAct = useAnonymous(submitAct)
|
||||
const invoiceableAct = useInvoiceable(submitAct)
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
@ -75,7 +75,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
|||
}}
|
||||
schema={amountSchema}
|
||||
onSubmit={async ({ amount }) => {
|
||||
await anonAct(amount)
|
||||
await invoiceableAct(amount)
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
|
|
@ -17,7 +17,7 @@ import Avatar from './avatar'
|
|||
import ActionTooltip from './action-tooltip'
|
||||
import { jobSchema } from '../lib/validate'
|
||||
import CancelButton from './cancel-button'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
function satsMin2Mo (minute) {
|
||||
return minute * 30 * 24 * 60
|
||||
|
@ -82,7 +82,7 @@ export default function JobForm ({ item, sub }) {
|
|||
}
|
||||
}, [upsertJob, router])
|
||||
|
||||
const anonUpsertJob = useAnonymous(submitUpsertJob, { requireSession: true })
|
||||
const invoiceableUpsertJob = useInvoiceable(submitUpsertJob, { requireSession: true })
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -102,7 +102,7 @@ export default function JobForm ({ item, sub }) {
|
|||
schema={jobSchema}
|
||||
storageKeyPrefix={storageKeyPrefix}
|
||||
onSubmit={(async ({ maxBid, stop, start, ...values }) => {
|
||||
await anonUpsertJob(1000, maxBid, stop, start, values)
|
||||
await invoiceableUpsertJob(1000, maxBid, stop, start, values)
|
||||
})}
|
||||
>
|
||||
<div className='form-group'>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { linkSchema } from '../lib/validate'
|
|||
import Moon from '../svgs/moon-fill.svg'
|
||||
import { SubSelectInitial } from './sub-select-form'
|
||||
import CancelButton from './cancel-button'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
export function LinkForm ({ item, sub, editThreshold, children }) {
|
||||
const router = useRouter()
|
||||
|
@ -90,7 +90,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||
}
|
||||
}, [upsertLink, router])
|
||||
|
||||
const anonUpsertLink = useAnonymous(submitUpsertLink)
|
||||
const invoiceableUpsertLink = useInvoiceable(submitUpsertLink)
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.pageTitleAndUnshorted?.title) {
|
||||
|
@ -119,7 +119,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||
}}
|
||||
schema={schema}
|
||||
onSubmit={async ({ boost, title, cost, ...values }) => {
|
||||
await anonUpsertLink(cost, boost, title, values)
|
||||
await invoiceableUpsertLink(cost, boost, title, values)
|
||||
}}
|
||||
storageKeyPrefix={item ? undefined : 'link'}
|
||||
>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { pollSchema } from '../lib/validate'
|
|||
import { SubSelectInitial } from './sub-select-form'
|
||||
import CancelButton from './cancel-button'
|
||||
import { useCallback } from 'react'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
export function PollForm ({ item, sub, editThreshold, children }) {
|
||||
const router = useRouter()
|
||||
|
@ -54,7 +54,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
}
|
||||
}, [upsertPoll, router])
|
||||
|
||||
const anonUpsertPoll = useAnonymous(submitUpsertPoll)
|
||||
const invoiceableUpsertPoll = useInvoiceable(submitUpsertPoll)
|
||||
|
||||
const initialOptions = item?.poll?.options.map(i => i.option)
|
||||
|
||||
|
@ -69,7 +69,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
}}
|
||||
schema={schema}
|
||||
onSubmit={async ({ boost, title, options, cost, ...values }) => {
|
||||
await anonUpsertPoll(cost, boost, title, options, values)
|
||||
await invoiceableUpsertPoll(cost, boost, title, options, values)
|
||||
}}
|
||||
storageKeyPrefix={item ? undefined : 'poll'}
|
||||
>
|
||||
|
|
|
@ -9,7 +9,7 @@ import FeeButton from './fee-button'
|
|||
import { commentsViewedAfterComment } from '../lib/new-comments'
|
||||
import { commentSchema } from '../lib/validate'
|
||||
import Info from './info'
|
||||
import { useAnonymous } from '../lib/anonymous'
|
||||
import { useInvoiceable } from './invoice'
|
||||
|
||||
export function ReplyOnAnotherPage ({ parentId }) {
|
||||
return (
|
||||
|
@ -101,7 +101,7 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||
setReply(replyOpen || false)
|
||||
}, [createComment, setReply])
|
||||
|
||||
const anonCreateComment = useAnonymous(submitComment)
|
||||
const invoiceableCreateComment = useInvoiceable(submitComment)
|
||||
|
||||
const replyInput = useRef(null)
|
||||
useEffect(() => {
|
||||
|
@ -130,7 +130,7 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||
}}
|
||||
schema={commentSchema}
|
||||
onSubmit={async ({ cost, ...values }, { resetForm }) => {
|
||||
await anonCreateComment(cost, values, parentId, resetForm)
|
||||
await invoiceableCreateComment(cost, values, parentId, resetForm)
|
||||
}}
|
||||
storageKeyPrefix={'reply-' + parentId}
|
||||
>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import UpBolt from '../svgs/bolt.svg'
|
||||
import styles from './upvote.module.css'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import FundError from './fund-error'
|
||||
import FundError, { isInsufficientFundsError } from './fund-error'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import ItemAct from './item-act'
|
||||
import { useMe } from './me'
|
||||
|
@ -12,7 +12,6 @@ import Overlay from 'react-bootstrap/Overlay'
|
|||
import Popover from 'react-bootstrap/Popover'
|
||||
import { useShowModal } from './modal'
|
||||
import { LightningConsumer, useLightning } from './lightning'
|
||||
import { isInsufficientFundsError } from '../lib/anonymous'
|
||||
import { numWithUnits } from '../lib/format'
|
||||
|
||||
const getColor = (meSats) => {
|
||||
|
|
214
lib/anonymous.js
214
lib/anonymous.js
|
@ -1,214 +0,0 @@
|
|||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { gql } from 'graphql-tag'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useShowModal } from '../components/modal'
|
||||
import { Invoice as QrInvoice } from '../components/invoice'
|
||||
import { QrSkeleton } from '../components/qr'
|
||||
import { useMe } from '../components/me'
|
||||
import { msatsToSats } from './format'
|
||||
import FundError from '../components/fund-error'
|
||||
import { INVOICE } from '../fragments/wallet'
|
||||
import InvoiceStatus from '../components/invoice-status'
|
||||
import { sleep } from './time'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { CopyInput } from '../components/form'
|
||||
|
||||
const Contacts = ({ invoiceHash }) => {
|
||||
const subject = `Support request for payment hash: ${invoiceHash}`
|
||||
const body = 'Hi, I successfully paid for <insert action> but the action did not work.'
|
||||
return (
|
||||
<div className='d-flex flex-column justify-content-center'>
|
||||
<span>Payment hash</span>
|
||||
<div className='w-100'>
|
||||
<CopyInput type='text' placeholder={invoiceHash} readOnly noForm />
|
||||
</div>
|
||||
<div className='d-flex flex-row justify-content-center'>
|
||||
<a
|
||||
href={`mailto:kk@stacker.news?subject=${subject}&body=${body}`} className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
e-mail
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span>
|
||||
<a
|
||||
href='https://tribes.sphinx.chat/t/stackerzchat' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
sphinx
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span>
|
||||
<a
|
||||
href='https://t.me/k00bideh' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
telegram
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span>
|
||||
<a
|
||||
href='https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2F6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE%3D%40smp10.simplex.im%2FebLYaEFGjsD3uK4fpE326c5QI1RZSxau%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAV086Oj5yCsavWzIbRMCVuF6jq793Tt__rWvCec__viI%253D%26srv%3Drb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22cZwSGoQhyOUulzp7rwCdWQ%3D%3D%22%7D' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
simplex
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Invoice = ({ id, hash, errorCount, repeat, ...props }) => {
|
||||
const { data, loading, error } = useQuery(INVOICE, {
|
||||
pollInterval: 1000,
|
||||
variables: { id }
|
||||
})
|
||||
if (error) {
|
||||
if (error.message?.includes('invoice not found')) {
|
||||
return
|
||||
}
|
||||
return <div>error</div>
|
||||
}
|
||||
if (!data || loading) {
|
||||
return <QrSkeleton status='loading' />
|
||||
}
|
||||
|
||||
let errorStatus = 'Something went wrong. Please try again.'
|
||||
if (errorCount > 1) {
|
||||
errorStatus = 'Something still went wrong.\nPlease contact admins for support or to request a refund.'
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<QrInvoice invoice={data.invoice} {...props} />
|
||||
{errorCount > 0
|
||||
? (
|
||||
<>
|
||||
<InvoiceStatus variant='failed' status={errorStatus} />
|
||||
{errorCount === 1
|
||||
? <div className='d-flex flex-row mt-3 justify-content-center'><Button variant='info' onClick={repeat}>Retry</Button></div>
|
||||
: <Contacts invoiceHash={hash} />}
|
||||
</>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const isInsufficientFundsError = (error) => {
|
||||
if (Array.isArray(error)) {
|
||||
return error.some(({ message }) => message.includes('insufficient funds'))
|
||||
}
|
||||
return error.toString().includes('insufficient funds')
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
forceInvoice: false,
|
||||
requireSession: false
|
||||
}
|
||||
export const useAnonymous = (fn, options = defaultOptions) => {
|
||||
const me = useMe()
|
||||
const [createInvoice, { data }] = useMutation(gql`
|
||||
mutation createInvoice($amount: Int!) {
|
||||
createInvoice(amount: $amount) {
|
||||
id
|
||||
hash
|
||||
}
|
||||
}`)
|
||||
const showModal = useShowModal()
|
||||
const [fnArgs, setFnArgs] = useState()
|
||||
|
||||
// fix for bug where `showModal` runs the code for two modals and thus executes `onConfirmation` twice
|
||||
let called = false
|
||||
let errorCount = 0
|
||||
const onConfirmation = useCallback(
|
||||
onClose => {
|
||||
called = false
|
||||
return async ({ id, satsReceived, hash }) => {
|
||||
if (called) return
|
||||
called = true
|
||||
await sleep(2000)
|
||||
const repeat = () =>
|
||||
fn(satsReceived, ...fnArgs, hash)
|
||||
.then(onClose)
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
errorCount++
|
||||
onClose()
|
||||
showModal(onClose => (
|
||||
<Invoice
|
||||
id={id}
|
||||
hash={hash}
|
||||
onConfirmation={onConfirmation(onClose)}
|
||||
successVerb='received'
|
||||
errorCount={errorCount}
|
||||
repeat={repeat}
|
||||
/>
|
||||
), { keepOpen: true })
|
||||
})
|
||||
// prevents infinite loop of calling `onConfirmation`
|
||||
if (errorCount === 0) await repeat()
|
||||
}
|
||||
}, [fn, fnArgs]
|
||||
)
|
||||
|
||||
const invoice = data?.createInvoice
|
||||
useEffect(() => {
|
||||
if (invoice) {
|
||||
showModal(onClose => (
|
||||
<Invoice
|
||||
id={invoice.id}
|
||||
hash={invoice.hash}
|
||||
onConfirmation={onConfirmation(onClose)}
|
||||
successVerb='received'
|
||||
/>
|
||||
), { keepOpen: true }
|
||||
)
|
||||
}
|
||||
}, [invoice?.id])
|
||||
|
||||
const anonFn = useCallback(async (amount, ...args) => {
|
||||
if (!me && options.requireSession) {
|
||||
throw new Error('you must be logged in')
|
||||
}
|
||||
if (me && !options.forceInvoice) {
|
||||
try {
|
||||
return await fn(amount, ...args)
|
||||
} catch (error) {
|
||||
if (isInsufficientFundsError(error)) {
|
||||
showModal(onClose => {
|
||||
return (
|
||||
<FundError
|
||||
onClose={onClose}
|
||||
amount={amount}
|
||||
onPayment={async (_, invoiceHash) => { await fn(amount, ...args, invoiceHash) }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
}
|
||||
setFnArgs(args)
|
||||
return createInvoice({ variables: { amount } })
|
||||
}, [fn, setFnArgs, createInvoice])
|
||||
|
||||
return anonFn
|
||||
}
|
||||
|
||||
export const checkInvoice = async (models, invoiceHash, fee) => {
|
||||
const invoice = await models.invoice.findUnique({
|
||||
where: { hash: invoiceHash },
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
})
|
||||
if (!invoice) {
|
||||
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
if (!invoice.msatsReceived) {
|
||||
throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
if (msatsToSats(invoice.msatsReceived) < fee) {
|
||||
throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
return invoice
|
||||
}
|
Loading…
Reference in New Issue