/** * PasswordResetConfirmForm Component * Handles password reset with token from email * Integrates with backend API password reset confirm flow */ 'use client'; import { useState } from 'react'; import { Link } from '@/lib/i18n/routing'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Alert } from '@/components/ui/alert'; import { usePasswordResetConfirm } from '@/lib/api/hooks/useAuth'; import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/errors'; // ============================================================================ // Validation Schema // ============================================================================ const createResetConfirmSchema = (t: (key: string) => string) => z .object({ token: z.string().min(1, t('tokenRequired')), new_password: z .string() .min(1, t('passwordRequired')) .min(8, t('passwordMinLength')) .regex(/[0-9]/, t('passwordNumber')) .regex(/[A-Z]/, t('passwordUppercase')), confirm_password: z.string().min(1, t('confirmPasswordRequired')), }) .refine((data) => data.new_password === data.confirm_password, { message: t('passwordMismatch'), path: ['confirm_password'], }); type ResetConfirmFormData = z.infer>; // ============================================================================ // Helper Functions // ============================================================================ /** * Calculate password strength based on requirements */ function calculatePasswordStrength(password: string): { hasMinLength: boolean; hasNumber: boolean; hasUppercase: boolean; strength: number; } { const hasMinLength = password.length >= 8; const hasNumber = /[0-9]/.test(password); const hasUppercase = /[A-Z]/.test(password); const strength = (hasMinLength ? 33 : 0) + (hasNumber ? 33 : 0) + (hasUppercase ? 34 : 0); return { hasMinLength, hasNumber, hasUppercase, strength }; } // ============================================================================ // Component // ============================================================================ interface PasswordResetConfirmFormProps { /** Reset token from URL query parameter */ token: string; /** Optional callback after successful reset */ onSuccess?: () => void; /** Show login link */ showLoginLink?: boolean; /** Custom className for form container */ className?: string; } /** * PasswordResetConfirmForm - Reset password with token * * Features: * - Token validation * - New password validation with strength indicator * - Password confirmation matching * - Loading states * - Server error display * - Success message * - Link back to login * * @example * ```tsx * router.push('/login')} * /> * ``` */ export function PasswordResetConfirmForm({ token, onSuccess, showLoginLink = true, className, }: PasswordResetConfirmFormProps) { const t = useTranslations('auth.passwordResetConfirm'); const [serverError, setServerError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const resetMutation = usePasswordResetConfirm(); const resetConfirmSchema = createResetConfirmSchema((key: string) => t(key)); const form = useForm({ resolver: zodResolver(resetConfirmSchema), defaultValues: { token, new_password: '', confirm_password: '', }, }); const watchPassword = form.watch('new_password'); const passwordStrength = calculatePasswordStrength(watchPassword); const onSubmit = async (data: ResetConfirmFormData) => { try { // Clear previous errors and success message setServerError(null); setSuccessMessage(null); form.clearErrors(); // Confirm password reset await resetMutation.mutateAsync({ token: data.token, new_password: data.new_password, }); // Show success message setSuccessMessage(t('success')); // Reset form form.reset({ token, new_password: '', confirm_password: '' }); // Success callback onSuccess?.(); } catch (error) { // Handle API errors with type guard if (isAPIErrorArray(error)) { // Set general error message const generalError = getGeneralError(error); if (generalError) { setServerError(generalError); } // Set field-specific errors const fieldErrors = getFieldErrors(error); Object.entries(fieldErrors).forEach(([field, message]) => { if (field === 'token' || field === 'new_password') { form.setError(field, { message }); } }); } else { // Unexpected error format setServerError(t('unexpectedError')); } } }; const isSubmitting = form.formState.isSubmitting || resetMutation.isPending; return (
{/* Success Alert */} {successMessage && (

{successMessage}

)} {/* Server Error Alert */} {serverError && (

{serverError}

)} {/* Instructions */}

{t('instructions')}

{/* Hidden Token Field (for form submission) */} {/* New Password Field */}
{form.formState.errors.new_password && (

{form.formState.errors.new_password.message}

)} {/* Password Strength Indicator */} {watchPassword && (
= 66 ? 'bg-yellow-500' : 'bg-red-500' }`} style={{ width: `${passwordStrength.strength}%` }} />
  • {passwordStrength.hasMinLength ? '✓' : '○'} {t('passwordRequirements.minLength')}
  • {passwordStrength.hasNumber ? '✓' : '○'} {t('passwordRequirements.hasNumber')}
  • {passwordStrength.hasUppercase ? '✓' : '○'}{' '} {t('passwordRequirements.hasUppercase')}
)}
{/* Confirm Password Field */}
{form.formState.errors.confirm_password && (

{form.formState.errors.confirm_password.message}

)}
{/* Submit Button */} {/* Login Link */} {showLoginLink && (

{t('rememberPassword')}{' '} {t('backToLogin')}

)}
); }