diff --git a/app/routes/api.users.me.resend-email-confirmation.ts b/app/routes/api.users.me.resend-email-confirmation.ts index 5ccb7e36..6ea63df3 100644 --- a/app/routes/api.users.me.resend-email-confirmation.ts +++ b/app/routes/api.users.me.resend-email-confirmation.ts @@ -9,16 +9,18 @@ export const action: ActionFunction = async ({ try { const jwtResponse = await getUserFromJwt(request) - if (typeof jwtResponse === 'string') + if (typeof jwtResponse === 'string') { return StandardResponse.forbidden( 'Invalid JWT authorization. Please sign in to obtain new JWT.', ) + } const result = await resendEmailConfirmation(jwtResponse) - if (result === 'already_confirmed') + if (result === 'already_confirmed') { return StandardResponse.unprocessableContent( `Email address ${jwtResponse.email} is already confirmed.`, ) + } return StandardResponse.ok({ code: 'Ok', diff --git a/app/routes/settings.account.tsx b/app/routes/settings.account.tsx index 26451ac2..07344300 100644 --- a/app/routes/settings.account.tsx +++ b/app/routes/settings.account.tsx @@ -1,8 +1,10 @@ +import { CheckLine, OctagonAlert } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Form, useActionData, + useFetcher, useLoaderData, data, redirect, @@ -29,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, @@ -39,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) @@ -53,39 +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 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 }, + ) + } - const errors = { - name: name ? null : 'Invalid name', - email: email ? null : 'Invalid email', - passwordUpdate: passwordUpdate ? null : 'Password is required', + 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, @@ -115,31 +147,49 @@ 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) { + 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() - } - // Show success toast if profile updated - if (actionData && !actionData?.errors?.passwordUpdate) { + } else { toast({ title: t('profile_successfully_updated'), variant: 'success', }) } - }, [actionData, toast]) + }, [actionData, toast, t]) + + // Handle resend verification response (via fetcher) + useEffect(() => { + 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]) return (
@@ -168,9 +218,39 @@ export default function EditUserProfilePage() { name="email" placeholder={t('enter_email')} type="email" - readOnly={true} defaultValue={userData?.email} /> + {userData?.emailIsConfirmed ? ( +

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

+ ) : ( +
+

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

+ +
+ )}
@@ -203,7 +283,6 @@ export default function EditUserProfilePage() {