feat: add forgot password flow and reusable password field

This commit is contained in:
2026-06-17 16:55:10 +05:30
parent 4c2241ff72
commit b325975282
15 changed files with 541 additions and 44 deletions

View File

@@ -0,0 +1,80 @@
'use client';
import { authService } from '@/services/api/auth.service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSearchParams } from 'next/navigation';
import { useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const forgotPasswordFormSchema = z.object({
email: z
.string()
.trim()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
});
export type ForgotPasswordFormValues = z.infer<
typeof forgotPasswordFormSchema
>;
export function useForgotPasswordForm() {
const searchParams = useSearchParams();
const email = searchParams.get('email') || '';
const [isSubmitted, setIsSubmitted] = useState(false);
const {
control,
register,
handleSubmit: submitForm,
formState: { errors, isSubmitting, isSubmitted: isFormSubmitted },
} = useForm<ForgotPasswordFormValues>({
defaultValues: { email },
mode: 'onSubmit',
reValidateMode: 'onChange',
resolver: zodResolver(forgotPasswordFormSchema),
});
const emailValue = useWatch({ control, name: 'email' }) || '';
const canSubmit = emailValue.trim().length > 0;
const displayInfo = useMemo(
() => ({
title: isSubmitted ? 'Check Your Email' : 'Forgot Password?',
description: isSubmitted
? 'We have sent a password reset link to your account. Please check your email and follow the instructions.'
: 'Enter your email address to receive a password reset link.',
buttonLabel: isSubmitted ? 'Back to Login' : 'Send Reset Link',
}),
[isSubmitted],
);
const onSubmit = submitForm(
async (values) => {
try {
await authService.forgotPassword({ email: values.email.trim() });
setIsSubmitted(true);
} catch (error) {
toast.error(
error instanceof Error ? error.message : 'Unable to send reset link',
);
}
},
(formErrors) => {
if (formErrors.email?.message) {
toast.error(formErrors.email.message);
}
},
);
return {
register,
handleSubmit: onSubmit,
errors,
isLoading: isSubmitting,
isSubmitted,
isFormSubmitted,
canSubmit,
displayInfo,
};
}

View File

@@ -0,0 +1,117 @@
'use client';
import { authService } from '@/services/api/auth.service';
import type { SetPasswordPayload } from '@/types';
import { ROUTES } from '@/utils/routes';
import { zodResolver } from '@hookform/resolvers/zod';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const setPasswordFormSchema = z
.object({
password: z
.string()
.trim()
.min(1, 'Password is required')
.min(6, 'Password must be at least 6 characters'),
confirmPassword: z.string().trim().min(1, 'Confirm password is required'),
})
.refine((values) => values.password === values.confirmPassword, {
path: ['confirmPassword'],
message: 'Passwords do not match',
});
export type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>;
export function useSetPasswordForm() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const token = searchParams.get('token') || '';
const isInvite = pathname.includes(ROUTES.SET_PASSWORD);
const {
control,
register,
handleSubmit: submitForm,
formState: { errors, isSubmitting, isSubmitted },
} = useForm<SetPasswordFormValues>({
defaultValues: {
password: '',
confirmPassword: '',
},
mode: 'onSubmit',
reValidateMode: 'onChange',
resolver: zodResolver(setPasswordFormSchema),
});
const password = useWatch({ control, name: 'password' }) || '';
const confirmPassword = useWatch({ control, name: 'confirmPassword' }) || '';
const canSubmit =
Boolean(token) &&
password.trim().length > 0 &&
confirmPassword.trim().length > 0;
const displayInfo = useMemo(
() => ({
title: isInvite ? 'Set Your Password' : 'Reset Your Password',
description: isInvite
? 'Welcome. Please set a secure password for your account.'
: 'Please enter your new password below.',
buttonLabel: isInvite ? 'Accept & Set Password' : 'Reset Password',
}),
[isInvite],
);
const onSubmit = submitForm(
async (values) => {
if (!token) {
toast.error('Password reset token is missing');
return;
}
const payload: SetPasswordPayload = {
token,
password: values.password.trim(),
confirm_password: values.confirmPassword.trim(),
};
try {
if (isInvite) {
await authService.setPassword(payload);
} else {
await authService.resetPassword(payload);
}
toast.success(
isInvite ? 'Password set successfully' : 'Password reset successfully',
);
router.replace(ROUTES.LOGIN);
} catch (error) {
toast.error(
error instanceof Error ? error.message : 'Unable to update password',
);
}
},
(formErrors) => {
if (formErrors.password?.message) {
toast.error(formErrors.password.message);
return;
}
if (formErrors.confirmPassword?.message) {
toast.error(formErrors.confirmPassword.message);
}
},
);
return {
register,
handleSubmit: onSubmit,
errors,
isLoading: isSubmitting,
isSubmitted,
canSubmit,
displayInfo,
hasToken: Boolean(token),
};
}