From 7f11792111616e48208066c0ed3bcc3cc085df9a Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 1 Dec 2024 23:31:47 +0100 Subject: [PATCH] Custom invite code and note (#1649) * Custom invite code and note * disable autocomplete and hide invite code under advanced * show invite description only to the owner * note->description and move unser advanced * Update lib/validate.js Co-authored-by: ekzyis * Update lib/webPush.js Co-authored-by: ekzyis * Update api/typeDefs/invite.js Co-authored-by: ekzyis * Update pages/invites/index.js Co-authored-by: ekzyis * Update pages/invites/index.js Co-authored-by: ekzyis * fix * apply review suggestions * change limits * Update lib/validate.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * don't show invite id in push notification * remove invoice metadata from push notifications * fix form reset, jsx/dom attrs, accidental uncontrolled prop warnings * support underscores as we claim * increase default gift to fit inflation --------- Co-authored-by: ekzyis Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b --- api/resolvers/invite.js | 31 +++++++--- api/typeDefs/invite.js | 3 +- components/invite.js | 1 + components/notifications.js | 2 +- components/reply.js | 2 +- fragments/invites.js | 1 + lib/validate.js | 4 +- pages/invites/index.js | 62 ++++++++++++++++--- .../migration.sql | 2 + prisma/schema.prisma | 2 + 10 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 prisma/migrations/20241125195641_custom_invites/migration.sql diff --git a/api/resolvers/invite.js b/api/resolvers/invite.js index c2ad92dd..226debe6 100644 --- a/api/resolvers/invite.js +++ b/api/resolvers/invite.js @@ -2,6 +2,7 @@ import { inviteSchema, validateSchema } from '@/lib/validate' import { msatsToSats } from '@/lib/format' import assertApiKeyNotPermitted from './apiKey' import { GqlAuthenticationError, GqlInputError } from '@/lib/error' +import { Prisma } from '@prisma/client' export default { Query: { @@ -9,7 +10,6 @@ export default { if (!me) { throw new GqlAuthenticationError() } - return await models.invite.findMany({ where: { userId: me.id @@ -29,17 +29,31 @@ export default { }, Mutation: { - createInvite: async (parent, { gift, limit }, { me, models }) => { + createInvite: async (parent, { id, gift, limit, description }, { me, models }) => { if (!me) { throw new GqlAuthenticationError() } assertApiKeyNotPermitted({ me }) - await validateSchema(inviteSchema, { gift, limit }) - - return await models.invite.create({ - data: { gift, limit, userId: me.id } - }) + await validateSchema(inviteSchema, { id, gift, limit, description }) + try { + return await models.invite.create({ + data: { + id, + gift, + limit, + userId: me.id, + description + } + }) + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002' && error.meta.target.includes('id')) { + throw new GqlInputError('an invite with this code already exists') + } + } + throw error + } }, revokeInvite: async (parent, { id }, { me, models }) => { if (!me) { @@ -70,6 +84,9 @@ export default { poor: async (invite, args, { me, models }) => { const user = await models.user.findUnique({ where: { id: invite.userId } }) return msatsToSats(user.msats) < invite.gift + }, + description: (invite, args, { me }) => { + return invite.userId === me?.id ? invite.description : undefined } } } diff --git a/api/typeDefs/invite.js b/api/typeDefs/invite.js index 93e8f964..038447d8 100644 --- a/api/typeDefs/invite.js +++ b/api/typeDefs/invite.js @@ -7,7 +7,7 @@ export default gql` } extend type Mutation { - createInvite(gift: Int!, limit: Int): Invite + createInvite(id: String, gift: Int!, limit: Int, description: String): Invite revokeInvite(id: ID!): Invite } @@ -20,5 +20,6 @@ export default gql` user: User! revoked: Boolean! poor: Boolean! + description: String } ` diff --git a/components/invite.js b/components/invite.js index cd2a08ca..9eddbc43 100644 --- a/components/invite.js +++ b/components/invite.js @@ -20,6 +20,7 @@ export default function Invite ({ invite, active }) {
+ {invite.description && {invite.description}} your invite has been redeemed by - {numWithUnits(n.invite.invitees.length, { + {' ' + numWithUnits(n.invite.invitees.length, { abbreviate: false, unitSingular: 'stacker', unitPlural: 'stackers' diff --git a/components/reply.js b/components/reply.js index eb6c0c41..71427b47 100644 --- a/components/reply.js +++ b/components/reply.js @@ -113,7 +113,7 @@ export default forwardRef(function Reply ({ } }, onSuccessfulSubmit: (data, { resetForm }) => { - resetForm({ text: '' }) + resetForm({ values: { text: '' } }) setReply(replyOpen || false) }, navigateOnSubmit: false diff --git a/fragments/invites.js b/fragments/invites.js index 038ba53f..1abc5488 100644 --- a/fragments/invites.js +++ b/fragments/invites.js @@ -19,5 +19,6 @@ export const INVITE_FIELDS = gql` ...StreakFields } poor + description } ` diff --git a/lib/validate.js b/lib/validate.js index bb4de8c5..39de723b 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -478,7 +478,9 @@ export const bioSchema = object({ export const inviteSchema = object({ gift: intValidator.positive('must be greater than 0').required('required'), - limit: intValidator.positive('must be positive') + limit: intValidator.positive('must be positive'), + description: string().trim().max(40, 'must be at most 40 characters'), + id: string().matches(/^[\w-_]+$/, 'only letters, numbers, underscores, and hyphens').min(4, 'must be at least 4 characters').max(32, 'must be at most 32 characters') }) export const pushSubscriptionSchema = object({ diff --git a/pages/invites/index.js b/pages/invites/index.js index 37c71b13..a7ee743d 100644 --- a/pages/invites/index.js +++ b/pages/invites/index.js @@ -9,6 +9,8 @@ import Invite from '@/components/invite' import { inviteSchema } from '@/lib/validate' import { SSR } from '@/lib/constants' import { getGetServerSideProps } from '@/api/ssrApollo' +import Info from '@/components/info' +import Text from '@/components/text' // force SSR to include CSP nonces export const getServerSideProps = getGetServerSideProps({ query: null }) @@ -17,8 +19,8 @@ function InviteForm () { const [createInvite] = useMutation( gql` ${INVITE_FIELDS} - mutation createInvite($gift: Int!, $limit: Int) { - createInvite(gift: $gift, limit: $limit) { + mutation createInvite($id: String, $gift: Int!, $limit: Int, $description: String) { + createInvite(id: $id, gift: $gift, limit: $limit, description: $description) { ...InviteFields } }`, { @@ -39,20 +41,28 @@ function InviteForm () { } ) + const initialValues = { + id: '', + gift: 1000, + limit: 1, + description: '' + } + return (
{ + onSubmit={async ({ id, gift, limit, description }, { resetForm }) => { const { error } = await createInvite({ variables: { - gift: Number(gift), limit: limit ? Number(limit) : limit + id: id || undefined, + gift: Number(gift), + limit: limit ? Number(limit) : limit, + description: description || undefined } }) if (error) throw error + resetForm({ values: initialValues }) }} > invitee limit optional} name='limit' /> - - create + + {`${process.env.NEXT_PUBLIC_URL}/invites/`}} + label={<>invite code optional} + name='id' + autoComplete='off' + /> + +
+ description optional + + + A brief description to keep track of the invite purpose, such as "Shared in group chat". + This description is private and visible only to you. + + +
+ + } + name='description' + autoComplete='off' + /> + + } + /> + create + ) } diff --git a/prisma/migrations/20241125195641_custom_invites/migration.sql b/prisma/migrations/20241125195641_custom_invites/migration.sql new file mode 100644 index 00000000..aa7e66a5 --- /dev/null +++ b/prisma/migrations/20241125195641_custom_invites/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Invite" ADD COLUMN "description" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cd726fcd..94eac2fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -477,6 +477,8 @@ model Invite { user User @relation("Invites", fields: [userId], references: [id], onDelete: Cascade) invitees User[] + description String? + @@index([createdAt], map: "Invite.created_at_index") @@index([userId], map: "Invite.userId_index") }