notify user when invoice is paid

This commit is contained in:
keyan 2022-03-23 13:54:39 -05:00
parent 69155139e6
commit 987a5ed3a3
7 changed files with 104 additions and 53 deletions

View File

@ -1,6 +1,7 @@
import { AuthenticationError } from 'apollo-server-micro' import { AuthenticationError } from 'apollo-server-micro'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { getItem } from './item' import { getItem } from './item'
import { getInvoice } from './wallet'
export default { export default {
Query: { Query: {
@ -70,6 +71,7 @@ export default {
// at most 25 ancesestors belonging to the same user for a given reply, see: (LIMIT+OFFSET)*25 which is // at most 25 ancesestors belonging to the same user for a given reply, see: (LIMIT+OFFSET)*25 which is
// undoubtably the case today ... this probably won't hold indefinitely though // undoubtably the case today ... this probably won't hold indefinitely though
// One other less HACKy way to do this is to store in each reply, an set of users it's in response to // One other less HACKy way to do this is to store in each reply, an set of users it's in response to
// or to simply denormalize the replies (simply the ids which we wouldn't have to be concerned about consitency)
const notifications = await models.$queryRaw(` const notifications = await models.$queryRaw(`
SELECT DISTINCT * SELECT DISTINCT *
FROM FROM
@ -128,8 +130,17 @@ export default {
(SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats", (SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats",
'Earn' AS type 'Earn' AS type
FROM "Earn" FROM "Earn"
WHERE "Earn"."userId" = $1 AND WHERE "Earn"."userId" = $1
FLOOR(msats / 1000) > 0 AND FLOOR(msats / 1000) > 0
AND created_at <= $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)
UNION ALL
(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
'InvoicePaid' AS type
FROM "Invoice"
WHERE "Invoice"."userId" = $1
AND "confirmedAt" IS NOT NULL
AND created_at <= $2 AND created_at <= $2
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)) AS n LIMIT ${LIMIT}+$3)) AS n
@ -165,6 +176,9 @@ export default {
mention: async (n, args, { models }) => true, mention: async (n, args, { models }) => true,
item: async (n, args, { models }) => getItem(n, { id: n.id }, { models }) item: async (n, args, { models }) => getItem(n, { id: n.id }, { models })
}, },
InvoicePaid: {
invoice: async (n, args, { me, models }) => getInvoice(n, { id: n.id }, { me, models })
},
Invitification: { Invitification: {
invite: async (n, args, { models }) => { invite: async (n, args, { models }) => {
return await models.invite.findUnique({ return await models.invite.findUnique({

View File

@ -260,6 +260,18 @@ export default {
return true return true
} }
const invoice = await models.invoice.findFirst({
where: {
userId: user.id,
confirmedAt: {
gt: user.checkedNotesAt || new Date(0)
}
}
})
if (invoice) {
return true
}
// check if new invites have been redeemed // check if new invites have been redeemed
const newInvitees = await models.$queryRaw(` const newInvitees = await models.$queryRaw(`
SELECT "Invite".id SELECT "Invite".id

View File

@ -5,28 +5,30 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import lnpr from 'bolt11' import lnpr from 'bolt11'
import { SELECT } from './item' import { SELECT } from './item'
export async function getInvoice (parent, { id }, { me, models }) {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
const inv = await models.invoice.findUnique({
where: {
id: Number(id)
},
include: {
user: true
}
})
if (inv.user.id !== me.id) {
throw new AuthenticationError('not ur invoice')
}
return inv
}
export default { export default {
Query: { Query: {
invoice: async (parent, { id }, { me, models, lnd }) => { invoice: getInvoice,
if (!me) {
throw new AuthenticationError('you must be logged in')
}
const inv = await models.invoice.findUnique({
where: {
id: Number(id)
},
include: {
user: true
}
})
if (inv.user.id !== me.id) {
throw new AuthenticationError('not ur invoice')
}
return inv
},
withdrawl: async (parent, { id }, { me, models, lnd }) => { withdrawl: async (parent, { id }, { me, models, lnd }) => {
if (!me) { if (!me) {
throw new AuthenticationError('you must be logged in') throw new AuthenticationError('you must be logged in')

View File

@ -37,8 +37,14 @@ export default gql`
sortTime: String! sortTime: String!
} }
type InvoicePaid {
earnedSats: Int!
invoice: Invoice!
sortTime: String!
}
union Notification = Reply | Votification | Mention union Notification = Reply | Votification | Mention
| Invitification | JobChanged | Earn | Invitification | JobChanged | Earn | InvoicePaid
type Notifications { type Notifications {
lastChecked: String lastChecked: String

View File

@ -7,7 +7,9 @@ import MoreFooter from './more-footer'
import Invite from './invite' import Invite from './invite'
import { ignoreClick } from '../lib/clicks' import { ignoreClick } from '../lib/clicks'
import Link from 'next/link' import Link from 'next/link'
import Check from '../svgs/check-double-line.svg'
// TODO: oh man, this is a mess ... each notification type should just be a component ...
function Notification ({ n }) { function Notification ({ n }) {
const router = useRouter() const router = useRouter()
return ( return (
@ -22,7 +24,9 @@ function Notification ({ n }) {
return return
} }
if (n.__typename === 'Invitification') { if (n.__typename === 'InvoicePaid') {
router.push(`/invoices/${n.invoice.id}`)
} else if (n.__typename === 'Invitification') {
router.push('/invites') router.push('/invites')
} else if (!n.item.title) { } else if (!n.item.title) {
router.push({ router.push({
@ -64,33 +68,35 @@ function Notification ({ n }) {
</div> </div>
</> </>
) )
: ( : n.__typename === 'InvoicePaid'
<> ? <div className='font-weight-bold text-info ml-2 py-1'><Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account</div>
{n.__typename === 'Votification' && : (
<small className='font-weight-bold text-success ml-2'> <>
your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats {n.__typename === 'Votification' &&
</small>} <small className='font-weight-bold text-success ml-2'>
{n.__typename === 'Mention' && your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
<small className='font-weight-bold text-info ml-2'> </small>}
you were mentioned in {n.__typename === 'Mention' &&
</small>} <small className='font-weight-bold text-info ml-2'>
{n.__typename === 'JobChanged' && you were mentioned in
<small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}> </small>}
{n.item.status === 'NOSATS' {n.__typename === 'JobChanged' &&
? 'your job ran out of sats' <small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}>
: 'your job is active again'} {n.item.status === 'NOSATS'
</small>} ? 'your job ran out of sats'
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}> : 'your job is active again'}
{n.item.maxBid </small>}
? <ItemJob item={n.item} /> <div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
: n.item.title {n.item.maxBid
? <Item item={n.item} /> ? <ItemJob item={n.item} />
: ( : n.item.title
<div className='pb-2'> ? <Item item={n.item} />
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext /> : (
</div>)} <div className='pb-2'>
</div> <Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
</>)} </div>)}
</div>
</>)}
</div> </div>
) )
} }

View File

@ -51,6 +51,13 @@ export const NOTIFICATIONS = gql`
sortTime sortTime
earnedSats earnedSats
} }
... on InvoicePaid {
sortTime
earnedSats
invoice {
id
}
}
} }
} }
} ` } `

View File

@ -380,11 +380,15 @@ textarea.form-control {
} }
.fill-success { .fill-success {
fill: #5c8001; fill: var(--success);
}
.fill-info {
fill: var(--info);
} }
.fill-danger { .fill-danger {
fill: #c03221; fill: var(--danger);
} }
.fill-theme-color { .fill-theme-color {