From de6ce5a23a44e4831b71d78d9152197a12cf42fe Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Mon, 18 Aug 2025 00:06:59 +0800 Subject: [PATCH 01/12] add validation and transaction --- src/app/verification/page.tsx | 36 +++++-- src/server/api/routers/users/create.ts | 99 ++++++++++++------- src/server/api/routers/users/index.ts | 2 + src/server/api/routers/users/rollbackClerk.ts | 30 ++++++ 4 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 src/server/api/routers/users/rollbackClerk.ts diff --git a/src/app/verification/page.tsx b/src/app/verification/page.tsx index 683c6a87f..647ac34c1 100644 --- a/src/app/verification/page.tsx +++ b/src/app/verification/page.tsx @@ -1,25 +1,42 @@ "use client" -import { useClerk } from "@clerk/nextjs" +import { useClerk, useUser } from "@clerk/nextjs" import { EmailLinkErrorCode, isEmailLinkError } from "@clerk/nextjs/errors" +import { useRouter } from "next/navigation" import * as React from "react" +import { api } from "~/trpc/react" + export default function Verification() { const [verificationStatus, setVerificationStatus] = React.useState("loading") const { handleEmailLinkVerification } = useClerk() - + const { user } = useUser() + const rollbackMutation = api.users.rollbackClerk.useMutation() + const { data: dbUser, isLoading: isDbUserLoading } = api.users.getCurrent.useQuery() + const router = useRouter() React.useEffect(() => { const verify = async () => { try { - // TODO redirect to dashboard if signing in, or to the create account page if signing up + // TODO: redirect to dashboard if signing in, or to the create account page if signing up await handleEmailLinkVerification({}) // If we're not redirected at this point, it means // that the flow has completed on another device. - setVerificationStatus("verified") + + if (isDbUserLoading) { + // still loading → do nothing yet + return + } + if (!dbUser) { + if (user?.id) { + await rollbackMutation.mutateAsync({ clerk_id: user.id }) + } + setVerificationStatus("dbFailed") + return + } else setVerificationStatus("verified") } catch (err) { // Verification has failed. - let status = "failed" + let status = "failed" // @ts-expect-error - Yes it does if (isEmailLinkError(err as Error) && err?.code === EmailLinkErrorCode.Expired) { status = "expired" @@ -30,7 +47,7 @@ export default function Verification() { verify().catch((e) => { console.error(e) }) - }, [handleEmailLinkVerification]) + }, [handleEmailLinkVerification, router, user, dbUser, rollbackMutation]) switch (verificationStatus) { case "loading": @@ -39,6 +56,13 @@ export default function Verification() { return
Verification link has expired!
case "failed": return
Verification failed!
+ case "dbFailed": + return ( +
+ Verification failed because the registration page was closed! Please DO NOT close the registration page while + verifying your email address! Please try again! +
+ ) default: return (
diff --git a/src/server/api/routers/users/create.ts b/src/server/api/routers/users/create.ts index 7ac2b5d33..64daf51e6 100644 --- a/src/server/api/routers/users/create.ts +++ b/src/server/api/routers/users/create.ts @@ -52,49 +52,72 @@ export const create = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) }), ) .mutation(async ({ ctx, input }) => { - // TODO: wrap in a transaction - const { result, statusCode } = await squareClient.customersApi.createCustomer({ - idempotencyKey: randomUUID(), - givenName: input.preferred_name, - familyName: input.name, - emailAddress: input.email, - // TODO: convert to user ID (UUIDv7) - referenceId: input.clerk_id, - }) + let createdSquareCustomerId: string | null = null + let createdClerkUserId: string | null = null - if (!result.customer?.id) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to create square customer ${statusCode}`, - cause: JSON.stringify(result), + try { + const { result, statusCode } = await squareClient.customersApi.createCustomer({ + idempotencyKey: randomUUID(), + givenName: input.preferred_name, + familyName: input.name, + emailAddress: input.email, + // TODO: convert to user ID (UUIDv7) + referenceId: input.clerk_id, }) - } - const clerkUser = await clerkClient().users.getUser(input.clerk_id) + if (!result.customer?.id) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Failed to create square customer ${statusCode}`, + cause: JSON.stringify(result), + }) + } + createdSquareCustomerId = result.customer.id + const clerkUser = await clerkClient().users.getUser(input.clerk_id) + + if (!clerkUser) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Clerk user with id: ${input.clerk_id} does not exist`, + }) + } + createdClerkUserId = clerkUser.id - if (!clerkUser) { + const [user] = await ctx.db + .insert(User) + .values({ + clerk_id: input.clerk_id, + name: input.name, + preferred_name: input.preferred_name, + email: input.email, + pronouns: input.pronouns, + student_number: input.student_number, + university: input.uni, + github: input.github, + discord: input.discord, + subscribe: input.subscribe ?? true, + square_customer_id: result.customer.id, + }) + .returning() + return user + } catch (err) { + if (createdSquareCustomerId) { + try { + await squareClient.customersApi.deleteCustomer(createdSquareCustomerId) + } catch (cleanupErr) { + console.error("Failed to cleanup Square customer", cleanupErr) + } + } + if (createdClerkUserId) { + try { + await clerkClient().users.deleteUser(createdClerkUserId) + } catch (cleanupErr) { + console.error("Failed to cleanup Clerk user", cleanupErr) + } + } throw new TRPCError({ - code: "NOT_FOUND", - message: `Clerk user with id: ${input.clerk_id} does not exist`, + code: "INTERNAL_SERVER_ERROR", + message: `Failed to register user: ${err}`, }) } - - const [user] = await ctx.db - .insert(User) - .values({ - clerk_id: input.clerk_id, - name: input.name, - preferred_name: input.preferred_name, - email: input.email, - pronouns: input.pronouns, - student_number: input.student_number, - university: input.uni, - github: input.github, - discord: input.discord, - subscribe: input.subscribe ?? true, - square_customer_id: result.customer.id, - }) - .returning() - - return user }) diff --git a/src/server/api/routers/users/index.ts b/src/server/api/routers/users/index.ts index 27d864f8f..b5256ee22 100644 --- a/src/server/api/routers/users/index.ts +++ b/src/server/api/routers/users/index.ts @@ -6,6 +6,7 @@ import { createTRPCRouter } from "~/server/api/trpc" import { create } from "./create" import { get } from "./get" import { getCurrent } from "./get-current" +import { rollbackClerk } from "./rollbackClerk" import { update } from "./update" import { updateSocials } from "./update-socials" @@ -15,4 +16,5 @@ export const usersRouter = createTRPCRouter({ get, update, updateSocials, + rollbackClerk, }) diff --git a/src/server/api/routers/users/rollbackClerk.ts b/src/server/api/routers/users/rollbackClerk.ts new file mode 100644 index 000000000..010244088 --- /dev/null +++ b/src/server/api/routers/users/rollbackClerk.ts @@ -0,0 +1,30 @@ +import { clerkClient } from "@clerk/nextjs/server" +import { TRPCError } from "@trpc/server" +import { Ratelimit } from "@upstash/ratelimit" +import { z } from "zod" + +import { publicRatedProcedure } from "~/server/api/trpc" + +export const rollbackClerk = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) + .input( + z.object({ + clerk_id: z + .string() + .min(2, { + message: "Clerk ID is required", + }) + .trim(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await clerkClient().users.deleteUser(input.clerk_id) + } catch (err) { + console.error("Failed to delete Clerk user", err) + throw new TRPCError({ + code: "NOT_FOUND", + message: `No Clerk customer found with id: ${input.clerk_id} during rollback`, + }) + } + return { success: true } + }) From bfecf4f44e4ee3c62529f7a3a6cd35bfa7418982 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Mon, 18 Aug 2025 00:13:30 +0800 Subject: [PATCH 02/12] fix type error --- src/server/api/routers/users/create.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/server/api/routers/users/create.ts b/src/server/api/routers/users/create.ts index 64daf51e6..ba69b3538 100644 --- a/src/server/api/routers/users/create.ts +++ b/src/server/api/routers/users/create.ts @@ -100,7 +100,7 @@ export const create = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) }) .returning() return user - } catch (err) { + } catch (err: unknown) { if (createdSquareCustomerId) { try { await squareClient.customersApi.deleteCustomer(createdSquareCustomerId) @@ -115,9 +115,19 @@ export const create = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) console.error("Failed to cleanup Clerk user", cleanupErr) } } + let message = "Unknown error" + if (err instanceof TRPCError) { + message = err.message + } else if (err instanceof Error) { + message = err.message + } else if (typeof err === "string") { + message = err + } else { + message = JSON.stringify(err) + } throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: `Failed to register user: ${err}`, + message: `Failed to register user: ${message}`, }) } }) From 38f93242b16f1fc2351dd8ead5b152ca64dae82e Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Sun, 31 Aug 2025 21:14:31 +0800 Subject: [PATCH 03/12] change email verification method to verification code --- src/app/verification/page.tsx | 36 ++++--------------- src/server/api/routers/users/index.ts | 2 -- src/server/api/routers/users/rollbackClerk.ts | 30 ---------------- 3 files changed, 6 insertions(+), 62 deletions(-) delete mode 100644 src/server/api/routers/users/rollbackClerk.ts diff --git a/src/app/verification/page.tsx b/src/app/verification/page.tsx index 647ac34c1..683c6a87f 100644 --- a/src/app/verification/page.tsx +++ b/src/app/verification/page.tsx @@ -1,42 +1,25 @@ "use client" -import { useClerk, useUser } from "@clerk/nextjs" +import { useClerk } from "@clerk/nextjs" import { EmailLinkErrorCode, isEmailLinkError } from "@clerk/nextjs/errors" -import { useRouter } from "next/navigation" import * as React from "react" -import { api } from "~/trpc/react" - export default function Verification() { const [verificationStatus, setVerificationStatus] = React.useState("loading") const { handleEmailLinkVerification } = useClerk() - const { user } = useUser() - const rollbackMutation = api.users.rollbackClerk.useMutation() - const { data: dbUser, isLoading: isDbUserLoading } = api.users.getCurrent.useQuery() - const router = useRouter() + React.useEffect(() => { const verify = async () => { try { - // TODO: redirect to dashboard if signing in, or to the create account page if signing up + // TODO redirect to dashboard if signing in, or to the create account page if signing up await handleEmailLinkVerification({}) // If we're not redirected at this point, it means // that the flow has completed on another device. - - if (isDbUserLoading) { - // still loading → do nothing yet - return - } - if (!dbUser) { - if (user?.id) { - await rollbackMutation.mutateAsync({ clerk_id: user.id }) - } - setVerificationStatus("dbFailed") - return - } else setVerificationStatus("verified") + setVerificationStatus("verified") } catch (err) { // Verification has failed. - let status = "failed" + // @ts-expect-error - Yes it does if (isEmailLinkError(err as Error) && err?.code === EmailLinkErrorCode.Expired) { status = "expired" @@ -47,7 +30,7 @@ export default function Verification() { verify().catch((e) => { console.error(e) }) - }, [handleEmailLinkVerification, router, user, dbUser, rollbackMutation]) + }, [handleEmailLinkVerification]) switch (verificationStatus) { case "loading": @@ -56,13 +39,6 @@ export default function Verification() { return
Verification link has expired!
case "failed": return
Verification failed!
- case "dbFailed": - return ( -
- Verification failed because the registration page was closed! Please DO NOT close the registration page while - verifying your email address! Please try again! -
- ) default: return (
diff --git a/src/server/api/routers/users/index.ts b/src/server/api/routers/users/index.ts index b5256ee22..27d864f8f 100644 --- a/src/server/api/routers/users/index.ts +++ b/src/server/api/routers/users/index.ts @@ -6,7 +6,6 @@ import { createTRPCRouter } from "~/server/api/trpc" import { create } from "./create" import { get } from "./get" import { getCurrent } from "./get-current" -import { rollbackClerk } from "./rollbackClerk" import { update } from "./update" import { updateSocials } from "./update-socials" @@ -16,5 +15,4 @@ export const usersRouter = createTRPCRouter({ get, update, updateSocials, - rollbackClerk, }) diff --git a/src/server/api/routers/users/rollbackClerk.ts b/src/server/api/routers/users/rollbackClerk.ts deleted file mode 100644 index 010244088..000000000 --- a/src/server/api/routers/users/rollbackClerk.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { clerkClient } from "@clerk/nextjs/server" -import { TRPCError } from "@trpc/server" -import { Ratelimit } from "@upstash/ratelimit" -import { z } from "zod" - -import { publicRatedProcedure } from "~/server/api/trpc" - -export const rollbackClerk = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) - .input( - z.object({ - clerk_id: z - .string() - .min(2, { - message: "Clerk ID is required", - }) - .trim(), - }), - ) - .mutation(async ({ ctx, input }) => { - try { - await clerkClient().users.deleteUser(input.clerk_id) - } catch (err) { - console.error("Failed to delete Clerk user", err) - throw new TRPCError({ - code: "NOT_FOUND", - message: `No Clerk customer found with id: ${input.clerk_id} during rollback`, - }) - } - return { success: true } - }) From 55183e62e77f8d3ce8093dc3532cfdfa4fc8cada Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Mon, 1 Sep 2025 00:06:17 +0800 Subject: [PATCH 04/12] add sso feature --- .../create-account/complete-profile/page.tsx | 635 ++++++++++++++++++ src/app/join/page.tsx | 66 +- src/app/sso-callback/page.tsx | 3 + src/server/api/routers/users/get.ts | 14 + src/server/api/routers/users/index.ts | 3 +- 5 files changed, 703 insertions(+), 18 deletions(-) create mode 100644 src/app/create-account/complete-profile/page.tsx create mode 100644 src/app/sso-callback/page.tsx diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx new file mode 100644 index 000000000..ea48b102e --- /dev/null +++ b/src/app/create-account/complete-profile/page.tsx @@ -0,0 +1,635 @@ +"use client" + +import { useSignIn, useUser } from "@clerk/nextjs" +import { zodResolver } from "@hookform/resolvers/zod" +import { track } from "@vercel/analytics/react" +import { set } from "date-fns" +import Link from "next/link" +import { useRouter, useSearchParams } from "next/navigation" +import * as React from "react" +import { useForm } from "react-hook-form" +import { siDiscord } from "simple-icons" +import * as z from "zod" + +// import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar" +// import GithubHeatmap from "../_components/github-heatmap" +import OnlinePaymentForm from "~/components/payment/online" +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert" +import { Button } from "~/components/ui/button" +import { Checkbox } from "~/components/ui/checkbox" +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form" +import { Input } from "~/components/ui/input" +import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs" +import { toast } from "~/components/ui/use-toast" + +import { PRONOUNS, SITE_URL, UNIVERSITIES } from "~/lib/constants" +import { cn, getIsMembershipOpen } from "~/lib/utils" +import { type User } from "~/server/db/types" +import { api } from "~/trpc/react" + +import DetailsBlock from "../details" +import PaymentBlock from "../payment" + +type ActiveView = "form" | "payment" + +const formSchema = z + .object({ + name: z.string().min(2, { + message: "Name is required", + }), + preferred_name: z.string().min(2, { + message: "Preferred name is required", + }), + email: z + .string() + .email({ + message: "Invalid email address", + }) + .min(2, { + message: "Email is required", + }), + pronouns: z.string().min(2, { + message: "Pronouns are required", + }), + isUWA: z.boolean(), + student_number: z.string().optional(), + uni: z.string().optional(), + github: z.string().optional(), + discord: z.string().optional(), + subscribe: z.boolean(), + }) + .refine(({ isUWA, student_number }) => !Boolean(isUWA) || student_number, { + message: "Student number is required", + path: ["student_number"], + }) + .refine(({ isUWA, student_number = "" }) => !Boolean(isUWA) || /^\d{8}$/.test(student_number), { + message: "Student number must be 8 digits", + path: ["student_number"], + }) + .refine(({ isUWA, uni = "" }) => Boolean(isUWA) || uni || uni === "other", { + message: "University is required", + path: ["uni"], + }) + +type FormSchema = z.infer + +const defaultValues = { + name: "", + preferred_name: "", + pronouns: PRONOUNS[0].value, + isUWA: true, + student_number: "", + uni: UNIVERSITIES[0].value, + github: "", + discord: "", + subscribe: true, +} + +export default function CompleteProfile() { + const [activeView, setActiveView] = React.useState("form") + const [loadingSkipPayment, setLoadingSkipPayment] = React.useState(false) + const [user, setUser] = React.useState() + const router = useRouter() + + const { isLoaded, user: clerkUser } = useUser() + const [step, setStep] = React.useState<"submitForm" | "enterCode" | "verifying">("submitForm") + if (!isLoaded) return null + + const clerk_id = clerkUser?.id + const email = clerkUser?.primaryEmailAddress?.emailAddress ?? "" + const name = clerkUser?.firstName ? `${clerkUser.firstName} ${clerkUser.lastName ?? ""}`.trim() : "" + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + ...defaultValues, + email: email, + name: name, + }, + }) + const { getValues, setError } = form + + const utils = api.useUtils() + const createUser = api.users.create.useMutation({ + onError: (error) => { + toast({ + variant: "destructive", + title: "Failed to create database user", + description: error.message, + }) + }, + }) + const { data: cards } = api.payments.getCards.useQuery(undefined, { + enabled: !!user, + staleTime: Infinity, // this is ok because this will be the first time ever the user will fetch cards, no risk of it being out of date + }) + + const onSubmit = async (values: FormSchema) => { + if (!isLoaded) return + + if (values.github !== "") { + const { status: githubStatus } = await fetch(`https://api.github.com/users/${values.github}`) + + if (githubStatus !== 200) { + toast({ + variant: "destructive", + title: "Github username not found", + description: "The Github username not found. Please try again.", + }) + setError("github", { + type: "custom", + message: "Github username not found", + }) + return + } + } + setStep("verifying") + if (process.env.NEXT_PUBLIC_VERCEL_ENV === "production") track("created-account") + + const userData: Omit = { + name: values.name, + preferred_name: values.preferred_name, + email: values.email, + pronouns: values.pronouns, + github: values.github, + discord: values.discord, + subscribe: values.subscribe, + } + + if (values.isUWA) { + userData.student_number = values.student_number + userData.uni = "UWA" + } else { + userData.uni = values.uni + } + + try { + if (clerkUser) + await clerkUser.update({ + firstName: values.name, // Use full name as first name + unsafeMetadata: { + preferred_name: values.preferred_name, + pronouns: values.pronouns, + github: values.github, + discord: values.discord, + subscribe: values.subscribe, + }, + }) + + const user = await createUser.mutateAsync({ + clerk_id: clerk_id ?? "", + ...userData, + }) + setUser(user) + + setActiveView("payment") + } catch (error) { + console.error("Signup error", error) + toast({ + variant: "destructive", + title: "Failed to create user", + description: `An error occurred while trying to create user ${error ?? ""}. Please try again later.`, + }) + } + } + + const handleAfterOnlinePayment = async (paymentID: string) => { + if (!user) { + toast({ + variant: "destructive", + title: "Unable to verify user", + description: "We were unable to verify if the user was created.", + }) + return + } + + user.role = "member" + utils.users.getCurrent.setData(undefined, user) + router.push("/dashboard") + } + + const handleSkipPayment = async () => { + if (user) { + setLoadingSkipPayment(true) + try { + utils.users.getCurrent.setData(undefined, user) + router.push("/dashboard") + } catch (error) { + console.error(error) + } finally { + setLoadingSkipPayment(false) + } + } + } + + return ( +
+ + mail + {activeView === "form" ? ( + step === "submitForm" ? ( + <> + New user detected! + + We couldn't find an account with that email address so you can create a new account here. If you + think it was a mistake,{" "} + + + + ) : ( + <> + Verifying your email... + Thanks for your patience! We are verifying your email. + + ) + ) : ( + <> + User created! + + Now you can proceed to payment or skip for now and complete later from your dashboard. + + + )} + +
+ {activeView === "form" ? ( + +
+

Personal details

+

Fields marked with * are required.

+
+ ( + + +

Email address

+

*

+
+ + + + +
+ )} + /> + ( + + +

Full name

*

+
+ + + + + We use your full name for internal committee records and official correspondence + + +
+ )} + /> + ( + + +

Preferred name

+

*

+
+ + + + This is how we normally refer to you + +
+ )} + /> + ( + + +

Pronouns

+

*

+
+ + + {PRONOUNS.map(({ label, value }) => ( + + + + + {label} + + ))} + + + + + {Boolean(PRONOUNS.find(({ value: val }) => val === field.value)) ? ( + Other + ) : ( + + + + )} + + + + +
+ )} + /> +
+ ( + + + + + I'm a UWA student + + + )} + /> + ( + + +

UWA student number

+

*

+
+ + + + +
+ )} + /> + ( + + University + + + {UNIVERSITIES.map(({ label, value }) => ( + + + + + {label} + + ))} + + + + + {Boolean(UNIVERSITIES.find(({ value: val }) => val === field.value)) ? ( + Other university + ) : ( + + + + )} + + + + + + )} + /> +
+
+
+

Socials

+

+ These fields are optional but are required if you plan on applying for projects during the winter and + summer breaks. +

+ + + {siDiscord.title} + + + Join our Discord! + + You can join our Discord server at{" "} + + + +
+ ( + + Github username + + + + + Sign up at{" "} + + + + + )} + /> + ( + + Discord username + + + + + Sign up at{" "} + + + + + )} + /> +
+ ( + + + + + I wish to receive emails about future CFC events + + + )} + /> + + + ) : ( + + )} + + {activeView === "payment" ? ( +
+
+

Payment

+
+

+ Become a paying member of Coders for Causes for just $5 a year (ends on 31st Dec{" "} + {new Date().getFullYear()}). There are many benefits to becoming a member which include: +

+
    +
  • discounts to paid events such as industry nights
  • +
  • the ability to vote and run for committee positions
  • +
  • the ability to join our projects run during the winter and summer breaks.
  • +
+
+
+ {getIsMembershipOpen() ? ( + <> + + + + Online + + + In-person + + + +

+ Our online payment system is handled by{" "} + + . We do not store your card details but we do record the information Square provides us after + confirming your card. +

+ +
+ +

+ We accept cash and card payments in-person. We use{" "} + {" "} + Point-of-Sale terminals to accept card payments. Reach out to a committee member via our Discord or + a CFC event to pay in-person. A committee member will update your status as a member on payment + confirmation. +

+ +
+
+
+
+ +
+
+ Or +
+
+
+

Skipping payment

+
+

+ You can skip payment for now but you will miss out on the benefits mentioned above until you do. You + can always pay later by going to your account dashboard. +

+
+
+ + + ) : ( +

+ Memberships are temporarily closed for the new year. Please check back later. +

+ )} +
+ ) : ( + + )} + {/*
+
+
+ + + CN + +
+
+
+
+

{getValues().name}

+

+ {getValues().email} +

+
+ +
+
*/} +
+ ) +} diff --git a/src/app/join/page.tsx b/src/app/join/page.tsx index 7fdd94216..d9cc2d9a1 100644 --- a/src/app/join/page.tsx +++ b/src/app/join/page.tsx @@ -1,11 +1,14 @@ "use client" import { useSignIn } from "@clerk/nextjs" +import { SignIn } from "@clerk/nextjs" +import { type EmailLinkFactor } from "@clerk/types" import { zodResolver } from "@hookform/resolvers/zod" import { track } from "@vercel/analytics/react" import { useRouter } from "next/navigation" import { useEffect, useState } from "react" import { useForm } from "react-hook-form" +import { siGoogle } from "simple-icons" import * as z from "zod" import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert" @@ -17,6 +20,7 @@ import { toast } from "~/components/ui/use-toast" import type { ClerkError } from "~/lib/types" import { api } from "~/trpc/react" +const googleIcon = { path: siGoogle.path, title: siGoogle.title } const formSchema = z.object({ email: z .string() @@ -127,6 +131,25 @@ export default function Join() { setStep("email") } } + const handleGoogleSignIn = async () => { + if (!isLoaded) return + + try { + // Start the Google OAuth flow + await signIn.authenticateWithRedirect({ + strategy: "oauth_google", + redirectUrl: `${window.location.origin}/sso-callback`, + redirectUrlComplete: "/dashboard", // after everything succeeds + }) + } catch (error) { + console.error("Google sign-in failed", error) + toast({ + variant: "destructive", + title: "Google sign-in failed", + description: `${error}`, + }) + } + } return (
@@ -156,24 +179,33 @@ export default function Join() { )} {step === "initial" ? ( -
- ( - - Email address - - - - - - )} - /> - + + - + ) : (
Enter one-time code from your email diff --git a/src/app/sso-callback/page.tsx b/src/app/sso-callback/page.tsx new file mode 100644 index 000000000..99bdfb34f --- /dev/null +++ b/src/app/sso-callback/page.tsx @@ -0,0 +1,3 @@ +export default function SsoCallback() { + return
Signing you in...
+} diff --git a/src/server/api/routers/users/get.ts b/src/server/api/routers/users/get.ts index de0a28cc3..e9ce04903 100644 --- a/src/server/api/routers/users/get.ts +++ b/src/server/api/routers/users/get.ts @@ -19,3 +19,17 @@ export const get = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) return user }) + +export const getByEmail = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) + .input(z.string().email()) + .query(async ({ ctx, input }) => { + const user = await ctx.db.query.User.findFirst({ + where: eq(User.email, input), + }) + + if (!user) { + throw new TRPCError({ code: "NOT_FOUND", message: `User with email: ${input} does not exist` }) + } + + return user + }) diff --git a/src/server/api/routers/users/index.ts b/src/server/api/routers/users/index.ts index 27d864f8f..e535a13a0 100644 --- a/src/server/api/routers/users/index.ts +++ b/src/server/api/routers/users/index.ts @@ -4,7 +4,7 @@ import { env } from "~/env" import { createTRPCRouter } from "~/server/api/trpc" import { create } from "./create" -import { get } from "./get" +import { get, getByEmail } from "./get" import { getCurrent } from "./get-current" import { update } from "./update" import { updateSocials } from "./update-socials" @@ -15,4 +15,5 @@ export const usersRouter = createTRPCRouter({ get, update, updateSocials, + getByEmail, }) From 98f5b634554ea858f1e2a5b437f083e7a952fa21 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Tue, 2 Sep 2025 00:36:01 +0800 Subject: [PATCH 05/12] add sso redirect --- src/app/create-account/complete-profile/page.tsx | 4 +++- src/app/join/page.tsx | 2 +- src/app/sso-callback/page.tsx | 11 +++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx index ea48b102e..44e8b279f 100644 --- a/src/app/create-account/complete-profile/page.tsx +++ b/src/app/create-account/complete-profile/page.tsx @@ -93,7 +93,7 @@ export default function CompleteProfile() { const router = useRouter() const { isLoaded, user: clerkUser } = useUser() - const [step, setStep] = React.useState<"submitForm" | "enterCode" | "verifying">("submitForm") + const [step, setStep] = React.useState<"submitForm" | "verifying">("submitForm") if (!isLoaded) return null const clerk_id = clerkUser?.id @@ -173,6 +173,8 @@ export default function CompleteProfile() { github: values.github, discord: values.discord, subscribe: values.subscribe, + university: values.uni, + student_number: values.student_number, }, }) diff --git a/src/app/join/page.tsx b/src/app/join/page.tsx index d9cc2d9a1..28c79e0cb 100644 --- a/src/app/join/page.tsx +++ b/src/app/join/page.tsx @@ -139,7 +139,7 @@ export default function Join() { await signIn.authenticateWithRedirect({ strategy: "oauth_google", redirectUrl: `${window.location.origin}/sso-callback`, - redirectUrlComplete: "/dashboard", // after everything succeeds + redirectUrlComplete: `/dashboard`, // after everything succeeds }) } catch (error) { console.error("Google sign-in failed", error) diff --git a/src/app/sso-callback/page.tsx b/src/app/sso-callback/page.tsx index 99bdfb34f..69872352c 100644 --- a/src/app/sso-callback/page.tsx +++ b/src/app/sso-callback/page.tsx @@ -1,3 +1,10 @@ -export default function SsoCallback() { - return
Signing you in...
+import { AuthenticateWithRedirectCallback } from "@clerk/nextjs" + +export default function Page() { + return ( + <> +
Signing you in...
+ + + ) } From 5bad9fba8e6f879eb081b2c476d65f807cd77ee0 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Thu, 4 Sep 2025 00:47:09 +0800 Subject: [PATCH 06/12] edit middleware and rename a component --- src/app/not-found.tsx | 2 +- .../{profile/page.tsx => page-content.tsx} | 4 +-- src/app/profile/[id]/page.tsx | 4 +-- src/middleware.ts | 35 +++++++++++-------- 4 files changed, 25 insertions(+), 20 deletions(-) rename src/app/profile/[id]/{profile/page.tsx => page-content.tsx} (97%) diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index b9e4ffda1..71e7996bd 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -10,7 +10,7 @@ import { Button } from "~/components/ui/button" export default function NotFound() { return ( -
+
Not Found | Coders for Causes
¯\_(ツ)_/¯

404: Not Found

diff --git a/src/app/profile/[id]/profile/page.tsx b/src/app/profile/[id]/page-content.tsx similarity index 97% rename from src/app/profile/[id]/profile/page.tsx rename to src/app/profile/[id]/page-content.tsx index 280acbdd2..dca52fb45 100644 --- a/src/app/profile/[id]/profile/page.tsx +++ b/src/app/profile/[id]/page-content.tsx @@ -18,7 +18,7 @@ interface ProfilePageProps { currentUser: RouterOutputs["users"]["getCurrent"] } -const ProfilePage = ({ id, currentUser }: ProfilePageProps) => { +const ProfilePageContent = ({ id, currentUser }: ProfilePageProps) => { const [isEditing, setIsEditing] = useState(false) const { data: user, refetch } = api.users.get.useQuery(id) @@ -128,4 +128,4 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => { return } -export default ProfilePage +export default ProfilePageContent diff --git a/src/app/profile/[id]/page.tsx b/src/app/profile/[id]/page.tsx index 9612759d5..9e23c3e56 100644 --- a/src/app/profile/[id]/page.tsx +++ b/src/app/profile/[id]/page.tsx @@ -1,13 +1,13 @@ import NotFound from "~/app/not-found" import { api } from "~/trpc/server" -import ProfilePage from "./profile/page" +import ProfilePageContent from "./page-content" const Profile = async ({ params: { id } }: { params: { id: string } }) => { const currentUser = await api.users.getCurrent.query() if (currentUser) { - return + return } return diff --git a/src/middleware.ts b/src/middleware.ts index a3b66b9ad..07b10f0b6 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,4 +1,5 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" +import { el } from "date-fns/locale" import { eq } from "drizzle-orm" import { db } from "./server/db" @@ -7,30 +8,34 @@ import { User } from "./server/db/schema" const adminRoles = ["admin", "committee"] const isAdminPage = createRouteMatcher(["/dashboard/admin(.*)"]) -const isProtectedPage = createRouteMatcher(["/dashboard(.*)", "/profile/settings(.*)"]) -const isAuthPage = createRouteMatcher(["/join(.*)"]) +const isProtectedPage = createRouteMatcher(["/dashboard(.*)", "/profile(.*)"]) +const isAuthPage = createRouteMatcher(["/join(.*)", "/sso-callback(.*)"]) export default clerkMiddleware(async (auth, req) => { - const clerkId = auth().userId + const { userId, redirectToSignIn } = await auth() - if (isAdminPage(req) && clerkId) { - const user = await db.query.User.findFirst({ - where: eq(User.clerk_id, clerkId), - }) - - if (!adminRoles.includes(user?.role ?? "")) { - // non-existent clerk role so we go to 404 page cleanly - auth().protect({ - role: "lmfaooo", + if (isAdminPage(req)) { + if (userId) { + const user = await db.query.User.findFirst({ + where: eq(User.clerk_id, userId), }) + + if (!adminRoles.includes(user?.role ?? "")) { + // non-existent clerk role so we go to 404 page cleanly + auth().protect({ + role: "lmfaooo", + }) + } + } else { + return redirectToSignIn() } } - if (isProtectedPage(req)) { - auth().protect() + if (isProtectedPage(req) && !userId) { + return redirectToSignIn() } - if (isAuthPage(req) && clerkId) { + if (isAuthPage(req) && userId) { return Response.redirect(new URL("/dashboard", req.url)) } }) From 6ca44a67c29b8647f3c2aeee5a619b96d4a04b99 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Thu, 4 Sep 2025 01:05:22 +0800 Subject: [PATCH 07/12] fix pieces --- .../create-account/complete-profile/page.tsx | 382 +++++++++--------- src/app/create-account/page.tsx | 2 +- src/middleware.ts | 2 +- 3 files changed, 194 insertions(+), 192 deletions(-) diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx index 44e8b279f..fce8d7831 100644 --- a/src/app/create-account/complete-profile/page.tsx +++ b/src/app/create-account/complete-profile/page.tsx @@ -190,7 +190,7 @@ export default function CompleteProfile() { toast({ variant: "destructive", title: "Failed to create user", - description: `An error occurred while trying to create user ${error ?? ""}. Please try again later.`, + description: `An error occurred while trying to create user. ${(error as { message?: string })?.message ?? ""}`, }) } } @@ -258,147 +258,80 @@ export default function CompleteProfile() { )} - {activeView === "form" ? ( - -
-

Personal details

-

Fields marked with * are required.

-
- ( - - -

Email address

-

*

-
- - - - -
- )} - /> - ( - - -

Full name

*

-
- - - - - We use your full name for internal committee records and official correspondence - - -
- )} - /> - ( - - -

Preferred name

-

*

-
- - - - This is how we normally refer to you - -
- )} - /> - ( - - -

Pronouns

-

*

-
- - - {PRONOUNS.map(({ label, value }) => ( - - - - - {label} - - ))} - - - - - {Boolean(PRONOUNS.find(({ value: val }) => val === field.value)) ? ( - Other - ) : ( - - - - )} - - - - -
- )} - /> -
+ + {activeView === "form" ? ( + <> +
+

Personal details

+

Fields marked with * are required.

+
( - + + +

Email address

+

*

+
- + - I'm a UWA student
)} /> ( - + -

UWA student number

+

Full name

*

+
+ + + + + We use your full name for internal committee records and official correspondence + + +
+ )} + /> + ( + + +

Preferred name

*

- + + This is how we normally refer to you
)} /> ( - - University + + +

Pronouns

+

*

+
- {UNIVERSITIES.map(({ label, value }) => ( + {PRONOUNS.map(({ label, value }) => ( @@ -410,11 +343,11 @@ export default function CompleteProfile() { - {Boolean(UNIVERSITIES.find(({ value: val }) => val === field.value)) ? ( - Other university + {Boolean(PRONOUNS.find(({ value: val }) => val === field.value)) ? ( + Other ) : ( - + )} @@ -424,93 +357,162 @@ export default function CompleteProfile() {
)} /> -
-
-
-

Socials

-

- These fields are optional but are required if you plan on applying for projects during the winter and - summer breaks. -

- - - {siDiscord.title} - - - Join our Discord! - - You can join our Discord server at{" "} - - - +
+ ( + + + + + I'm a UWA student + + + )} + /> + ( + + +

UWA student number

+

*

+
+ + + + +
+ )} + /> + ( + + University + + + {UNIVERSITIES.map(({ label, value }) => ( + + + + + {label} + + ))} + + + + + {Boolean(UNIVERSITIES.find(({ value: val }) => val === field.value)) ? ( + Other university + ) : ( + + + + )} + + + + + + )} + />
- ( - - Github username - - - - - Sign up at{" "} +
+
+

Socials

+

+ These fields are optional but are required if you plan on applying for projects during the winter + and summer breaks. +

+ + + {siDiscord.title} + + + Join our Discord! + + You can join our Discord server at{" "} - - - - )} - /> + + +
+ ( + + Github username + + + + + Sign up at{" "} + + + + + )} + /> + ( + + Discord username + + + + + Sign up at{" "} + + + + + )} + /> +
( - - Discord username + - + - - Sign up at{" "} - - + I wish to receive emails about future CFC events )} /> -
- ( - - - - - I wish to receive emails about future CFC events - - - )} - /> - - - ) : ( - - )} + + + ) : ( + + )} + {activeView === "payment" ? (
diff --git a/src/app/create-account/page.tsx b/src/app/create-account/page.tsx index 8ccaa6cac..a254c850b 100644 --- a/src/app/create-account/page.tsx +++ b/src/app/create-account/page.tsx @@ -244,7 +244,7 @@ export default function CreateAccount() { toast({ variant: "destructive", title: "Failed to create user", - description: `An error occurred while trying to create user. ${(error as { message?: string })?.message ?? ""}.`, + description: `An error occurred while trying to create user. ${(error as { message?: string })?.message ?? ""}`, }) setStep("enterCode") } diff --git a/src/middleware.ts b/src/middleware.ts index 07b10f0b6..232207090 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,7 +12,7 @@ const isProtectedPage = createRouteMatcher(["/dashboard(.*)", "/profile(.*)"]) const isAuthPage = createRouteMatcher(["/join(.*)", "/sso-callback(.*)"]) export default clerkMiddleware(async (auth, req) => { - const { userId, redirectToSignIn } = await auth() + const { userId, redirectToSignIn } = auth() if (isAdminPage(req)) { if (userId) { From 975a8e69e462b334bc7ec6e03ec8a8807c096881 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Thu, 4 Sep 2025 20:21:14 +0800 Subject: [PATCH 08/12] edit bits and pieces --- .../create-account/complete-profile/page.tsx | 393 +++++++++--------- src/app/create-account/page.tsx | 7 +- src/app/join/page.tsx | 2 +- src/middleware.ts | 1 - src/server/api/routers/users/create.ts | 109 ++--- 5 files changed, 241 insertions(+), 271 deletions(-) diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx index fce8d7831..bc820177d 100644 --- a/src/app/create-account/complete-profile/page.tsx +++ b/src/app/create-account/complete-profile/page.tsx @@ -1,9 +1,8 @@ "use client" -import { useSignIn, useUser } from "@clerk/nextjs" +import { useUser } from "@clerk/nextjs" import { zodResolver } from "@hookform/resolvers/zod" import { track } from "@vercel/analytics/react" -import { set } from "date-fns" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import * as React from "react" @@ -23,7 +22,7 @@ import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group" import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs" import { toast } from "~/components/ui/use-toast" -import { PRONOUNS, SITE_URL, UNIVERSITIES } from "~/lib/constants" +import { PRONOUNS, UNIVERSITIES } from "~/lib/constants" import { cn, getIsMembershipOpen } from "~/lib/utils" import { type User } from "~/server/db/types" import { api } from "~/trpc/react" @@ -143,6 +142,10 @@ export default function CompleteProfile() { return } } + window.scrollTo({ + top: 0, + behavior: "smooth", // smooth scrolling + }) setStep("verifying") if (process.env.NEXT_PUBLIC_VERCEL_ENV === "production") track("created-account") @@ -244,8 +247,8 @@ export default function CompleteProfile() { ) : ( <> - Verifying your email... - Thanks for your patience! We are verifying your email. + Creating your account... + Thanks for your patience! We are creating your account. ) ) : ( @@ -258,80 +261,147 @@ export default function CompleteProfile() { )}
- - {activeView === "form" ? ( - <> -
-

Personal details

-

Fields marked with * are required.

-
- ( - - -

Email address

-

*

-
- - - - -
- )} - /> + {activeView === "form" ? ( + +
+

Personal details

+

Fields marked with * are required.

+
+ ( + + +

Email address

+

*

+
+ + + + +
+ )} + /> + ( + + +

Full name

*

+
+ + + + + We use your full name for internal committee records and official correspondence + + +
+ )} + /> + ( + + +

Preferred name

+

*

+
+ + + + This is how we normally refer to you + +
+ )} + /> + ( + + +

Pronouns

+

*

+
+ + + {PRONOUNS.map(({ label, value }) => ( + + + + + {label} + + ))} + + + + + {Boolean(PRONOUNS.find(({ value: val }) => val === field.value)) ? ( + Other + ) : ( + + + + )} + + + + +
+ )} + /> +
( - - -

Full name

*

-
+ - + - - We use your full name for internal committee records and official correspondence - + I'm a UWA student )} /> ( - + -

Preferred name

+

UWA student number

*

- + - This is how we normally refer to you
)} /> ( - - -

Pronouns

-

*

-
+ + University - {PRONOUNS.map(({ label, value }) => ( + {UNIVERSITIES.map(({ label, value }) => ( @@ -343,11 +413,11 @@ export default function CompleteProfile() { - {Boolean(PRONOUNS.find(({ value: val }) => val === field.value)) ? ( - Other + {Boolean(UNIVERSITIES.find(({ value: val }) => val === field.value)) ? ( + Other university ) : ( - + )} @@ -357,162 +427,93 @@ export default function CompleteProfile() { )} /> -
- ( - - - - - I'm a UWA student - - - )} - /> - ( - - -

UWA student number

-

*

-
- - - - -
- )} - /> - ( - - University - - - {UNIVERSITIES.map(({ label, value }) => ( - - - - - {label} - - ))} - - - - - {Boolean(UNIVERSITIES.find(({ value: val }) => val === field.value)) ? ( - Other university - ) : ( - - - - )} - - - - - - )} - /> +
+
+
+

Socials

+

+ These fields are optional but are required if you plan on applying for projects during the winter and + summer breaks. +

+ + + {siDiscord.title} + + + Join our Discord! + + You can join our Discord server at{" "} + + +
-
-
-

Socials

-

- These fields are optional but are required if you plan on applying for projects during the winter - and summer breaks. -

- - - {siDiscord.title} - - - Join our Discord! - - You can join our Discord server at{" "} + ( + + Github username + + + + + Sign up at{" "} - - -
- ( - - Github username - - - - - Sign up at{" "} - - - - - )} - /> - ( - - Discord username - - - - - Sign up at{" "} - - - - - )} - /> -
+ + + + )} + /> ( - + + Discord username - + - I wish to receive emails about future CFC events + + Sign up at{" "} + + )} /> - - - ) : ( - - )} - +
+ ( + + + + + I wish to receive emails about future CFC events + + + )} + /> + + + ) : ( + + )} {activeView === "payment" ? (
diff --git a/src/app/create-account/page.tsx b/src/app/create-account/page.tsx index a254c850b..a71f1295a 100644 --- a/src/app/create-account/page.tsx +++ b/src/app/create-account/page.tsx @@ -180,7 +180,7 @@ export default function CreateAccount() { setStep("enterCode") window.scrollTo({ top: 0, - behavior: "smooth", // smooth scrolling + behavior: "smooth", }) } catch (error) { console.error("Error sending OTP", error) @@ -194,7 +194,10 @@ export default function CreateAccount() { const onSubmit = async (values: FormSchema) => { if (!isLoaded) return - + window.scrollTo({ + top: 0, + behavior: "smooth", + }) setStep("verifying") if (process.env.NEXT_PUBLIC_VERCEL_ENV === "production") track("created-account") diff --git a/src/app/join/page.tsx b/src/app/join/page.tsx index 28c79e0cb..2e41c2bc1 100644 --- a/src/app/join/page.tsx +++ b/src/app/join/page.tsx @@ -146,7 +146,7 @@ export default function Join() { toast({ variant: "destructive", title: "Google sign-in failed", - description: `${error}`, + description: `${(error as { message?: string })?.message ?? ""}`, }) } } diff --git a/src/middleware.ts b/src/middleware.ts index 232207090..e65162f75 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,5 +1,4 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" -import { el } from "date-fns/locale" import { eq } from "drizzle-orm" import { db } from "./server/db" diff --git a/src/server/api/routers/users/create.ts b/src/server/api/routers/users/create.ts index ba69b3538..7ac2b5d33 100644 --- a/src/server/api/routers/users/create.ts +++ b/src/server/api/routers/users/create.ts @@ -52,82 +52,49 @@ export const create = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) }), ) .mutation(async ({ ctx, input }) => { - let createdSquareCustomerId: string | null = null - let createdClerkUserId: string | null = null + // TODO: wrap in a transaction + const { result, statusCode } = await squareClient.customersApi.createCustomer({ + idempotencyKey: randomUUID(), + givenName: input.preferred_name, + familyName: input.name, + emailAddress: input.email, + // TODO: convert to user ID (UUIDv7) + referenceId: input.clerk_id, + }) - try { - const { result, statusCode } = await squareClient.customersApi.createCustomer({ - idempotencyKey: randomUUID(), - givenName: input.preferred_name, - familyName: input.name, - emailAddress: input.email, - // TODO: convert to user ID (UUIDv7) - referenceId: input.clerk_id, + if (!result.customer?.id) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Failed to create square customer ${statusCode}`, + cause: JSON.stringify(result), }) + } - if (!result.customer?.id) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to create square customer ${statusCode}`, - cause: JSON.stringify(result), - }) - } - createdSquareCustomerId = result.customer.id - const clerkUser = await clerkClient().users.getUser(input.clerk_id) - - if (!clerkUser) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `Clerk user with id: ${input.clerk_id} does not exist`, - }) - } - createdClerkUserId = clerkUser.id + const clerkUser = await clerkClient().users.getUser(input.clerk_id) - const [user] = await ctx.db - .insert(User) - .values({ - clerk_id: input.clerk_id, - name: input.name, - preferred_name: input.preferred_name, - email: input.email, - pronouns: input.pronouns, - student_number: input.student_number, - university: input.uni, - github: input.github, - discord: input.discord, - subscribe: input.subscribe ?? true, - square_customer_id: result.customer.id, - }) - .returning() - return user - } catch (err: unknown) { - if (createdSquareCustomerId) { - try { - await squareClient.customersApi.deleteCustomer(createdSquareCustomerId) - } catch (cleanupErr) { - console.error("Failed to cleanup Square customer", cleanupErr) - } - } - if (createdClerkUserId) { - try { - await clerkClient().users.deleteUser(createdClerkUserId) - } catch (cleanupErr) { - console.error("Failed to cleanup Clerk user", cleanupErr) - } - } - let message = "Unknown error" - if (err instanceof TRPCError) { - message = err.message - } else if (err instanceof Error) { - message = err.message - } else if (typeof err === "string") { - message = err - } else { - message = JSON.stringify(err) - } + if (!clerkUser) { throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to register user: ${message}`, + code: "NOT_FOUND", + message: `Clerk user with id: ${input.clerk_id} does not exist`, }) } + + const [user] = await ctx.db + .insert(User) + .values({ + clerk_id: input.clerk_id, + name: input.name, + preferred_name: input.preferred_name, + email: input.email, + pronouns: input.pronouns, + student_number: input.student_number, + university: input.uni, + github: input.github, + discord: input.discord, + subscribe: input.subscribe ?? true, + square_customer_id: result.customer.id, + }) + .returning() + + return user }) From 3c954d71269833a8322e5d736edbc7a6d61d1299 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Thu, 4 Sep 2025 21:14:14 +0800 Subject: [PATCH 09/12] edit calling useForm --- .../create-account/complete-profile/page.tsx | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx index bc820177d..7fccd3d1f 100644 --- a/src/app/create-account/complete-profile/page.tsx +++ b/src/app/create-account/complete-profile/page.tsx @@ -4,8 +4,8 @@ import { useUser } from "@clerk/nextjs" import { zodResolver } from "@hookform/resolvers/zod" import { track } from "@vercel/analytics/react" import Link from "next/link" -import { useRouter, useSearchParams } from "next/navigation" -import * as React from "react" +import { useRouter } from "next/navigation" +import { useEffect, useState } from "react" import { useForm } from "react-hook-form" import { siDiscord } from "simple-icons" import * as z from "zod" @@ -86,26 +86,39 @@ const defaultValues = { } export default function CompleteProfile() { - const [activeView, setActiveView] = React.useState("form") - const [loadingSkipPayment, setLoadingSkipPayment] = React.useState(false) - const [user, setUser] = React.useState() + const [activeView, setActiveView] = useState("form") + const [loadingSkipPayment, setLoadingSkipPayment] = useState(false) + const [user, setUser] = useState() const router = useRouter() const { isLoaded, user: clerkUser } = useUser() - const [step, setStep] = React.useState<"submitForm" | "verifying">("submitForm") - if (!isLoaded) return null - - const clerk_id = clerkUser?.id - const email = clerkUser?.primaryEmailAddress?.emailAddress ?? "" - const name = clerkUser?.firstName ? `${clerkUser.firstName} ${clerkUser.lastName ?? ""}`.trim() : "" + const [step, setStep] = useState<"submitForm" | "verifying">("submitForm") const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { ...defaultValues, - email: email, - name: name, }, }) + useEffect(() => { + async function updateForm() { + if (!clerkUser) return null + const email = clerkUser?.primaryEmailAddress?.emailAddress ?? "" + const name = clerkUser?.firstName ? `${clerkUser.firstName} ${clerkUser.lastName ?? ""}`.trim() : "" + const user = await Promise.resolve({ + ...defaultValues, + name: name, + email: email, + }) + + form.reset(user) + } + + updateForm() + }, [clerkUser]) + if (!isLoaded) return null + + const clerk_id = clerkUser?.id + const { getValues, setError } = form const utils = api.useUtils() From 99317b999d455e6015469e6bc58f7f204f0de410 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Thu, 4 Sep 2025 21:42:32 +0800 Subject: [PATCH 10/12] fix useEffect --- src/app/create-account/complete-profile/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx index 7fccd3d1f..a30436dd9 100644 --- a/src/app/create-account/complete-profile/page.tsx +++ b/src/app/create-account/complete-profile/page.tsx @@ -100,7 +100,7 @@ export default function CompleteProfile() { }, }) useEffect(() => { - async function updateForm() { + const updateForm = async () => { if (!clerkUser) return null const email = clerkUser?.primaryEmailAddress?.emailAddress ?? "" const name = clerkUser?.firstName ? `${clerkUser.firstName} ${clerkUser.lastName ?? ""}`.trim() : "" @@ -113,7 +113,7 @@ export default function CompleteProfile() { form.reset(user) } - updateForm() + updateForm().catch(console.error) }, [clerkUser]) if (!isLoaded) return null From 00d00a3ae9ca1f92a00c863188b4a92c501372d3 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Sat, 13 Sep 2025 00:23:40 +0800 Subject: [PATCH 11/12] add fetching updated logged-in user --- src/app/create-account/complete-profile/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx index a30436dd9..dde7006a5 100644 --- a/src/app/create-account/complete-profile/page.tsx +++ b/src/app/create-account/complete-profile/page.tsx @@ -223,6 +223,7 @@ export default function CompleteProfile() { user.role = "member" utils.users.getCurrent.setData(undefined, user) + utils.users.getCurrent.invalidate() router.push("/dashboard") } @@ -231,6 +232,7 @@ export default function CompleteProfile() { setLoadingSkipPayment(true) try { utils.users.getCurrent.setData(undefined, user) + utils.users.getCurrent.invalidate() router.push("/dashboard") } catch (error) { console.error(error) From 0d2df553a817fa49caa9acdeef16fcedac7d2b69 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Sat, 13 Sep 2025 00:42:36 +0800 Subject: [PATCH 12/12] fix error --- .../create-account/complete-profile/page.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/app/create-account/complete-profile/page.tsx b/src/app/create-account/complete-profile/page.tsx index dde7006a5..ea99a150f 100644 --- a/src/app/create-account/complete-profile/page.tsx +++ b/src/app/create-account/complete-profile/page.tsx @@ -222,9 +222,18 @@ export default function CompleteProfile() { } user.role = "member" - utils.users.getCurrent.setData(undefined, user) - utils.users.getCurrent.invalidate() - router.push("/dashboard") + try { + utils.users.getCurrent.setData(undefined, user) + await utils.users.getCurrent.invalidate() + router.push("/dashboard") + } catch (error) { + console.error(error) + toast({ + variant: "destructive", + title: "Unable to update user", + description: `We were unable to update the user. ${(error as { message?: string })?.message ?? ""}`, + }) + } } const handleSkipPayment = async () => { @@ -232,10 +241,15 @@ export default function CompleteProfile() { setLoadingSkipPayment(true) try { utils.users.getCurrent.setData(undefined, user) - utils.users.getCurrent.invalidate() + await utils.users.getCurrent.invalidate() router.push("/dashboard") } catch (error) { console.error(error) + toast({ + variant: "destructive", + title: "Unable to skip payment", + description: `Error occurred when trying to skip payment.${(error as { message?: string })?.message ?? ""}`, + }) } finally { setLoadingSkipPayment(false) }