From d873c8eaf77249f5feeebf7eecaee618277a0038 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sat, 11 Jul 2026 17:18:45 -0400 Subject: [PATCH] fix(auth): retain input on failure + copy fixes --- .../src/app/(auth)/forgot-password/page.tsx | 12 +++-- apps/next/src/app/(auth)/sign-in/page.tsx | 18 ++++--- .../component/sign-in-retention.test.tsx | 49 +++++++++++++++++++ 3 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 apps/next/tests/component/sign-in-retention.test.tsx diff --git a/apps/next/src/app/(auth)/forgot-password/page.tsx b/apps/next/src/app/(auth)/forgot-password/page.tsx index ec9c61f..fe56805 100644 --- a/apps/next/src/app/(auth)/forgot-password/page.tsx +++ b/apps/next/src/app/(auth)/forgot-password/page.tsx @@ -100,12 +100,14 @@ const ForgotPassword = () => { await signIn('password', formData).then(() => { setEmail(values.email); setFlow('reset-verification'); + // Only clear the form once the request succeeded; a failed request + // keeps the typed email so the user can retry without re-entering it. + forgotPasswordForm.reset(); }); } catch (error) { console.error('Error resetting password: ', error); toast.error('Error resetting password.'); } finally { - forgotPasswordForm.reset(); setLoading(false); } }; @@ -121,12 +123,14 @@ const ForgotPassword = () => { setLoading(true); try { await signIn('password', formData); + // Only clear on success (navigation follows); a failed submit keeps the + // entered password so the user can correct and retry. + resetVerificationForm.reset(); router.push('/'); } catch (error) { console.error('Error resetting password: ', error); toast.error('Error resetting password.'); } finally { - resetVerificationForm.reset(); setLoading(false); } }; @@ -230,7 +234,7 @@ const ForgotPassword = () => { Please enter the one-time password sent to your - phone. + email.
@@ -265,7 +269,7 @@ const ForgotPassword = () => { render={({ field }) => ( - Confirm Passsword + Confirm Password { formData.append('flow', flow); setLoading(true); try { - await signIn('password', formData).then(() => router.push('/dashboard')); + await signIn('password', formData).then(() => { + signInForm.reset(); + router.push('/dashboard'); + }); } catch (error) { console.error('Error signing in:', error); toast.error('Error signing in.'); } finally { - signInForm.reset(); setLoading(false); } }; @@ -153,6 +155,7 @@ const SignIn = () => { if (values.confirmPassword !== values.password) throw new ConvexError('Passwords do not match.'); await signIn('password', formData).then(() => { + signUpForm.reset(); setEmail(values.email); setFlow('email-verification'); }); @@ -160,7 +163,6 @@ const SignIn = () => { console.error('Error signing up:', error); toast.error('Error signing up.'); } finally { - signUpForm.reset(); setLoading(false); } }; @@ -174,12 +176,14 @@ const SignIn = () => { formData.append('email', email); setLoading(true); try { - await signIn('password', formData).then(() => router.push('/dashboard')); + await signIn('password', formData).then(() => { + verifyEmailForm.reset(); + router.push('/dashboard'); + }); } catch (error) { console.error('Error verifying email:', error); toast.error('Error verifying email.'); } finally { - verifyEmailForm.reset(); setLoading(false); } }; @@ -232,7 +236,7 @@ const SignIn = () => { /> Verify Email @@ -423,7 +427,7 @@ const SignIn = () => { render={({ field }) => ( - Confirm Passsword + Confirm Password ({ + mockSignIn: vi.fn(), + mockToastError: vi.fn(), +})); + +vi.mock('@convex-dev/auth/react', () => ({ + useAuthActions: () => ({ signIn: mockSignIn }), +})); + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), +})); + +vi.mock('sonner', () => ({ + toast: { + error: mockToastError, + success: vi.fn(), + }, +})); + +describe('sign-in input retention', () => { + it('keeps the typed email and password after a failed sign-in', async () => { + mockSignIn.mockRejectedValue(new Error('Invalid credentials')); + + render(); + + const emailInput = screen.getByPlaceholderText('you@example.com'); + const passwordInput = screen.getByPlaceholderText('Your password'); + + fireEvent.change(emailInput, { + target: { value: 'person@example.com' }, + }); + fireEvent.change(passwordInput, { target: { value: 'Sup3rSecret!' } }); + + fireEvent.click(screen.getByRole('button', { name: 'Sign In' })); + + await waitFor(() => expect(mockToastError).toHaveBeenCalled()); + + await waitFor(() => { + expect(emailInput).toHaveValue('person@example.com'); + expect(passwordInput).toHaveValue('Sup3rSecret!'); + }); + }); +});