feat: add forgot password flow and reusable password field
This commit is contained in:
80
src/hooks/useForgotPasswordForm.ts
Normal file
80
src/hooks/useForgotPasswordForm.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
117
src/hooks/useSetPasswordForm.ts
Normal file
117
src/hooks/useSetPasswordForm.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user