From 64aeb5bff5c26a276296fd58d0d5ad0efa9dab6a Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 4 Feb 2026 09:20:52 +0100 Subject: [PATCH 01/12] This commit modifies the code to let the user know if their emails are confirmed. --- app/routes/settings.account.tsx | 14 +++- public/locales/de/settings.json | 2 + public/locales/en/settings.json | 118 ++++++++++++++++---------------- 3 files changed, 72 insertions(+), 62 deletions(-) diff --git a/app/routes/settings.account.tsx b/app/routes/settings.account.tsx index 26451ac2..2262f167 100644 --- a/app/routes/settings.account.tsx +++ b/app/routes/settings.account.tsx @@ -139,7 +139,7 @@ export default function EditUserProfilePage() { variant: 'success', }) } - }, [actionData, toast]) + }, [actionData, toast, t]) return (
@@ -168,9 +168,19 @@ export default function EditUserProfilePage() { name="email" placeholder={t('enter_email')} type="email" - readOnly={true} + // readOnly={true} defaultValue={userData?.email} /> + {/* Email confirmation status */} + {userData?.emailIsConfirmed ? ( +

+ {t('email_confirmed')} +

+ ) : ( +

+ {t('email_not_confirmed')} +

+ )}
diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json index d3ecacff..60fe8577 100644 --- a/public/locales/de/settings.json +++ b/public/locales/de/settings.json @@ -29,6 +29,8 @@ "enter_name": "Gib deinen Namen ein", "email": "E-Mail", "enter_email": "Gib deine E-Mail-Adresse ein", + "email_confirmed": "E-Mail bestätigt", + "email_not_confirmed": "E-Mail nicht bestätigt. Bitte überprüfe deinen Posteingang.", "language": "Sprache", "select_language": "Wähle die Sprache aus", "confirm_password": "Bestätige dein Passwort", diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 9f6935ac..a4f81859 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -1,61 +1,59 @@ { - "public_profile": "Public Profile", - "account": "Account", - "password": "Password", - "delete_account": "Delete Account", - "profile_updated": "Profile updated", - "profile_updated_description": "Your profile has been updated successfully.", - "something_went_wrong": "Something went wrong.", - "something_went_wrong_description": "Please try again later.", - "profile_settings": "Profile Settings", - "profile_settings_description": "This is how others see your profile.", - "username": "Username", - "if_public": "If your profile is public, this is how people will see you.", - "enter_username": "Enter your new username", - "if_activated_public_1": "If activated, others will be able to see your public", - "if_activated_public_2": "profile", - "if_activated_public_3": ".", - "change_profile_photo": "Change profile photo", - "save_changes": "Save changes", - "profile_photo": "Profile photo", - "save_photo": "Save Photo", - "reset": "Reset", - "change": "Change", - "invalid_password": "Invalid password", - "profile_successfully_updated": "Profile successfully updated.", - "account_information": "Account Information", - "update_basic_details": "Update your basic account details.", - "name": "Name", - "enter_name": "Enter your name", - "email": "Email", - "enter_email": "Enter your email", - "language": "Language", - "select_language": "Select language", - "confirm_password": "Confirm password", - "enter_current_password": "Enter your current password", - "try_again": "Please try again.", - "update_password": "Update Password", - "update_password_description": "Enter your current password and a new password to update your account password.", - "current_password": "Current Password", - "new_password": "New Password", - "enter_new_password": "Enter your new password", - "confirm_new_password": "Confirm your new password", - "password_required": "Password is required.", - "password_length": "Password must be at least 8 characters long.", - "email_not_found": "Email not found!", - "current_password_required": "Current password is required.", - "new_passwords_do_not_match": "New passwords do not match.", - "current_password_incorret": "Current password is incorrect.", - "password_updated_successfully": "Password updated successfully.", - "delete_account_description": "Deleting your account will permanently remove all of your data from our servers. This action cannot be undone.", - "enter_password": "Enter your password", - "device_security": { - "warning_deactive_auth": "If you disable device security, anyone can send measurement data that is assigned to your device!", - "page_title": "Change security settings", - "auth_enable_checkbox_label": "Enable authentication", - "api_key_label": "API Key", - "explanation_text": "Devices should use their API key shown on this page to authenticate requests sent to the openSenseMap API. This ensures that only authenticated devices update the state of the device on openSenseMap. The API key is appended to every request made to the API. More information can be found <2>in the docs.", - "generate_new_key_button": "Generate a new key", - "generate_new_key_warning": "Generating a new key will require you to update your device (e.g. change the sketch/ code). <1>This step can not be undone." - } -} + "public_profile": "Public Profile", + "account": "Account", + "password": "Password", + "delete_account": "Delete Account", + + "profile_updated": "Profile updated", + "profile_updated_description": "Your profile has been updated successfully.", + "something_went_wrong": "Something went wrong.", + "something_went_wrong_description": "Please try again later.", + "profile_settings": "Profile Settings", + "profile_settings_description": "This is how others see your profile.", + "username": "Username", + "if_public": "If your profile is public, this is how people will see you.", + "enter_username": "Enter your new username", + "if_activated_public_1": "If activated, others will be able to see your public", + "if_activated_public_2": "profile", + "if_activated_public_3": ".", + "change_profile_photo": "Change profile photo", + "save_changes": "Save changes", + + "profile_photo": "Profile photo", + "save_photo": "Save Photo", + "reset": "Reset", + "change": "Change", + + "invalid_password": "Invalid password", + "profile_successfully_updated": "Profile successfully updated.", + "account_information": "Account Information", + "update_basic_details": "Update your basic account details.", + "name": "Name", + "enter_name": "Enter your name", + "email": "Email", + "enter_email": "Enter your email", + "email_confirmed": "Email confirmed", + "email_not_confirmed": "Email not confirmed. Please check your inbox.", + "language": "Language", + "select_language": "Select language", + "confirm_password": "Confirm password", + "enter_current_password": "Enter your current password", + + "try_again": "Please try again.", + "update_password": "Update Password", + "update_password_description": "Enter your current password and a new password to update your account password.", + "current_password": "Current Password", + "new_password": "New Password", + "enter_new_password": "Enter your new password", + "confirm_new_password": "Confirm your new password", + "password_required": "Password is required.", + "password_length": "Password must be at least 8 characters long.", + "email_not_found": "Email not found!", + "current_password_required": "Current password is required.", + "new_passwords_do_not_match": "New passwords do not match.", + "current_password_incorret": "Current password is incorrect.", + "password_updated_successfully": "Password updated successfully.", + + "delete_account_description": "Deleting your account will permanently remove all of your data from our servers. This action cannot be undone.", + "enter_password": "Enter your password" +} \ No newline at end of file From 8b5e249873c3e48a359902a92383cf218836f2d4 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 4 Feb 2026 12:53:48 +0100 Subject: [PATCH 02/12] This commit will help the user to see if their email is confirmed or not and request to resend the confirmation mail if they are not confirmed. --- app/routes/settings.account.tsx | 59 +++++++++++++++++++++++++++------ public/locales/de/settings.json | 16 ++++----- public/locales/en/settings.json | 35 ++++++++++--------- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/app/routes/settings.account.tsx b/app/routes/settings.account.tsx index 2262f167..5c6ad88b 100644 --- a/app/routes/settings.account.tsx +++ b/app/routes/settings.account.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { Form, useActionData, + useFetcher, useLoaderData, data, redirect, @@ -54,7 +55,6 @@ export async function loader({ request }: LoaderFunctionArgs) { export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData() const { name, passwordUpdate, email, language } = Object.fromEntries(formData) - const errors = { name: name ? null : 'Invalid name', email: email ? null : 'Invalid email', @@ -115,16 +115,17 @@ export async function action({ request }: ActionFunctionArgs) { //***************************************************** export default function EditUserProfilePage() { - const userData = useLoaderData() // Load user data + const userData = useLoaderData() const actionData = useActionData() + const fetcher = useFetcher() const [lang, setLang] = useState(userData?.language || 'en_US') const [name, setName] = useState(userData?.name || '') - const passwordUpdRef = useRef(null) // For password update focus + const passwordUpdRef = useRef(null) const { toast } = useToast() const { t } = useTranslation('settings') + // Handle profile update responses useEffect(() => { - // Handle invalid password update error if (actionData && actionData?.errors?.passwordUpdate) { toast({ title: t('invalid_password'), @@ -132,7 +133,6 @@ export default function EditUserProfilePage() { }) passwordUpdRef.current?.focus() } - // Show success toast if profile updated if (actionData && !actionData?.errors?.passwordUpdate) { toast({ title: t('profile_successfully_updated'), @@ -141,6 +141,28 @@ export default function EditUserProfilePage() { } }, [actionData, toast, t]) + // Handle resend verification response + useEffect(() => { + if (fetcher.state === 'idle' && fetcher.data) { + if (fetcher.data.code === 'Ok') { + toast({ + title: t('verification_email_sent'), + variant: 'success', + }) + } else if (fetcher.data.code === 'UnprocessableContent') { + toast({ + title: t('email_already_confirmed'), + variant: 'default', + }) + } else { + toast({ + title: t('verification_email_failed'), + variant: 'destructive', + }) + } + } + }, [fetcher.state, fetcher.data, toast, t]) + return ( @@ -168,18 +190,34 @@ export default function EditUserProfilePage() { name="email" placeholder={t('enter_email')} type="email" - // readOnly={true} defaultValue={userData?.email} /> {/* Email confirmation status */} {userData?.emailIsConfirmed ? ( -

+

{t('email_confirmed')}

) : ( -

- {t('email_not_confirmed')} -

+
+

+ {t('email_not_confirmed')} +

+ + + +
)}
@@ -213,7 +251,6 @@ export default function EditUserProfilePage() { - + {fetcher.state === 'submitting' + ? t('sending') + : t('resend_verification')} +
)} diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json index a3ccad85..97a74b9f 100644 --- a/public/locales/de/settings.json +++ b/public/locales/de/settings.json @@ -33,7 +33,7 @@ "email_not_confirmed": "E-Mail nicht bestätigt. Bitte überprüfe deinen Posteingang.", "resend_verification": "Bestätigungs-E-Mail erneut senden", "sending": "Wird gesendet...", - "verification_email_sent": "Bestätigungs-E-Mail gesendet", + "verification_email_sent": "✔️ Bestätigungs-E-Mail gesendet", "verification_email_failed": "Bestätigungs-E-Mail konnte nicht gesendet werden.", "email_already_confirmed": "E-Mail ist bereits bestätigt.", "language": "Sprache", diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 67067da3..72107fe0 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -36,7 +36,7 @@ "email_not_confirmed": "Email not confirmed. Please check your inbox.", "resend_verification": "Resend confirmation email", "sending": "Sending...", - "verification_email_sent": "Verification email sent", + "verification_email_sent": "✔️ Verification email sent", "verification_email_failed": "Failed to send verification email", "email_already_confirmed": "Email is already confirmed", "language": "Language", From aa38fa3a9df928f9d9e750222dbfba7663a879aa Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 4 Feb 2026 16:10:47 +0100 Subject: [PATCH 04/12] This commit modifies the api to first check for seesion-based auth for web UI and falls back to JWT auth for API clients/tests. --- .../api.users.me.resend-email-confirmation.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/app/routes/api.users.me.resend-email-confirmation.ts b/app/routes/api.users.me.resend-email-confirmation.ts index 5ccb7e36..8260d4db 100644 --- a/app/routes/api.users.me.resend-email-confirmation.ts +++ b/app/routes/api.users.me.resend-email-confirmation.ts @@ -1,24 +1,36 @@ import { type ActionFunction, type ActionFunctionArgs } from 'react-router' import { getUserFromJwt } from '~/lib/jwt' +import { getUserFromJwt } from '~/lib/jwt' import { resendEmailConfirmation } from '~/lib/user-service.server' +import { getUserByEmail } from '~/models/user.server' import { StandardResponse } from '~/utils/response-utils' +import { getUserEmail } from '~/utils/session.server' export const action: ActionFunction = async ({ request, }: ActionFunctionArgs) => { try { - const jwtResponse = await getUserFromJwt(request) + // Try session-based auth first (for web UI) + const sessionEmail = await getUserEmail(request) + let user = sessionEmail ? await getUserByEmail(sessionEmail) : null - if (typeof jwtResponse === 'string') - return StandardResponse.forbidden( - 'Invalid JWT authorization. Please sign in to obtain new JWT.', - ) + // Fall back to JWT auth (for API clients) + if (!user) { + const jwtResponse = await getUserFromJwt(request) + if (!jwtResponse || typeof jwtResponse === 'string') { + return StandardResponse.forbidden( + 'Invalid authorization. Please sign in.', + ) + } + user = jwtResponse + } - const result = await resendEmailConfirmation(jwtResponse) - if (result === 'already_confirmed') + const result = await resendEmailConfirmation(user) + if (result === 'already_confirmed') { return StandardResponse.unprocessableContent( - `Email address ${jwtResponse.email} is already confirmed.`, + `Email address ${user.email} is already confirmed.`, ) + } return StandardResponse.ok({ code: 'Ok', From 52c48d2ffef22931f811af76ebf1e7962ac280cc Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 4 Feb 2026 16:23:52 +0100 Subject: [PATCH 05/12] This commit fixes the test failure associated with the test route tests\routes\api.users.me.boxes.$deviceId.spec.ts. --- tests/routes/api.users.me.boxes.$deviceId.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/routes/api.users.me.boxes.$deviceId.spec.ts b/tests/routes/api.users.me.boxes.$deviceId.spec.ts index 11569b2e..a5d815d7 100644 --- a/tests/routes/api.users.me.boxes.$deviceId.spec.ts +++ b/tests/routes/api.users.me.boxes.$deviceId.spec.ts @@ -10,7 +10,7 @@ import { type User } from '~/schema' const BOX_TEST_USER = generateTestUserCredentials() const BOX_TEST_USER_BOX = { - name: `${BOX_TEST_USER}s Box`, + name: `${BOX_TEST_USER.name}s Box`, // Fixed: was using BOX_TEST_USER instead of BOX_TEST_USER.name exposure: 'outdoor', expiresAt: null, tags: [], @@ -20,14 +20,13 @@ const BOX_TEST_USER_BOX = { mqttEnabled: false, ttnEnabled: false, } - -const OTHER_TEST_USER = generateTestUserCredentials() - // TODO Give the users some boxes to test with +const OTHER_TEST_USER = generateTestUserCredentials() describe('openSenseMap API Routes: /users', () => { describe('/me/boxes/:deviceId', () => { - describe('GET', async () => { + describe('GET', () => { + // Removed async here - not needed on describe let jwt: string = '' let otherJwt: string = '' let deviceId: string = '' @@ -53,7 +52,7 @@ describe('openSenseMap API Routes: /users', () => { const device = await createDevice(BOX_TEST_USER_BOX, (user as User).id) deviceId = device.id - }) + }, 30000) // Increase timeout to 30 seconds it('should let users retrieve one of their boxes with all fields', async () => { // Act: Get single box @@ -70,6 +69,7 @@ describe('openSenseMap API Routes: /users', () => { // Assert: Response for single box expect(singleBoxResponse.status).toBe(200) }) + it('should deny to retrieve a box of other user', async () => { // Arrange const forbiddenRequest = new Request( @@ -96,7 +96,7 @@ describe('openSenseMap API Routes: /users', () => { // delete the valid test user await deleteUserByEmail(BOX_TEST_USER.email) await deleteUserByEmail(OTHER_TEST_USER.email) - }) + }, 30000) // Increase timeout for cleanup too }) }) }) From 08daacd9c19c0d67287f7a7b426a412b7c45c8d5 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 4 Feb 2026 16:49:42 +0100 Subject: [PATCH 06/12] removes unwanted incrfesed timeout interval from the beforeall and afterall hooks in the file tests\routes\api.users.me.boxes.$deviceId.spec.ts --- tests/routes/api.users.me.boxes.$deviceId.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/routes/api.users.me.boxes.$deviceId.spec.ts b/tests/routes/api.users.me.boxes.$deviceId.spec.ts index a5d815d7..33b41471 100644 --- a/tests/routes/api.users.me.boxes.$deviceId.spec.ts +++ b/tests/routes/api.users.me.boxes.$deviceId.spec.ts @@ -26,7 +26,6 @@ const OTHER_TEST_USER = generateTestUserCredentials() describe('openSenseMap API Routes: /users', () => { describe('/me/boxes/:deviceId', () => { describe('GET', () => { - // Removed async here - not needed on describe let jwt: string = '' let otherJwt: string = '' let deviceId: string = '' @@ -52,7 +51,7 @@ describe('openSenseMap API Routes: /users', () => { const device = await createDevice(BOX_TEST_USER_BOX, (user as User).id) deviceId = device.id - }, 30000) // Increase timeout to 30 seconds + }) it('should let users retrieve one of their boxes with all fields', async () => { // Act: Get single box @@ -96,7 +95,7 @@ describe('openSenseMap API Routes: /users', () => { // delete the valid test user await deleteUserByEmail(BOX_TEST_USER.email) await deleteUserByEmail(OTHER_TEST_USER.email) - }, 30000) // Increase timeout for cleanup too + }) }) }) }) From 3d540ad8fbe13c3101140476cec704bcf1a50a24 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 11 Feb 2026 11:30:40 +0100 Subject: [PATCH 07/12] removes duplicate imports.. --- app/routes/api.users.me.resend-email-confirmation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/routes/api.users.me.resend-email-confirmation.ts b/app/routes/api.users.me.resend-email-confirmation.ts index 8260d4db..7720d2a5 100644 --- a/app/routes/api.users.me.resend-email-confirmation.ts +++ b/app/routes/api.users.me.resend-email-confirmation.ts @@ -1,6 +1,5 @@ import { type ActionFunction, type ActionFunctionArgs } from 'react-router' import { getUserFromJwt } from '~/lib/jwt' -import { getUserFromJwt } from '~/lib/jwt' import { resendEmailConfirmation } from '~/lib/user-service.server' import { getUserByEmail } from '~/models/user.server' import { StandardResponse } from '~/utils/response-utils' From 2fee5a48f5c3462063e8524c7422b376c28b71b2 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 11 Feb 2026 12:28:56 +0100 Subject: [PATCH 08/12] updates the settings route to use intent-based action for resend verification. Also updates the api to serve only jwt clients. --- .../api.users.me.resend-email-confirmation.ts | 25 ++-- app/routes/settings.account.tsx | 117 +++++++++++------- 2 files changed, 82 insertions(+), 60 deletions(-) diff --git a/app/routes/api.users.me.resend-email-confirmation.ts b/app/routes/api.users.me.resend-email-confirmation.ts index 7720d2a5..9ecda2b5 100644 --- a/app/routes/api.users.me.resend-email-confirmation.ts +++ b/app/routes/api.users.me.resend-email-confirmation.ts @@ -1,33 +1,24 @@ import { type ActionFunction, type ActionFunctionArgs } from 'react-router' import { getUserFromJwt } from '~/lib/jwt' import { resendEmailConfirmation } from '~/lib/user-service.server' -import { getUserByEmail } from '~/models/user.server' import { StandardResponse } from '~/utils/response-utils' -import { getUserEmail } from '~/utils/session.server' export const action: ActionFunction = async ({ request, }: ActionFunctionArgs) => { try { - // Try session-based auth first (for web UI) - const sessionEmail = await getUserEmail(request) - let user = sessionEmail ? await getUserByEmail(sessionEmail) : null - - // Fall back to JWT auth (for API clients) - if (!user) { - const jwtResponse = await getUserFromJwt(request) - if (!jwtResponse || typeof jwtResponse === 'string') { - return StandardResponse.forbidden( - 'Invalid authorization. Please sign in.', - ) - } - user = jwtResponse + // JWT auth only (session-based auth is handled in route actions) + const jwtResponse = await getUserFromJwt(request) + if (!jwtResponse || typeof jwtResponse === 'string') { + return StandardResponse.forbidden( + 'Invalid authorization. Please sign in.', + ) } - const result = await resendEmailConfirmation(user) + const result = await resendEmailConfirmation(jwtResponse) if (result === 'already_confirmed') { return StandardResponse.unprocessableContent( - `Email address ${user.email} is already confirmed.`, + `Email address ${jwtResponse.email} is already confirmed.`, ) } diff --git a/app/routes/settings.account.tsx b/app/routes/settings.account.tsx index 0eb07496..07344300 100644 --- a/app/routes/settings.account.tsx +++ b/app/routes/settings.account.tsx @@ -1,3 +1,4 @@ +import { CheckLine, OctagonAlert } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -30,6 +31,7 @@ import { SelectValue, } from '~/components/ui/select' import { useToast } from '~/components/ui/use-toast' +import { resendEmailConfirmation } from '~/lib/user-service.server' import { getUserByEmail, updateUserName, @@ -40,11 +42,9 @@ import { getUserEmail, getUserId } from '~/utils/session.server' //***************************************************** export async function loader({ request }: LoaderFunctionArgs) { - // If user is not logged in, redirect to home const userId = await getUserId(request) if (!userId) return redirect('/') - // Get user email and load user data const userEmail = await getUserEmail(request) invariant(userEmail, `Email not found!`) const userData = await getUserByEmail(userEmail) @@ -54,38 +54,71 @@ export async function loader({ request }: LoaderFunctionArgs) { //***************************************************** export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData() - const { name, passwordUpdate, email, language } = Object.fromEntries(formData) - const errors = { - name: name ? null : 'Invalid name', - email: email ? null : 'Invalid email', - passwordUpdate: passwordUpdate ? null : 'Password is required', + const intent = formData.get('intent') + + if (intent === 'resend-verification') { + const userEmail = await getUserEmail(request) + if (!userEmail) { + return data( + { intent: 'resend-verification', code: 'Forbidden' }, + { status: 403 }, + ) + } + const user = await getUserByEmail(userEmail) + if (!user) { + return data( + { intent: 'resend-verification', code: 'Forbidden' }, + { status: 403 }, + ) + } + + try { + const result = await resendEmailConfirmation(user) + if (result === 'already_confirmed') { + return data( + { intent: 'resend-verification', code: 'UnprocessableContent' }, + { status: 422 }, + ) + } + return data( + { intent: 'resend-verification', code: 'Ok' }, + { status: 200 }, + ) + } catch (err) { + console.warn(err) + return data( + { intent: 'resend-verification', code: 'Error' }, + { status: 500 }, + ) + } } + const { name, passwordUpdate, email, language } = Object.fromEntries(formData) + invariant(typeof name === 'string', 'name must be a string') invariant(typeof email === 'string', 'email must be a string') invariant(typeof passwordUpdate === 'string', 'password must be a string') invariant(typeof language === 'string', 'language must be a string') - // Validate password - if (errors.passwordUpdate) { + if (!passwordUpdate) { return data( { + intent: 'update-profile', errors: { name: null, email: null, - passwordUpdate: errors.passwordUpdate, + passwordUpdate: 'Password is required', }, - status: 400, }, { status: 400 }, ) } const user = await verifyLogin(email, passwordUpdate) - // If password is invalid if (!user) { return data( { + intent: 'update-profile', errors: { name: null, email: null, @@ -96,13 +129,12 @@ export async function action({ request }: ActionFunctionArgs) { ) } - // Update locale and name await updateUserlocale(email, language) await updateUserName(email, name) - // Return success response return data( { + intent: 'update-profile', errors: { name: null, email: null, @@ -117,7 +149,7 @@ export async function action({ request }: ActionFunctionArgs) { export default function EditUserProfilePage() { const userData = useLoaderData() const actionData = useActionData() - const fetcher = useFetcher() + const fetcher = useFetcher() const [lang, setLang] = useState(userData?.language || 'en_US') const [name, setName] = useState(userData?.name || '') const passwordUpdRef = useRef(null) @@ -126,14 +158,16 @@ export default function EditUserProfilePage() { // Handle profile update responses useEffect(() => { - if (actionData && actionData?.errors?.passwordUpdate) { + if (!actionData || actionData.intent !== 'update-profile') return + if (!('errors' in actionData)) return + + if (actionData.errors?.passwordUpdate) { toast({ title: t('invalid_password'), variant: 'destructive', }) passwordUpdRef.current?.focus() - } - if (actionData && !actionData?.errors?.passwordUpdate) { + } else { toast({ title: t('profile_successfully_updated'), variant: 'success', @@ -141,25 +175,19 @@ export default function EditUserProfilePage() { } }, [actionData, toast, t]) - // Handle resend verification response + // Handle resend verification response (via fetcher) useEffect(() => { - if (fetcher.state === 'idle' && fetcher.data) { - if (fetcher.data.code === 'Ok') { - toast({ - title: t('verification_email_sent'), - variant: 'success', - }) - } else if (fetcher.data.code === 'UnprocessableContent') { - toast({ - title: t('email_already_confirmed'), - variant: 'default', - }) - } else { - toast({ - title: t('verification_email_failed'), - variant: 'destructive', - }) - } + if (fetcher.state !== 'idle' || !fetcher.data) return + if (fetcher.data.intent !== 'resend-verification') return + if (!('code' in fetcher.data)) return + + const { code } = fetcher.data + if (code === 'Ok') { + toast({ title: t('verification_email_sent'), variant: 'success' }) + } else if (code === 'UnprocessableContent') { + toast({ title: t('email_already_confirmed'), variant: 'default' }) + } else { + toast({ title: t('verification_email_failed'), variant: 'destructive' }) } }, [fetcher.state, fetcher.data, toast, t]) @@ -192,15 +220,18 @@ export default function EditUserProfilePage() { type="email" defaultValue={userData?.email} /> - {/* Email confirmation status */} {userData?.emailIsConfirmed ? (

- {t('email_confirmed')} + + {t('email_confirmed')} +

) : (

- {t('email_not_confirmed')} + + {t('email_not_confirmed')} +