finish up invites
This commit is contained in:
parent
3a52f8967a
commit
955d1aa1b2
|
@ -10,6 +10,16 @@ export default {
|
|||
return await models.invite.findMany({
|
||||
where: {
|
||||
userId: me.id
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
},
|
||||
invite: async (parent, { id }, { me, models }) => {
|
||||
return await models.invite.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -44,6 +54,13 @@ export default {
|
|||
Invite: {
|
||||
invitees: async (invite, args, { me, models }) => {
|
||||
return await models.user.findMany({ where: { inviteId: invite.id } })
|
||||
},
|
||||
user: async (invite, args, { me, models }) => {
|
||||
return await models.user.findUnique({ where: { id: invite.userId } })
|
||||
},
|
||||
poor: async (invite, args, { me, models }) => {
|
||||
const user = await models.user.findUnique({ where: { id: invite.userId } })
|
||||
return user.msats < invite.gift * 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,12 @@ async function serialize (models, call) {
|
|||
if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) {
|
||||
bail(new Error('withdrawal invoice exists and is pending'))
|
||||
}
|
||||
if (error.message.includes('SN_INELIGIBLE')) {
|
||||
bail(new Error('user ineligible for gift'))
|
||||
}
|
||||
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
|
||||
bail(new Error('faucet has been revoked or is exhausted'))
|
||||
}
|
||||
if (error.message.includes('40001')) {
|
||||
throw new Error('wallet balance serialization failure - retry again')
|
||||
}
|
||||
|
|
|
@ -84,6 +84,12 @@ export default {
|
|||
bio: async (user, args, { models }) => {
|
||||
return getItem(user, { id: user.bioId }, { models })
|
||||
},
|
||||
hasInvites: async (user, args, { models }) => {
|
||||
const anInvite = await models.invite.findFirst({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
return !!anInvite
|
||||
},
|
||||
hasNewNotes: async (user, args, { models }) => {
|
||||
// check if any votes have been cast for them since checkedNotesAt
|
||||
const votes = await models.$queryRaw(`
|
||||
|
|
|
@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro'
|
|||
export default gql`
|
||||
extend type Query {
|
||||
invites: [Invite!]!
|
||||
invite(id: ID!): Invite
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
@ -16,6 +17,8 @@ export default gql`
|
|||
invitees: [User!]!
|
||||
gift: Int!
|
||||
limit: Int
|
||||
user: User!
|
||||
revoked: Boolean!
|
||||
poor: Boolean!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -23,6 +23,7 @@ export default gql`
|
|||
freePosts: Int!
|
||||
freeComments: Int!
|
||||
hasNewNotes: Boolean!
|
||||
hasInvites: Boolean!
|
||||
tipDefault: Int!
|
||||
bio: Item
|
||||
sats: Int!
|
||||
|
|
|
@ -64,7 +64,12 @@ export default function Header () {
|
|||
</Link>
|
||||
<NavDropdown.Divider />
|
||||
<Link href='/invites' passHref>
|
||||
<NavDropdown.Item>invites</NavDropdown.Item>
|
||||
<NavDropdown.Item>invites
|
||||
{me && !me.hasInvites &&
|
||||
<div className='p-1 d-inline-block bg-success ml-1'>
|
||||
<span className='invisible'>{' '}</span>
|
||||
</div>}
|
||||
</NavDropdown.Item>
|
||||
</Link>
|
||||
<div>
|
||||
<NavDropdown.Divider />
|
||||
|
|
|
@ -17,7 +17,7 @@ export const EmailSchema = Yup.object({
|
|||
email: Yup.string().email('email is no good').required('required').trim()
|
||||
})
|
||||
|
||||
export default function Login ({ providers, callbackUrl, error }) {
|
||||
export default function Login ({ providers, callbackUrl, error, Header }) {
|
||||
const errors = {
|
||||
Signin: 'Try signing with a different account.',
|
||||
OAuthSignin: 'Try signing with a different account.',
|
||||
|
@ -37,6 +37,7 @@ export default function Login ({ providers, callbackUrl, error }) {
|
|||
return (
|
||||
<LayoutCenter noFooter>
|
||||
<div className={styles.login}>
|
||||
{Header && <Header />}
|
||||
{errorMessage &&
|
||||
<Alert variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>{errorMessage}</Alert>}
|
||||
{router.query.type === 'lightning'
|
||||
|
|
|
@ -20,6 +20,7 @@ export function MeProvider ({ children }) {
|
|||
bio {
|
||||
id
|
||||
}
|
||||
hasInvites
|
||||
}
|
||||
}`
|
||||
const { data } = useQuery(query, { pollInterval: 1000 })
|
||||
|
|
|
@ -11,5 +11,10 @@ export const INVITE_FIELDS = gql`
|
|||
gift
|
||||
limit
|
||||
revoked
|
||||
user {
|
||||
name
|
||||
id
|
||||
}
|
||||
poor
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,11 +1,42 @@
|
|||
import Login from '../../components/login'
|
||||
import { providers, getSession } from 'next-auth/client'
|
||||
import models from '../../api/models'
|
||||
import serialize from '../../api/resolvers/serial'
|
||||
import { gql } from '@apollo/client'
|
||||
import { INVITE_FIELDS } from '../../fragments/invites'
|
||||
import getSSRApolloClient from '../../api/ssrApollo'
|
||||
import Link from 'next/link'
|
||||
|
||||
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
||||
const session = await getSession({ req })
|
||||
|
||||
const client = await getSSRApolloClient(req)
|
||||
const { data } = await client.query({
|
||||
query: gql`
|
||||
${INVITE_FIELDS}
|
||||
{
|
||||
invite(id: "${id}") {
|
||||
...InviteFields
|
||||
}
|
||||
}`
|
||||
})
|
||||
|
||||
if (!data?.invite) {
|
||||
return {
|
||||
notFound: true
|
||||
}
|
||||
}
|
||||
|
||||
if (session && res) {
|
||||
// send down the userid and the invite to the db
|
||||
try {
|
||||
// attempt to send gift
|
||||
// catch any errors and just ignore them for now
|
||||
await serialize(models,
|
||||
models.$queryRaw('SELECT invite_drain($1, $2)', session.user.id, id))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
res.writeHead(302, {
|
||||
Location: '/'
|
||||
})
|
||||
|
@ -17,9 +48,36 @@ export async function getServerSideProps ({ req, res, query: { id, error = null
|
|||
props: {
|
||||
providers: await providers({ req, res }),
|
||||
callbackUrl: process.env.SELF_URL + req.url,
|
||||
invite: data.invite,
|
||||
error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Login
|
||||
function InviteHeader ({ invite }) {
|
||||
console.log(invite.poor)
|
||||
let Inner
|
||||
if (invite.revoked) {
|
||||
Inner = () => <div className='text-danger'>this invite link expired</div>
|
||||
} else if ((invite.limit && invite.limit <= invite.invitees.length) || invite.poor) {
|
||||
Inner = () => <div className='text-danger'>this invite link has no more sats</div>
|
||||
} else {
|
||||
Inner = () => (
|
||||
<div>
|
||||
get <span className='text-success'>{invite.gift} free sats</span> from{' '}
|
||||
<Link href={`/${invite.user.name}`} passHref><a>@{invite.user.name}</a></Link>{' '}
|
||||
when you sign up today
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<h2 className='text-center pb-3'>
|
||||
<Inner />
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Invite ({ invite, ...props }) {
|
||||
return <Login Header={() => <InviteHeader invite={invite} />} {...props} />
|
||||
}
|
||||
|
|
|
@ -46,10 +46,10 @@ function InviteForm () {
|
|||
limit: undefined
|
||||
}}
|
||||
schema={InviteSchema}
|
||||
onSubmit={async ({ limit, ...values }) => {
|
||||
onSubmit={async ({ limit, gift }) => {
|
||||
const { error } = await createInvite({
|
||||
variables: {
|
||||
...values, limit: limit ? Number(limit) : limit
|
||||
gift: Number(gift), limit: limit ? Number(limit) : limit
|
||||
}
|
||||
})
|
||||
if (error) {
|
||||
|
@ -95,6 +95,8 @@ function Invite ({ invite, active }) {
|
|||
placeholder={`https://stacker.news/invites/${invite.id}`} readOnly
|
||||
/>
|
||||
<div className={styles.other}>
|
||||
<span>{invite.gift} sat gift</span>
|
||||
<span> \ </span>
|
||||
<span>{invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''}</span>
|
||||
{active
|
||||
? (
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
CREATE OR REPLACE FUNCTION invite_drain(user_id INTEGER, invite_id TEXT)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
inviter_id INTEGER;
|
||||
inviter_sats INTEGER;
|
||||
gift INTEGER;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
-- check user was created in last hour
|
||||
-- check user did not already redeem an invite
|
||||
PERFORM FROM users
|
||||
WHERE id = user_id AND users.created_at >= NOW() AT TIME ZONE 'UTC' - INTERVAL '1 HOUR'
|
||||
AND users."inviteId" IS NULL;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'SN_INELIGIBLE';
|
||||
END IF;
|
||||
|
||||
-- check that invite has not reached limit
|
||||
-- check that invite is not revoked
|
||||
SELECT "Invite"."userId", "Invite".gift INTO inviter_id, gift FROM "Invite"
|
||||
LEFT JOIN users ON users."inviteId" = invite_id
|
||||
WHERE "Invite".id = invite_id AND NOT "Invite".revoked
|
||||
GROUP BY "Invite".id
|
||||
HAVING COUNT(DISTINCT users.id) < "Invite".limit OR "Invite".limit IS NULL;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
|
||||
END IF;
|
||||
|
||||
-- check that inviter has sufficient balance
|
||||
SELECT (msats / 1000) INTO inviter_sats
|
||||
FROM users WHERE id = inviter_id;
|
||||
IF inviter_sats < gift THEN
|
||||
RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
|
||||
END IF;
|
||||
|
||||
-- subtract amount from inviter
|
||||
UPDATE users SET msats = msats - (1000 * gift) WHERE id = inviter_id;
|
||||
-- add amount to invitee
|
||||
UPDATE users SET msats = msats + (1000 * gift), "inviteId" = invite_id WHERE id = user_id;
|
||||
|
||||
RETURN 0;
|
||||
END;
|
||||
$$;
|
Loading…
Reference in New Issue