From 6b1e48f8d6ee2aef730c8f82f2f8968284664a08 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Sun, 18 Feb 2024 17:41:50 +1030 Subject: [PATCH 01/10] feat: check if paid member before display drive link --- .env.local.example | 3 ++ .../api/verify-membership-payment/route.ts | 9 ++++++ src/components/Header.tsx | 9 ++++++ src/components/UserButton.tsx | 31 ++++++++++++------- src/db/queries.ts | 17 ++++++++++ 5 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 src/app/api/verify-membership-payment/route.ts diff --git a/.env.local.example b/.env.local.example index e73abf77..77420c19 100644 --- a/.env.local.example +++ b/.env.local.example @@ -12,3 +12,6 @@ SQUARE_LOCATION_ID= # Redis REDIS_URI= + +# Drive link +NEXT_PUBLIC_DRIVE_LINK= diff --git a/src/app/api/verify-membership-payment/route.ts b/src/app/api/verify-membership-payment/route.ts new file mode 100644 index 00000000..2e9234cc --- /dev/null +++ b/src/app/api/verify-membership-payment/route.ts @@ -0,0 +1,9 @@ +import { verifyMembershipPayment } from '@/db/queries'; +import { currentUser } from '@clerk/nextjs'; + +export async function GET() { + const user = await currentUser(); + + const paid = await verifyMembershipPayment(user?.id ?? ''); + return Response.json({ paid }); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9f5d6d19..e94b0f33 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -27,6 +27,14 @@ export default function Header() { isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, }); + const checkUserPaid = useSWR<{ paid: boolean }>( + ['verify-membership-payment'], + fetcher.get.query, + { + isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, + } + ); + const [isScrolled, setIsScrolled] = useState(false); useMount(() => { window.addEventListener('scroll', handleScroll); @@ -154,6 +162,7 @@ export default function Header() { )} ) : ( diff --git a/src/components/UserButton.tsx b/src/components/UserButton.tsx index 67c40ed7..b25655a5 100644 --- a/src/components/UserButton.tsx +++ b/src/components/UserButton.tsx @@ -5,7 +5,13 @@ import Link from 'next/link'; import { useState } from 'react'; import FancyRectangle from './FancyRectangle'; -export default function UserButton({ userExists }: { userExists: boolean }) { +export default function UserButton({ + userExists, + userPaid, +}: { + userExists: boolean; + userPaid: boolean; +}) { const { user } = useUser(); const [isPopupOpen, setPopupOpen] = useState(false); const { signOut } = useClerk(); @@ -20,6 +26,8 @@ export default function UserButton({ userExists }: { userExists: boolean }) { if (!user) return <>; + const driveLink = process.env.NEXT_PUBLIC_DRIVE_LINK; + return (
@@ -30,18 +38,19 @@ export default function UserButton({ userExists }: { userExists: boolean }) { {/* Popup menu */} {isPopupOpen && (
- {/* Only show options if finished sign up */} + {/* Only show settings if finished sign up and show drive link if membership paid */} {userExists && ( <> - {/* TODO(#16): Link to CS Club Drive */} - - CS Club Drive - + {driveLink && userPaid && ( + + CS Club Drive + + )} Settings diff --git a/src/db/queries.ts b/src/db/queries.ts index 86f822eb..bc862e15 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -19,3 +19,20 @@ export const updateMemberExpiryDate = async (id: string, idType: 'clerkId' | 'id .where(eq(memberTable[idType], id)); return expiryDate; }; + +export const verifyMembershipPayment = async (clerkUserId: string) => { + const [{ membershipExpiresAt }] = await db + .select({ + id: memberTable.id, + membershipExpiresAt: memberTable.membershipExpiresAt, + }) + .from(memberTable) + .where(eq(memberTable.clerkId, clerkUserId)); + + console.log(membershipExpiresAt); + + if (membershipExpiresAt) { + return true; + } + return false; +}; From a38adcdc8940a59df7e4abe8df6e7918b0d00247 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Sun, 18 Feb 2024 17:43:28 +1030 Subject: [PATCH 02/10] chore: remove console log --- src/db/queries.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/db/queries.ts b/src/db/queries.ts index bc862e15..475f5f36 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -29,8 +29,6 @@ export const verifyMembershipPayment = async (clerkUserId: string) => { .from(memberTable) .where(eq(memberTable.clerkId, clerkUserId)); - console.log(membershipExpiresAt); - if (membershipExpiresAt) { return true; } From 34ac99280f43717fa426bbc7cba846902f6bf9f7 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Sun, 18 Feb 2024 18:03:08 +1030 Subject: [PATCH 03/10] chore: improve drive link comment --- .env.local.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.local.example b/.env.local.example index 77420c19..400206e7 100644 --- a/.env.local.example +++ b/.env.local.example @@ -13,5 +13,5 @@ SQUARE_LOCATION_ID= # Redis REDIS_URI= -# Drive link +# Club Resources OneDrive link NEXT_PUBLIC_DRIVE_LINK= From 37b13f509dd96f3ccc8eb829b7cd2128706c593b Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Sun, 18 Feb 2024 18:06:21 +1030 Subject: [PATCH 04/10] fix: change isPaused condition for checkUserPaid --- src/components/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index e94b0f33..4d3e4e67 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -31,7 +31,7 @@ export default function Header() { ['verify-membership-payment'], fetcher.get.query, { - isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, + isPaused: () => clerkUser.isLoaded && clerkUser.isSignedIn, } ); From c5b261e21a15a12249e96357932f84592d3e8dbb Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sun, 18 Feb 2024 18:36:08 +1030 Subject: [PATCH 05/10] refactor(env): use t3 env --- src/components/UserButton.tsx | 7 +++---- src/env.mjs | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/UserButton.tsx b/src/components/UserButton.tsx index b25655a5..e5c80281 100644 --- a/src/components/UserButton.tsx +++ b/src/components/UserButton.tsx @@ -1,4 +1,5 @@ import Button from '@/components/Button'; +import { env } from '@/env.mjs'; import { useClerk, useUser } from '@clerk/clerk-react'; import Image from 'next/image'; import Link from 'next/link'; @@ -26,8 +27,6 @@ export default function UserButton({ if (!user) return <>; - const driveLink = process.env.NEXT_PUBLIC_DRIVE_LINK; - return (
@@ -41,9 +40,9 @@ export default function UserButton({ {/* Only show settings if finished sign up and show drive link if membership paid */} {userExists && ( <> - {driveLink && userPaid && ( + {userPaid && ( Date: Sun, 18 Feb 2024 18:59:32 +1030 Subject: [PATCH 06/10] refactor(api): encapsulate reusable server functions --- src/app/(account)/settings/Settings.tsx | 2 +- src/app/(account)/settings/page.tsx | 47 ++----------------- src/app/api/payment/route.ts | 13 ++++- .../api/verify-membership-payment/route.ts | 9 ---- src/db/queries.ts | 36 -------------- src/server/check-user-exists.ts | 11 +++++ src/server/update-member-expiry-date.ts | 13 +++++ src/server/verify-membership-payment.ts | 43 +++++++++++++++++ 8 files changed, 83 insertions(+), 91 deletions(-) delete mode 100644 src/app/api/verify-membership-payment/route.ts delete mode 100644 src/db/queries.ts create mode 100644 src/server/check-user-exists.ts create mode 100644 src/server/update-member-expiry-date.ts create mode 100644 src/server/verify-membership-payment.ts diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index d7ba8d12..410cde79 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -1,9 +1,9 @@ 'use client'; +import type { MembershipPayment } from '@/server/verify-membership-payment'; import { useRouter } from 'next/navigation'; import React, { useState } from 'react'; import Sidebar from './Sidebar'; -import type { MembershipPayment } from './page'; import MembershipSettings from './tabs/MembershipSettings'; export const TAB_NAMES = ['Account', 'Personal Info', 'Membership', 'Notifications'] as const; diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index badfc082..f6e92e0c 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -1,53 +1,12 @@ import FancyRectangle from '@/components/FancyRectangle'; import Title from '@/components/Title'; -import { db } from '@/db'; -import { checkUserExists, updateMemberExpiryDate } from '@/db/queries'; -import { memberTable } from '@/db/schema'; -import { redisClient } from '@/lib/redis'; -import { squareClient } from '@/lib/square'; +import { checkUserExists } from '@/server/check-user-exists'; +import { verifyMembershipPayment } from '@/server/verify-membership-payment'; import { currentUser } from '@clerk/nextjs'; -import { eq } from 'drizzle-orm'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import Settings from './Settings'; -const verifyMembershipPayment = async (clerkId: string) => { - // Get user's membership expiry date from the database - const [{ membershipExpiresAt }] = await db - .select({ - id: memberTable.id, - membershipExpiresAt: memberTable.membershipExpiresAt, - }) - .from(memberTable) - .where(eq(memberTable.clerkId, clerkId)); - // If membership expiry date exists, return the existing date - if (membershipExpiresAt) { - return { paid: true as const, membershipExpiresAt }; - } - - const paymentId = await redisClient.hGet(`payment:membership:${clerkId}`, 'paymentId'); - if (!paymentId) { - // Membership payment for the user does not exist - return { paid: false as const }; - } - - const resp = await squareClient.checkoutApi.retrievePaymentLink(paymentId); - const respFields = resp.result; - if (!respFields.paymentLink || respFields.paymentLink.id !== paymentId) { - // Payment has not been made - return { paid: false as const }; - } - - // Set expiry date to be the January 1st of the following year - const expiryDate = await updateMemberExpiryDate(clerkId, 'clerkId'); - - // Delete key from Redis since it is no longer needed - await redisClient.del(`payment:membership:${clerkId}`); - - return { paid: true as const, membershipExpiresAt: expiryDate }; -}; -export type MembershipPayment = Awaited>; - export default async function SettingsPage() { const user = await currentUser(); if (!user) return notFound(); @@ -61,7 +20,7 @@ export default async function SettingsPage() { Settings
- +
{exists ? ( diff --git a/src/app/api/payment/route.ts b/src/app/api/payment/route.ts index e231873e..323c44ae 100644 --- a/src/app/api/payment/route.ts +++ b/src/app/api/payment/route.ts @@ -6,11 +6,12 @@ */ import { PRODUCTS } from '@/data/products'; import { db } from '@/db'; -import { updateMemberExpiryDate } from '@/db/queries'; import { memberTable } from '@/db/schema'; import { env } from '@/env.mjs'; import { redisClient } from '@/lib/redis'; import { squareClient } from '@/lib/square'; +import { updateMemberExpiryDate } from '@/server/update-member-expiry-date'; +import { verifyMembershipPayment } from '@/server/verify-membership-payment'; import { currentUser } from '@clerk/nextjs'; import { eq } from 'drizzle-orm'; import type { CreatePaymentLinkRequest } from 'square'; @@ -117,3 +118,13 @@ export async function PUT(request: Request) { } return Response.json({ success: true }); } + +export async function GET() { + const user = await currentUser(); + if (!user) { + return new Response(null, { status: 401 }); + } + + const { paid } = await verifyMembershipPayment(user.id); + return Response.json({ paid }); +} diff --git a/src/app/api/verify-membership-payment/route.ts b/src/app/api/verify-membership-payment/route.ts deleted file mode 100644 index 2e9234cc..00000000 --- a/src/app/api/verify-membership-payment/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { verifyMembershipPayment } from '@/db/queries'; -import { currentUser } from '@clerk/nextjs'; - -export async function GET() { - const user = await currentUser(); - - const paid = await verifyMembershipPayment(user?.id ?? ''); - return Response.json({ paid }); -} diff --git a/src/db/queries.ts b/src/db/queries.ts deleted file mode 100644 index 475f5f36..00000000 --- a/src/db/queries.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { eq } from 'drizzle-orm'; -import { db } from '.'; -import { memberTable } from './schema'; - -export const checkUserExists = async (clerkUserId: string) => { - const existingUser = await db - .select({ count: memberTable.id }) - .from(memberTable) - .where(eq(memberTable.clerkId, clerkUserId)); - return existingUser.length > 0; -}; - -export const updateMemberExpiryDate = async (id: string, idType: 'clerkId' | 'id') => { - const now = new Date(); - const expiryDate = new Date(`${now.getFullYear() + 1}-01-01`); - await db - .update(memberTable) - .set({ membershipExpiresAt: expiryDate }) - .where(eq(memberTable[idType], id)); - return expiryDate; -}; - -export const verifyMembershipPayment = async (clerkUserId: string) => { - const [{ membershipExpiresAt }] = await db - .select({ - id: memberTable.id, - membershipExpiresAt: memberTable.membershipExpiresAt, - }) - .from(memberTable) - .where(eq(memberTable.clerkId, clerkUserId)); - - if (membershipExpiresAt) { - return true; - } - return false; -}; diff --git a/src/server/check-user-exists.ts b/src/server/check-user-exists.ts new file mode 100644 index 00000000..b636b22e --- /dev/null +++ b/src/server/check-user-exists.ts @@ -0,0 +1,11 @@ +import { db } from '@/db'; +import { memberTable } from '@/db/schema'; +import { eq } from 'drizzle-orm'; + +export const checkUserExists = async (clerkUserId: string) => { + const existingUser = await db + .select({ count: memberTable.id }) + .from(memberTable) + .where(eq(memberTable.clerkId, clerkUserId)); + return existingUser.length > 0; +}; diff --git a/src/server/update-member-expiry-date.ts b/src/server/update-member-expiry-date.ts new file mode 100644 index 00000000..0c6d2866 --- /dev/null +++ b/src/server/update-member-expiry-date.ts @@ -0,0 +1,13 @@ +import { db } from '@/db'; +import { memberTable } from '@/db/schema'; +import { eq } from 'drizzle-orm'; + +export const updateMemberExpiryDate = async (id: string, idType: 'clerkId' | 'id') => { + const now = new Date(); + const expiryDate = new Date(`${now.getFullYear() + 1}-01-01`); + await db + .update(memberTable) + .set({ membershipExpiresAt: expiryDate }) + .where(eq(memberTable[idType], id)); + return expiryDate; +}; diff --git a/src/server/verify-membership-payment.ts b/src/server/verify-membership-payment.ts new file mode 100644 index 00000000..352e555f --- /dev/null +++ b/src/server/verify-membership-payment.ts @@ -0,0 +1,43 @@ +import { db } from '@/db'; +import { memberTable } from '@/db/schema'; +import { redisClient } from '@/lib/redis'; +import { squareClient } from '@/lib/square'; +import { eq } from 'drizzle-orm'; +import { updateMemberExpiryDate } from './update-member-expiry-date'; + +export const verifyMembershipPayment = async (clerkId: string) => { + // Get user's membership expiry date from the database + const [{ membershipExpiresAt }] = await db + .select({ + id: memberTable.id, + membershipExpiresAt: memberTable.membershipExpiresAt, + }) + .from(memberTable) + .where(eq(memberTable.clerkId, clerkId)); + // If membership expiry date exists, return the existing date + if (membershipExpiresAt) { + return { paid: true as const, membershipExpiresAt }; + } + + const paymentId = await redisClient.hGet(`payment:membership:${clerkId}`, 'paymentId'); + if (!paymentId) { + // Membership payment for the user does not exist + return { paid: false as const }; + } + + const resp = await squareClient.checkoutApi.retrievePaymentLink(paymentId); + const respFields = resp.result; + if (!respFields.paymentLink || respFields.paymentLink.id !== paymentId) { + // Payment has not been made + return { paid: false as const }; + } + + // Set expiry date to be the January 1st of the following year + const expiryDate = await updateMemberExpiryDate(clerkId, 'clerkId'); + + // Delete key from Redis since it is no longer needed + await redisClient.del(`payment:membership:${clerkId}`); + + return { paid: true as const, membershipExpiresAt: expiryDate }; +}; +export type MembershipPayment = Awaited>; From 1dbdfaccbf93c8e5c51bf6d4b544dddd6c2688b9 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sun, 18 Feb 2024 19:00:27 +1030 Subject: [PATCH 07/10] refactor(api): `/verify-user-exists` -> `/user-existence` --- src/app/api/{check-user-exists => user-existence}/route.ts | 2 +- src/components/Header.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/app/api/{check-user-exists => user-existence}/route.ts (81%) diff --git a/src/app/api/check-user-exists/route.ts b/src/app/api/user-existence/route.ts similarity index 81% rename from src/app/api/check-user-exists/route.ts rename to src/app/api/user-existence/route.ts index cb2b62d0..26a0a228 100644 --- a/src/app/api/check-user-exists/route.ts +++ b/src/app/api/user-existence/route.ts @@ -1,4 +1,4 @@ -import { checkUserExists } from '@/db/queries'; +import { checkUserExists } from '@/server/check-user-exists'; import { currentUser } from '@clerk/nextjs'; export async function GET() { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4d3e4e67..8624fa5c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -23,7 +23,7 @@ export default function Header() { }; const clerkUser = useUser(); - const checkUserExists = useSWR<{ exists: boolean }>(['check-user-exists'], fetcher.get.query, { + const checkUserExists = useSWR<{ exists: boolean }>(['user-existence'], fetcher.get.query, { isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, }); From 20fd3df3e07877df9cb39c6a81e50c134dd9f836 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sun, 18 Feb 2024 19:02:03 +1030 Subject: [PATCH 08/10] fix(header): payment api check pause condition --- src/components/Header.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 8624fa5c..cc69b2a0 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -27,13 +27,9 @@ export default function Header() { isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, }); - const checkUserPaid = useSWR<{ paid: boolean }>( - ['verify-membership-payment'], - fetcher.get.query, - { - isPaused: () => clerkUser.isLoaded && clerkUser.isSignedIn, - } - ); + const checkUserPaid = useSWR<{ paid: boolean }>(['payment'], fetcher.get.query, { + isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, + }); const [isScrolled, setIsScrolled] = useState(false); useMount(() => { From c7af6fc300c4b638b57d762ab9ffdf5621fc4f9d Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sun, 18 Feb 2024 19:03:21 +1030 Subject: [PATCH 09/10] feat(header): payment button when user didn't pay --- src/components/Header.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index cc69b2a0..9972d5a0 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -156,6 +156,15 @@ export default function Header() { Continue Signing Up )} + {!checkUserPaid.data?.paid && ( + + )} Date: Sun, 18 Feb 2024 20:23:09 +1030 Subject: [PATCH 10/10] chore(payment): Update function comment --- src/app/api/payment/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/api/payment/route.ts b/src/app/api/payment/route.ts index 323c44ae..a57894f4 100644 --- a/src/app/api/payment/route.ts +++ b/src/app/api/payment/route.ts @@ -119,6 +119,7 @@ export async function PUT(request: Request) { return Response.json({ success: true }); } +// Get membership payment status export async function GET() { const user = await currentUser(); if (!user) {