From 7e09d043c514febb981f8bf69c0da7dc78028d9b Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 25 Mar 2026 14:24:09 +0100 Subject: [PATCH 1/2] This commit communicates issues related to mail delivery to the user. --- app/lib/user-service.server.ts | 33 +++++++++------ app/routes/explore.forgot.tsx | 2 +- app/routes/explore.register.tsx | 56 ++++++++++++++++++++------ app/routes/settings.account.tsx | 20 ++++++++- public/locales/de/forgot-password.json | 2 +- public/locales/de/register.json | 6 ++- public/locales/de/settings.json | 2 + public/locales/en/forgot-password.json | 2 +- public/locales/en/register.json | 6 ++- public/locales/en/settings.json | 2 + 10 files changed, 100 insertions(+), 31 deletions(-) diff --git a/app/lib/user-service.server.ts b/app/lib/user-service.server.ts index 58e8b9499..de47d430b 100644 --- a/app/lib/user-service.server.ts +++ b/app/lib/user-service.server.ts @@ -1,5 +1,5 @@ import bcrypt from 'bcryptjs' -import { and, eq, gt, isNull } from 'drizzle-orm' +import { and, eq, gt } from 'drizzle-orm' import ConfirmEmailAddress, { subject as ConfirmEmailAddressSubject, } from 'emails/confirm-email' @@ -42,7 +42,7 @@ import { actionToken, user, type User } from '~/schema' const ONE_HOUR_MILLIS: number = 60 * 60 * 1000 type RegisterUserResult = - | { ok: true; user: User } + | { ok: true; user: User; emailSent: boolean } | { ok: false field: 'username' | 'email' | 'password' | 'tos' | 'form' @@ -175,21 +175,28 @@ export const registerUser = async ( const token = await issueEmailConfirmationToken(newUser.id) - await sendMail({ - recipientAddress: newUser.email, - recipientName: newUser.name, - subject: NewUserEmailSubject[lng], - body: NewUserEmail({ - user: { name: newUser.name }, - email: newUser.email, - token, - language: lng, - }), - }) + let emailSent = true + try { + await sendMail({ + recipientAddress: newUser.email, + recipientName: newUser.name, + subject: NewUserEmailSubject[lng], + body: NewUserEmail({ + user: { name: newUser.name }, + email: newUser.email, + token, + language: lng, + }), + }) + } catch (err) { + console.error('Failed to send registration confirmation email:', err) + emailSent = false + } return { ok: true, user: newUser, + emailSent, } } diff --git a/app/routes/explore.forgot.tsx b/app/routes/explore.forgot.tsx index dbfa9a274..642ae1465 100644 --- a/app/routes/explore.forgot.tsx +++ b/app/routes/explore.forgot.tsx @@ -61,7 +61,7 @@ export async function action({ request }: ActionFunctionArgs) { console.warn(err) return data( { - errors: { email: 'An error occurred. Please try again later.' }, + errors: { email: 'generic_error_try_again' }, success: false, }, { status: 500 }, diff --git a/app/routes/explore.register.tsx b/app/routes/explore.register.tsx index 66d1e1eec..47c2eb104 100644 --- a/app/routes/explore.register.tsx +++ b/app/routes/explore.register.tsx @@ -191,14 +191,18 @@ export async function action({ request }: ActionFunctionArgs) { }, { status: 400 }, ) -} + } + + if (!result.emailSent) { + return data({ emailDeliveryFailed: true }, { status: 200 }) + } return createUserSession({ - request, - userId: result.user.id, - remember: false, - redirectTo, -}) + request, + userId: result.user.id, + remember: false, + redirectTo, + }) } export const meta: MetaFunction = () => { @@ -215,15 +219,43 @@ export default function RegisterDialog() { const passwordRef = React.useRef(null) React.useEffect(() => { - if (actionData?.errors?.username) { - usernameRef.current?.focus() - } else if (actionData?.errors?.email) { - emailRef.current?.focus() - } else if (actionData?.errors?.password) { - passwordRef.current?.focus() + if (actionData && 'errors' in actionData) { + if (actionData.errors?.username) { + usernameRef.current?.focus() + } else if (actionData.errors?.email) { + emailRef.current?.focus() + } else if (actionData.errors?.password) { + passwordRef.current?.focus() + } } }, [actionData]) + if (actionData && 'emailDeliveryFailed' in actionData && actionData.emailDeliveryFailed) { + return ( +
+ +
+ + + + {t('account_created')} + {t('email_delivery_failed_description')} + + + + + + + +
+ ) + } + return (
Date: Wed, 25 Mar 2026 15:19:28 +0100 Subject: [PATCH 2/2] This commit fixes a TypeScript error caused by the union return type of the register action. --- app/routes/explore.register.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/app/routes/explore.register.tsx b/app/routes/explore.register.tsx index 47c2eb104..be237d91f 100644 --- a/app/routes/explore.register.tsx +++ b/app/routes/explore.register.tsx @@ -230,6 +230,8 @@ export default function RegisterDialog() { } }, [actionData]) + const actionErrors = actionData && 'errors' in actionData ? actionData.errors : undefined + if (actionData && 'emailDeliveryFailed' in actionData && actionData.emailDeliveryFailed) { return (
@@ -293,9 +295,9 @@ export default function RegisterDialog() {

{t('username_hint')}

- {actionData?.errors?.username && ( + {actionErrors?.username && (
- {t(actionData.errors.username)} + {t(actionErrors?.username)}
)}
@@ -310,12 +312,12 @@ export default function RegisterDialog() { autoFocus={true} name="email" autoComplete="email" - aria-invalid={actionData?.errors?.email ? true : undefined} + aria-invalid={actionErrors?.email ? true : undefined} aria-describedby="email-error" /> - {actionData?.errors?.email && ( + {actionErrors?.email && (
- {t(actionData.errors.email)} + {t(actionErrors?.email)}
)}
@@ -328,15 +330,15 @@ export default function RegisterDialog() { ref={passwordRef} name="password" autoComplete="new-password" - aria-invalid={actionData?.errors?.password ? true : undefined} + aria-invalid={actionErrors?.password ? true : undefined} aria-describedby="password-error" />

{t('password_hint')}

- {actionData?.errors?.password && ( + {actionErrors?.password && (
- {t(actionData.errors.password)} + {t(actionErrors?.password)}
)}
@@ -346,7 +348,7 @@ export default function RegisterDialog() { name="tosAccepted" type="checkbox" className="mt-1 h-4 w-4" - aria-invalid={actionData?.errors?.tosAccepted ? true : undefined} + aria-invalid={actionErrors?.tosAccepted ? true : undefined} aria-describedby="tos-error" /> - {actionData?.errors?.tosAccepted && ( + {actionErrors?.tosAccepted && (
- {t(actionData.errors.tosAccepted)} + {t(actionErrors?.tosAccepted)}
)}