From aa9d48975ad963faa3033ca689202e1dce921c71 Mon Sep 17 00:00:00 2001 From: jjohngrey Date: Fri, 13 Mar 2026 10:18:41 -0700 Subject: [PATCH 1/8] Change email --- .../general/general-profile-section.tsx | 1 - .../hooks/use-general-profile-submit.ts | 40 ++++++++++++-- src/lib/auth/index.ts | 20 +++++++ .../emails/templates/request-change-email.tsx | 52 +++++++++++++++++++ src/server/services/jobService.ts | 2 +- 5 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 src/server/emails/templates/request-change-email.tsx diff --git a/src/components/settings/pages/profile/general/general-profile-section.tsx b/src/components/settings/pages/profile/general/general-profile-section.tsx index 2812ec84..a44eafcc 100644 --- a/src/components/settings/pages/profile/general/general-profile-section.tsx +++ b/src/components/settings/pages/profile/general/general-profile-section.tsx @@ -55,7 +55,6 @@ export function GeneralProfileSection({ control={control} label="Email" type="email" - disabled />
diff --git a/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts b/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts index 0eb7e7ef..03a88543 100644 --- a/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts +++ b/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts @@ -4,9 +4,35 @@ import { authClient } from "@/lib/auth/client"; import { getBetterAuthErrorMessage } from "@/lib/auth/extensions/get-better-auth-error"; import { useImageUpload } from "@/hooks/use-image-upload"; import type { GeneralProfileSchemaType } from "../schema"; +import type { Session } from "@/lib/auth"; + +async function emailSubmit(data: GeneralProfileSchemaType, session: Session) { + const nextEmail = data.email.trim(); + const currentEmail = session?.user?.email?.trim(); + if (nextEmail && currentEmail && nextEmail !== currentEmail) { + const changeEmailResult = await authClient.changeEmail({ + newEmail: nextEmail, + callbackURL: window.location.origin, + }); + + if (!changeEmailResult) { + throw new Error( + "Profile updated, but email change could not be started. Please try again.", + ); + } + + if (changeEmailResult.error) { + throw new Error(getBetterAuthErrorMessage(changeEmailResult.error.code)); + } + + toast.success( + `A request to change your email address has been sent to the ${nextEmail}. Please check your inbox to confirm the change.`, + ); + } +} export function useGeneralProfileSubmit() { - const { refetch: refetchSession } = authClient.useSession(); + const { data: session, refetch: refetchSession } = authClient.useSession(); const { uploadImage } = useImageUpload(); const mutation = useMutation({ @@ -23,16 +49,22 @@ export function useGeneralProfileSubmit() { } // If data.image is null/undefined, imageKey stays null (clear image) - const { error } = await authClient.updateUser({ + const updateResult = await authClient.updateUser({ name: data.firstName, lastName: data.lastName, image: imageKey, }); - if (error) { - throw new Error(getBetterAuthErrorMessage(error.code)); + if (!updateResult) { + throw new Error("Failed to update profile. Please try again."); + } + + if (updateResult.error) { + throw new Error(getBetterAuthErrorMessage(updateResult.error.code)); } + await emailSubmit(data, session); + await refetchSession(); }, onSuccess: () => { diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 4d5882da..24d303d8 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -16,6 +16,7 @@ import { } from "@/server/db/schema/auth"; import { user, volunteer } from "@/server/db/schema/user"; import { renderForgotPassword } from "@/server/emails/templates/forgot-password"; +import { renderRequestChangeEmail } from "@/server/emails/templates/request-change-email"; import { renderVerifyEmail } from "@/server/emails/templates/verify-email"; import { betterAuth, type BetterAuthOptions } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; @@ -37,6 +38,25 @@ export const auth = betterAuth({ }, lastName: { type: "string" }, }, + changeEmail: { + enabled: true, + }, + emailVerification: { + sendVerificationEmail: async ({ user, url, token }) => { + const scope = createRequestScope(); + const { emailService } = scope.cradle; + const { html, text } = await renderRequestChangeEmail({ + url, + userName: user.name, + }); + await emailService.send( + user.email, + "Confirm your email address change", + text, + html, + ); + } + } }, database: drizzleAdapter(db, { provider: "pg", diff --git a/src/server/emails/templates/request-change-email.tsx b/src/server/emails/templates/request-change-email.tsx new file mode 100644 index 00000000..3e53e657 --- /dev/null +++ b/src/server/emails/templates/request-change-email.tsx @@ -0,0 +1,52 @@ +import { Button, Heading, Section, Text } from "@react-email/components"; +import { EmailLayout } from "./components/email-layout"; +import { renderEmail } from "../render"; + +interface RequestChangeEmailProps { + url: string; + userName?: string; +} + +export function RequestChangeEmail({ + url, + userName, +}: RequestChangeEmailProps) { + return ( + + + Update Your Email Address + + + Hi{userName ? ` ${userName}` : ""}, + + + We received a request to change the email address for your account to this inbox. Use the + button below to confirm the change. This link is only valid for a + limited time. + +
+ +
+ + Or copy and paste this link: {url} + + + If you did not request a change, you can disregard this email. + +
+ ); +} + +export default RequestChangeEmail; + +export function renderRequestChangeEmail(props: RequestChangeEmailProps) { + return renderEmail(); +} diff --git a/src/server/services/jobService.ts b/src/server/services/jobService.ts index 3d3dde7e..a3eec152 100644 --- a/src/server/services/jobService.ts +++ b/src/server/services/jobService.ts @@ -357,7 +357,7 @@ export class JobService implements IJobService { ...definition.retryOpts, }); } - + const worker = async (job: any) => { const jobItems = Array.isArray(job) ? job : [job]; From 5aa6691670492b414d751527afd458fe3a60f3a3 Mon Sep 17 00:00:00 2001 From: jjohngrey Date: Fri, 13 Mar 2026 10:30:47 -0700 Subject: [PATCH 2/8] Make toast messages ordering clearer --- .../profile/general/hooks/use-general-profile-submit.ts | 9 +++++---- src/lib/auth/index.ts | 8 +++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts b/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts index 03a88543..ebe28bba 100644 --- a/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts +++ b/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts @@ -6,7 +6,10 @@ import { useImageUpload } from "@/hooks/use-image-upload"; import type { GeneralProfileSchemaType } from "../schema"; import type { Session } from "@/lib/auth"; -async function emailSubmit(data: GeneralProfileSchemaType, session: Session) { +async function emailSubmit( + data: GeneralProfileSchemaType, + session: Session | null, +) { const nextEmail = data.email.trim(); const currentEmail = session?.user?.email?.trim(); if (nextEmail && currentEmail && nextEmail !== currentEmail) { @@ -62,14 +65,12 @@ export function useGeneralProfileSubmit() { if (updateResult.error) { throw new Error(getBetterAuthErrorMessage(updateResult.error.code)); } + toast.success("Your profile has been successfully updated!"); await emailSubmit(data, session); await refetchSession(); }, - onSuccess: () => { - toast.success("Your profile has been successfully updated!"); - }, onError: (error: Error) => { toast.error(error.message); }, diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 24d303d8..c5479155 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -40,9 +40,7 @@ export const auth = betterAuth({ }, changeEmail: { enabled: true, - }, - emailVerification: { - sendVerificationEmail: async ({ user, url, token }) => { + sendChangeEmailConfirmation: async ({ user, url }) => { const scope = createRequestScope(); const { emailService } = scope.cradle; const { html, text } = await renderRequestChangeEmail({ @@ -55,8 +53,8 @@ export const auth = betterAuth({ text, html, ); - } - } + }, + }, }, database: drizzleAdapter(db, { provider: "pg", From 8965d873765e52c964b6a6e40a7db02377e48ad3 Mon Sep 17 00:00:00 2001 From: jjohngrey Date: Fri, 13 Mar 2026 10:35:07 -0700 Subject: [PATCH 3/8] fix build error --- src/lib/auth/index.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index c5479155..31bc1f72 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -40,7 +40,13 @@ export const auth = betterAuth({ }, changeEmail: { enabled: true, - sendChangeEmailConfirmation: async ({ user, url }) => { + sendChangeEmailConfirmation: async ({ + user, + url, + }: { + user: { name: string; email: string }; + url: string; + }) => { const scope = createRequestScope(); const { emailService } = scope.cradle; const { html, text } = await renderRequestChangeEmail({ @@ -74,7 +80,7 @@ export const auth = betterAuth({ databaseHooks: { user: { create: { - after: async (user) => { + after: async (user: { role: Role; id: string }) => { switch (user.role) { case Role.volunteer: await db.insert(volunteer).values({ userId: user.id }); @@ -92,7 +98,13 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: true, - sendResetPassword: async ({ user, url }) => { + sendResetPassword: async ({ + user, + url, + }: { + user: { name: string; email: string }; + url: string; + }) => { const scope = createRequestScope(); const { emailService } = scope.cradle; const { html, text } = await renderForgotPassword({ @@ -104,7 +116,13 @@ export const auth = betterAuth({ }, emailVerification: { sendOnSignUp: true, - sendVerificationEmail: async ({ user, url }) => { + sendVerificationEmail: async ({ + user, + url, + }: { + user: { name: string; email: string }; + url: string; + }) => { const scope = createRequestScope(); const { emailService } = scope.cradle; const { html, text } = await renderVerifyEmail({ From b018b2459bf3e1dec5c319e3db204f0651c52a59 Mon Sep 17 00:00:00 2001 From: jjohngrey Date: Fri, 13 Mar 2026 12:03:38 -0700 Subject: [PATCH 4/8] Clarify toast messages --- .../hooks/use-general-profile-submit.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts b/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts index ebe28bba..dfbe3306 100644 --- a/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts +++ b/src/components/settings/pages/profile/general/hooks/use-general-profile-submit.ts @@ -29,7 +29,7 @@ async function emailSubmit( } toast.success( - `A request to change your email address has been sent to the ${nextEmail}. Please check your inbox to confirm the change.`, + `A request to change your email address has been sent to ${nextEmail}. Please check your inbox to confirm the change.`, ); } } @@ -65,11 +65,24 @@ export function useGeneralProfileSubmit() { if (updateResult.error) { throw new Error(getBetterAuthErrorMessage(updateResult.error.code)); } - toast.success("Your profile has been successfully updated!"); - - await emailSubmit(data, session); - + + let emailChangeErrorMessage: string | null = null; + try { + await emailSubmit(data, session); + } catch (error) { + emailChangeErrorMessage = + error instanceof Error + ? error.message + : "Failed to start email change request."; + } await refetchSession(); + if (emailChangeErrorMessage) { + toast.warning( + `Everything except your email was updated. ${emailChangeErrorMessage}`, + ); + return; + } + toast.success("Your profile has been successfully updated!"); }, onError: (error: Error) => { toast.error(error.message); From 60c8970893d9afb9364391e7cac41292b0e7cb5b Mon Sep 17 00:00:00 2001 From: jjohngrey Date: Fri, 13 Mar 2026 12:13:36 -0700 Subject: [PATCH 5/8] Add verification to email --- src/lib/auth/index.ts | 5 ++++- src/server/emails/templates/request-change-email.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 31bc1f72..e658e3ff 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -42,9 +42,11 @@ export const auth = betterAuth({ enabled: true, sendChangeEmailConfirmation: async ({ user, + newEmail, url, }: { user: { name: string; email: string }; + newEmail: string; url: string; }) => { const scope = createRequestScope(); @@ -52,10 +54,11 @@ export const auth = betterAuth({ const { html, text } = await renderRequestChangeEmail({ url, userName: user.name, + newEmail: newEmail, }); await emailService.send( user.email, - "Confirm your email address change", + `Confirm your email address change to ${newEmail}`, text, html, ); diff --git a/src/server/emails/templates/request-change-email.tsx b/src/server/emails/templates/request-change-email.tsx index 3e53e657..ef54349c 100644 --- a/src/server/emails/templates/request-change-email.tsx +++ b/src/server/emails/templates/request-change-email.tsx @@ -5,11 +5,13 @@ import { renderEmail } from "../render"; interface RequestChangeEmailProps { url: string; userName?: string; + newEmail: string; } export function RequestChangeEmail({ url, userName, + newEmail, }: RequestChangeEmailProps) { return ( @@ -23,9 +25,9 @@ export function RequestChangeEmail({ Hi{userName ? ` ${userName}` : ""}, - We received a request to change the email address for your account to this inbox. Use the + We received a request to update the email address for your account to {newEmail}. Use the button below to confirm the change. This link is only valid for a - limited time. + limited time. A new email address will be sent to {newEmail} to verify that email address.
+
+ + Or copy and paste this link: {url} + + + If you did not request this change, you can safely ignore this email. + +
+ ); +} + +export default VerifyNewEmail; + +export function renderVerifyNewEmail(props: VerifyNewEmailProps) { + return renderEmail(); +}