feat: build generic voting api, build useVote hook

This commit is contained in:
MTG2000
2022-05-21 13:36:34 +03:00
parent 5ff83ddc62
commit cdbd10da4e
12 changed files with 243 additions and 220 deletions

View File

@@ -32,7 +32,7 @@ export interface NexusGenInputs {
export interface NexusGenEnums {
POST_TYPE: "Bounty" | "Question" | "Story"
VOTE_ITEM_TYPE: "Bounty" | "Comment" | "Project" | "Question" | "Story" | "User"
VOTE_ITEM_TYPE: "Bounty" | "PostComment" | "Project" | "Question" | "Story" | "User"
}
export interface NexusGenScalars {
@@ -137,13 +137,6 @@ export interface NexusGenObjects {
name: string; // String!
}
Vote: { // root type
amount_in_sat: number; // Int!
id: number; // Int!
paid: boolean; // Boolean!
payment_hash: string; // String!
payment_request: string; // String!
}
Vote2: { // root type
amount_in_sat: number; // Int!
id: number; // Int!
item_id: number; // Int!
@@ -214,7 +207,6 @@ export interface NexusGenFieldTypes {
Mutation: { // field return type
confirmVote: NexusGenRootTypes['Vote']; // Vote!
vote: NexusGenRootTypes['Vote']; // Vote!
vote2: NexusGenRootTypes['Vote2']; // Vote2!
}
PostComment: { // field return type
author: NexusGenRootTypes['User']; // User!
@@ -298,14 +290,6 @@ export interface NexusGenFieldTypes {
name: string; // String!
}
Vote: { // field return type
amount_in_sat: number; // Int!
id: number; // Int!
paid: boolean; // Boolean!
payment_hash: string; // String!
payment_request: string; // String!
project: NexusGenRootTypes['Project']; // Project!
}
Vote2: { // field return type
amount_in_sat: number; // Int!
id: number; // Int!
item_id: number; // Int!
@@ -372,7 +356,6 @@ export interface NexusGenFieldTypeNames {
Mutation: { // field return type name
confirmVote: 'Vote'
vote: 'Vote'
vote2: 'Vote2'
}
PostComment: { // field return type name
author: 'User'
@@ -456,14 +439,6 @@ export interface NexusGenFieldTypeNames {
name: 'String'
}
Vote: { // field return type name
amount_in_sat: 'Int'
id: 'Int'
paid: 'Boolean'
payment_hash: 'String'
payment_request: 'String'
project: 'Project'
}
Vote2: { // field return type name
amount_in_sat: 'Int'
id: 'Int'
item_id: 'Int'
@@ -489,10 +464,6 @@ export interface NexusGenArgTypes {
preimage: string; // String!
}
vote: { // args
amount_in_sat: number; // Int!
project_id: number; // Int!
}
vote2: { // args
amount_in_sat: number; // Int!
item_id: number; // Int!
item_type: NexusGenEnums['VOTE_ITEM_TYPE']; // VOTE_ITEM_TYPE!

View File

@@ -56,8 +56,7 @@ type LnurlDetails {
type Mutation {
confirmVote(payment_request: String!, preimage: String!): Vote!
vote(amount_in_sat: Int!, project_id: Int!): Vote!
vote2(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote2!
vote(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote!
}
enum POST_TYPE {
@@ -168,7 +167,7 @@ type User {
enum VOTE_ITEM_TYPE {
Bounty
Comment
PostComment
Project
Question
Story
@@ -176,15 +175,6 @@ enum VOTE_ITEM_TYPE {
}
type Vote {
amount_in_sat: Int!
id: Int!
paid: Boolean!
payment_hash: String!
payment_request: String!
project: Project!
}
type Vote2 {
amount_in_sat: Int!
id: Int!
item_id: Int!

View File

@@ -34,7 +34,8 @@ async function getLnurlCallbackUrl(lightning_address) {
);
}
async function getPaymetRequestForProject(project, amount_in_sat) {
async function getPaymetRequestForItem(lightning_address, amount_in_sat) {
// # NOTE: CACHING LNURL CALLBACK URLS + PARAMETERS
// LNURL flows have a lot of back and forth and can impact
// the load time for your application users.
@@ -45,11 +46,9 @@ async function getPaymetRequestForProject(project, amount_in_sat) {
// careful when trying to optimise the amount of
// requests so be mindful of this when you are storing
// these items.
let lnurlCallbackUrl = project.lnurl_callback_url;
const amount = amount_in_sat * 1000; // msats
if (!lnurlCallbackUrl) {
lnurlCallbackUrl = await getLnurlCallbackUrl(project.lightning_address);
}
let lnurlCallbackUrl = await getLnurlCallbackUrl(lightning_address);
return axios
.get(lnurlCallbackUrl, { params: { amount } })
.then((prResponse) => {
@@ -69,10 +68,9 @@ const paginationArgs = (args) => {
}
module.exports = {
getPaymetRequestForProject,
getPaymetRequestForItem,
hexToUint8Array,
lightningAddressToLnurl,
getLnurlDetails,
paginationArgs
}

View File

@@ -8,7 +8,7 @@ const {
enumType,
} = require('nexus')
const { parsePaymentRequest } = require('invoices');
const { getPaymetRequestForProject, hexToUint8Array } = require('./helpers');
const { getPaymetRequestForItem, hexToUint8Array } = require('./helpers');
const { createHash } = require('crypto');
const { prisma } = require('../prisma')
@@ -16,9 +16,10 @@ const { prisma } = require('../prisma')
// the types of items we can vote to
const VOTE_ITEM_TYPE = enumType({
name: 'VOTE_ITEM_TYPE',
members: ['Story', 'Bounty', 'Question', 'Project', 'User', 'Comment'],
members: ['Story', 'Bounty', 'Question', 'Project', 'User', 'PostComment'],
})
const Vote = objectType({
name: 'Vote',
definition(t) {
@@ -28,26 +29,6 @@ const Vote = objectType({
t.nonNull.string('payment_hash');
t.nonNull.boolean('paid');
t.nonNull.field('project', {
type: "Project",
resolve: (parent, args,) => {
return parent.project ?? prisma.vote.findUnique({
where: { id: parent.id }
}).project()
}
})
}
})
const Vote2 = objectType({
name: 'Vote2',
definition(t) {
t.nonNull.int('id');
t.nonNull.int('amount_in_sat');
t.nonNull.string('payment_request');
t.nonNull.string('payment_hash');
t.nonNull.boolean('paid');
t.nonNull.field('item_type', {
type: "VOTE_ITEM_TYPE"
})
@@ -67,46 +48,78 @@ const LnurlDetails = objectType({
}
})
// This is the old voting mutation, it can only vote for projects (SHOULD BE REPLACED BY THE NEW VOTE MUTATION WHEN THAT ONE IS WORKING)
const getModalOfType = (type) => {
switch (type) {
case "Story":
return prisma.story;
case "Question":
return prisma.question;
case "Project":
return prisma.project;
case "Comment":
return prisma.postComment;
default:
return null;
}
}
const getLightningAddress = async (item_id, item_type) => {
switch (item_type) {
case "Story":
return prisma.story.findUnique({
where: { id: item_id }, include: {
user: {
select: {
lightning_address: true
}
}
}
}).then(data => data.user.lightning_address);
case "Question":
return prisma.question.findUnique({
where: { id: item_id }, include: {
user: {
select: {
lightning_address: true
}
}
}
}).then(data => data.user.lightning_address);
case "Project":
return prisma.project.findUnique({
where: { id: item_id },
select: {
lightning_address: true
}
}).then(data => data.lightning_address);
case "Comment":
return prisma.postComment.findUnique({
where: { id: item_id }, include: {
user: {
select: {
lightning_address: true
}
}
}
}).then(data => data.user.lightning_address);
default:
return null;
}
}
// type => modal
// type => lightning address (pr)
// This is the new voting mutation, it can vote for any type of item that we define in the VOTE_ITEM_TYPE enum
const voteMutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field('vote', {
type: "Vote",
args: {
project_id: nonNull(intArg()),
amount_in_sat: nonNull(intArg())
},
resolve: async (_, args) => {
const project = await prisma.project.findUnique({
where: { id: args.project_id },
});
const pr = await getPaymetRequestForProject(project, args.amount_in_sat);
const invoice = parsePaymentRequest({ request: pr });
return prisma.vote.create({
data: {
project_id: project.id,
amount_in_sat: args.amount_in_sat,
payment_request: pr,
payment_hash: invoice.id,
},
include: {
project: true
}
});
}
})
}
})
// This is the new voting mutation, it can vote for any type of item that we define in the VOTE_ITEM_TYPE enum
const vote2Mutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field('vote2', {
type: "Vote2",
args: {
item_type: arg({
type: nonNull("VOTE_ITEM_TYPE")
@@ -117,20 +130,23 @@ const vote2Mutation = extendType({
resolve: async (_, args) => {
const { item_id, item_type, amount_in_sat } = args;
const lightning_address = getLightningAddress(item_id, item_type)
const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat);
const invoice = parsePaymentRequest({ request: pr });
// Create the invoice here according to it's type & get a payment request and a payment hash
// #TODO remove votes rows that get added but not confirmed after some time
// maybe using a scheduler, timeout, or whatever mean available
return {
id: 111,
amount_in_sat: amount_in_sat,
payment_request: '{{payment_request}}',
payment_hash: '{{payment_hash}}',
paid: true,
item_type: item_type,
item_id: item_id,
}
return prisma.vote.create({
data: {
item_type: item_type,
item_id: item_id,
amount_in_sat: amount_in_sat,
payment_request: pr,
payment_hash: invoice.id,
}
});
}
})
}
})
@@ -156,17 +172,19 @@ const confirmVoteMutation = extendType({
payment_hash: paymentHash,
},
});
// if we find a vote it means the preimage is correct and we update the vote and mark it as paid
// can we write this nicer?
if (vote) {
const project = await prisma.project.findUnique({
where: { id: vote.project_id },
const modal = getModalOfType(vote.item_type);
const item = await modal.findUnique({
where: { id: vote.item_id },
});
// count up votes cache
await prisma.project.update({
where: { id: project.id },
await modal.update({
where: { id: item.id },
data: {
votes_count: project.votes_count + vote.amount_in_sat,
votes_count: item.votes_count + vote.amount_in_sat,
},
});
// return the current vote
@@ -175,9 +193,6 @@ const confirmVoteMutation = extendType({
data: {
paid: true,
preimage: args.preimage,
},
include: {
project: true
}
});
} else {
@@ -194,11 +209,9 @@ module.exports = {
// Types
Vote,
Vote2,
LnurlDetails,
// Mutations
voteMutation,
vote2Mutation,
confirmVoteMutation
}

View File

@@ -0,0 +1,15 @@
/*
Warnings:
- You are about to drop the column `project_id` on the `Vote` table. All the data in the column will be lost.
- Added the required column `item_id` to the `Vote` table without a default value. This is not possible if the table is not empty.
- Added the required column `item_type` to the `Vote` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "Vote" DROP CONSTRAINT "Vote_project_id_fkey";
-- AlterTable
ALTER TABLE "Vote" DROP COLUMN "project_id",
ADD COLUMN "item_id" INTEGER NOT NULL,
ADD COLUMN "item_type" TEXT NOT NULL;

View File

@@ -22,8 +22,8 @@ model Tag {
model Vote {
id Int @id @default(autoincrement())
project Project @relation(fields: [project_id], references: [id])
project_id Int
item_id Int
item_type String
amount_in_sat Int
payment_request String?
payment_hash String?
@@ -73,7 +73,6 @@ model Project {
category Category @relation(fields: [category_id], references: [id])
category_id Int
votes_count Int @default(0)
vote Vote[]
createdAt DateTime @default(now())
awards Award[]

View File

@@ -3,11 +3,10 @@ import React, { FormEvent, useState } from 'react';
import { AiFillThunderbolt } from 'react-icons/ai'
import { IoClose } from 'react-icons/io5'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
import { useAppSelector } from 'src/utils/hooks';
import { PaymentStatus, useVote } from 'src/utils/hooks';
import Confetti from "react-confetti";
import { Wallet_Service } from 'src/services';
import { useWindowSize } from '@react-hookz/web';
import { useConfirmVoteMutation, useVoteMutation } from 'src/graphql';
import { Vote_Item_Type } from 'src/graphql';
const defaultOptions = [
{ text: '100 sat', value: 100 },
@@ -16,17 +15,6 @@ const defaultOptions = [
]
enum PaymentStatus {
DEFAULT,
FETCHING_PAYMENT_DETAILS,
PAID,
AWAITING_PAYMENT,
PAYMENT_CONFIRMED,
NOT_PAID,
CANCELED
}
interface Props extends ModalCard {
projectId: number;
initVotes?: number;
@@ -35,57 +23,24 @@ interface Props extends ModalCard {
export default function VoteCard({ onClose, direction, projectId, initVotes, ...props }: Props) {
const { width, height } = useWindowSize()
const { isWalletConnected } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
initVotes: state.vote.voteAmount,
projectId: state.project.openId
}));
const [selectedOption, setSelectedOption] = useState(10);
const [voteAmount, setVoteAmount] = useState<number>(initVotes ?? 10);
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.DEFAULT);
const [vote, { data }] = useVoteMutation({
onCompleted: async (votingData) => {
try {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
const webln = await Wallet_Service.getWebln()
const paymentResponse = await webln.sendPayment(votingData.vote.payment_request);
setPaymentStatus(PaymentStatus.PAID);
confirmVote({
variables: {
paymentRequest: votingData.vote.payment_request,
preimage: paymentResponse.preimage
}
})
} catch (error) {
console.log(error);
setPaymentStatus(PaymentStatus.NOT_PAID);
}
const { vote, paymentStatus } = useVote({
onSuccess: () => {
setTimeout(() => {
onClose?.();
}, 4000);
},
onError: (error) => {
console.log(error);
alert("Something wrong happened...")
setPaymentStatus(PaymentStatus.NOT_PAID);
onError: () => {
setTimeout(() => {
onClose?.();
}, 4000);
}
});
})
const [confirmVote, { data: confirmedVoteData }] = useConfirmVoteMutation({
onCompleted: (votingData) => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
setTimeout(() => {
onClose?.();
}, 4000);
},
onError: () => { }
});
const onChangeInput = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedOption(-1);
@@ -99,8 +54,7 @@ export default function VoteCard({ onClose, direction, projectId, initVotes, ...
const requestPayment = (e: FormEvent) => {
e.preventDefault();
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS);
vote({ variables: { "amountInSat": voteAmount, "projectId": projectId! } });
vote({ variables: { "amountInSat": voteAmount, "itemId": projectId!, itemType: Vote_Item_Type.Project } });
}
return (

View File

@@ -75,7 +75,6 @@ export type Mutation = {
__typename?: 'Mutation';
confirmVote: Vote;
vote: Vote;
vote2: Vote2;
};
@@ -86,12 +85,6 @@ export type MutationConfirmVoteArgs = {
export type MutationVoteArgs = {
amount_in_sat: Scalars['Int'];
project_id: Scalars['Int'];
};
export type MutationVote2Args = {
amount_in_sat: Scalars['Int'];
item_id: Scalars['Int'];
item_type: Vote_Item_Type;
@@ -274,7 +267,7 @@ export type User = {
export enum Vote_Item_Type {
Bounty = 'Bounty',
Comment = 'Comment',
PostComment = 'PostComment',
Project = 'Project',
Question = 'Question',
Story = 'Story',
@@ -285,16 +278,6 @@ export type Vote = {
__typename?: 'Vote';
amount_in_sat: Scalars['Int'];
id: Scalars['Int'];
paid: Scalars['Boolean'];
payment_hash: Scalars['String'];
payment_request: Scalars['String'];
project: Project;
};
export type Vote2 = {
__typename?: 'Vote2';
amount_in_sat: Scalars['Int'];
id: Scalars['Int'];
item_id: Scalars['Int'];
item_type: Vote_Item_Type;
paid: Scalars['Boolean'];
@@ -372,12 +355,13 @@ export type ProjectDetailsQueryVariables = Exact<{
export type ProjectDetailsQuery = { __typename?: 'Query', getProject: { __typename?: 'Project', id: number, title: string, description: string, cover_image: string, thumbnail_image: string, screenshots: Array<string>, website: string, lightning_address: string | null, lnurl_callback_url: string | null, votes_count: number, category: { __typename?: 'Category', id: number, title: string }, awards: Array<{ __typename?: 'Award', title: string, image: string, url: string, id: number }>, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } };
export type VoteMutationVariables = Exact<{
projectId: Scalars['Int'];
itemType: Vote_Item_Type;
itemId: Scalars['Int'];
amountInSat: Scalars['Int'];
}>;
export type VoteMutation = { __typename?: 'Mutation', vote: { __typename?: 'Vote', id: number, amount_in_sat: number, payment_request: string, payment_hash: string, paid: boolean } };
export type VoteMutation = { __typename?: 'Mutation', vote: { __typename?: 'Vote', id: number, amount_in_sat: number, payment_request: string, payment_hash: string, paid: boolean, item_type: Vote_Item_Type, item_id: number } };
export type ConfirmVoteMutationVariables = Exact<{
paymentRequest: Scalars['String'];
@@ -385,7 +369,7 @@ export type ConfirmVoteMutationVariables = Exact<{
}>;
export type ConfirmVoteMutation = { __typename?: 'Mutation', confirmVote: { __typename?: 'Vote', id: number, amount_in_sat: number, payment_request: string, payment_hash: string, paid: boolean, project: { __typename?: 'Project', id: number, votes_count: number } } };
export type ConfirmVoteMutation = { __typename?: 'Mutation', confirmVote: { __typename?: 'Vote', id: number, amount_in_sat: number, payment_request: string, payment_hash: string, paid: boolean, item_type: Vote_Item_Type, item_id: number } };
export const NavCategoriesDocument = gql`
@@ -1029,13 +1013,15 @@ export type ProjectDetailsQueryHookResult = ReturnType<typeof useProjectDetailsQ
export type ProjectDetailsLazyQueryHookResult = ReturnType<typeof useProjectDetailsLazyQuery>;
export type ProjectDetailsQueryResult = Apollo.QueryResult<ProjectDetailsQuery, ProjectDetailsQueryVariables>;
export const VoteDocument = gql`
mutation Vote($projectId: Int!, $amountInSat: Int!) {
vote(project_id: $projectId, amount_in_sat: $amountInSat) {
mutation Vote($itemType: VOTE_ITEM_TYPE!, $itemId: Int!, $amountInSat: Int!) {
vote(item_type: $itemType, item_id: $itemId, amount_in_sat: $amountInSat) {
id
amount_in_sat
payment_request
payment_hash
paid
item_type
item_id
}
}
`;
@@ -1054,7 +1040,8 @@ export type VoteMutationFn = Apollo.MutationFunction<VoteMutation, VoteMutationV
* @example
* const [voteMutation, { data, loading, error }] = useVoteMutation({
* variables: {
* projectId: // value for 'projectId'
* itemType: // value for 'itemType'
* itemId: // value for 'itemId'
* amountInSat: // value for 'amountInSat'
* },
* });
@@ -1074,10 +1061,8 @@ export const ConfirmVoteDocument = gql`
payment_request
payment_hash
paid
project {
id
votes_count
}
item_type
item_id
}
}
`;

View File

@@ -5,3 +5,4 @@ export * from "./useInfiniteQuery";
export * from "./useReachedBottom";
export * from "./useAutoResizableTextArea";
export * from "./useCopyToClipboard";
export * from "./useVote";

View File

@@ -0,0 +1 @@
export * from './useVote'

View File

@@ -0,0 +1,96 @@
import { gql } from '@apollo/client';
import { useState } from 'react';
import { useConfirmVoteMutation, useVoteMutation } from 'src/graphql';
import { Wallet_Service } from 'src/services';
export enum PaymentStatus {
DEFAULT,
FETCHING_PAYMENT_DETAILS,
PAID,
AWAITING_PAYMENT,
PAYMENT_CONFIRMED,
NOT_PAID,
CANCELED
}
export const useVote = ({ onSuccess, onError }: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.DEFAULT);
const [voteMutaion] = useVoteMutation({
onCompleted: async (votingData) => {
try {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
const webln = await Wallet_Service.getWebln()
const paymentResponse = await webln.sendPayment(votingData.vote.payment_request);
setPaymentStatus(PaymentStatus.PAID);
confirmVote({
variables: {
paymentRequest: votingData.vote.payment_request,
preimage: paymentResponse.preimage
}
})
} catch (error) {
console.log(error);
setPaymentStatus(PaymentStatus.NOT_PAID);
}
},
onError: (error) => {
console.log(error);
alert("Something wrong happened...")
setPaymentStatus(PaymentStatus.NOT_PAID);
onError?.(error)
}
});
const [confirmVote] = useConfirmVoteMutation({
onCompleted: () => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
onSuccess?.();
},
update(cache, { data }) {
try {
const { item_id, item_type, amount_in_sat } = data!.confirmVote;
const { votes_count } = cache.readFragment({
id: `${item_type}:${item_id}`,
fragment: gql`
fragment My${item_type} on ${item_type} {
votes_count
}`
}) ?? {};
cache.writeFragment({
id: `${item_type}:${item_id}`,
fragment: gql`
fragment My${item_type} on ${item_type} {
votes_count
}
`,
data: {
votes_count: votes_count + amount_in_sat
},
})
} catch (error) {
}
},
onError: () => { }
});
const vote = (...params: Parameters<typeof voteMutaion>) => {
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS)
voteMutaion(...params)
}
return {
paymentStatus,
vote
}
}

View File

@@ -1,10 +1,12 @@
mutation Vote($projectId: Int!, $amountInSat: Int!) {
vote(project_id: $projectId, amount_in_sat: $amountInSat) {
mutation Vote($itemType: VOTE_ITEM_TYPE!, $itemId: Int!, $amountInSat: Int!) {
vote(item_type: $itemType, item_id: $itemId, amount_in_sat: $amountInSat) {
id
amount_in_sat
payment_request
payment_hash
paid
item_type
item_id
}
}
@@ -15,9 +17,7 @@ mutation ConfirmVote($paymentRequest: String!, $preimage: String!) {
payment_request
payment_hash
paid
project {
id
votes_count
}
item_type
item_id
}
}