Skip to content

Commit

Permalink
Merge pull request #63 from compsci-adl/feat/drive-link
Browse files Browse the repository at this point in the history
feat: Add drive link
  • Loading branch information
rayokamoto authored Feb 18, 2024
2 parents 43a0b50 + 3f2461f commit d175095
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 69 deletions.
3 changes: 3 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ SQUARE_LOCATION_ID=

# Redis
REDIS_URI=

# Club Resources OneDrive link
NEXT_PUBLIC_DRIVE_LINK=
2 changes: 1 addition & 1 deletion src/app/(account)/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
47 changes: 3 additions & 44 deletions src/app/(account)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof verifyMembershipPayment>>;

export default async function SettingsPage() {
const user = await currentUser();
if (!user) return notFound();
Expand All @@ -61,7 +20,7 @@ export default async function SettingsPage() {
<Title colour="purple">Settings</Title>
</div>
<section className="mt-12 flex justify-center">
<FancyRectangle colour="purple" offset="8" filled={true}>
<FancyRectangle colour="purple" offset="8" filled>
<div className="z-0 flex w-fit gap-8 border-4 border-black bg-white px-8 py-8 text-black md:w-[48rem] md:px-12 md:py-12">
{exists ? (
<Settings settingData={{ membershipPayment }} />
Expand Down
14 changes: 13 additions & 1 deletion src/app/api/payment/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -117,3 +118,14 @@ 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) {
return new Response(null, { status: 401 });
}

const { paid } = await verifyMembershipPayment(user.id);
return Response.json({ paid });
}
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
16 changes: 15 additions & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ 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,
});

const checkUserPaid = useSWR<{ paid: boolean }>(['payment'], fetcher.get.query, {
isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn,
});

Expand Down Expand Up @@ -152,8 +156,18 @@ export default function Header() {
Continue Signing Up
</Button>
)}
{!checkUserPaid.data?.paid && (
<Button
colour="orange"
href="/settings"
onClick={closeMenu}
>
Continue to payment
</Button>
)}
<UserButton
userExists={Boolean(checkUserExists.data?.exists)}
userPaid={Boolean(checkUserPaid.data?.paid)}
/>
</>
) : (
Expand Down
30 changes: 19 additions & 11 deletions src/components/UserButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
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';
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();
Expand All @@ -30,18 +37,19 @@ export default function UserButton({ userExists }: { userExists: boolean }) {
{/* Popup menu */}
{isPopupOpen && (
<div className="absolute right-0 top-10 z-10 flex w-52 flex-col gap-y-4 border-4 border-black bg-white p-4 text-xl md:w-44 md:text-base">
{/* 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 */}
<a
href=""
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
CS Club Drive
</a>
{userPaid && (
<Link
href={env.NEXT_PUBLIC_DRIVE_LINK}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
CS Club Drive
</Link>
)}
<Link href="/settings" className="hover:underline">
Settings
</Link>
Expand Down
2 changes: 2 additions & 0 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ export const env = createEnv({
},
client: {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
NEXT_PUBLIC_DRIVE_LINK: z.string().url().min(1),
},
experimental__runtimeEnv: {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
NEXT_PUBLIC_DRIVE_LINK: process.env.NEXT_PUBLIC_DRIVE_LINK,
},
skipValidation: process.env.SKIP_ENV_VALIDATION,
});
11 changes: 11 additions & 0 deletions src/server/check-user-exists.ts
Original file line number Diff line number Diff line change
@@ -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;
};
12 changes: 2 additions & 10 deletions src/db/queries.ts → src/server/update-member-expiry-date.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { db } from '@/db';
import { memberTable } from '@/db/schema';
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();
Expand Down
43 changes: 43 additions & 0 deletions src/server/verify-membership-payment.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof verifyMembershipPayment>>;

0 comments on commit d175095

Please sign in to comment.