diff --git a/src/AuthProvider.tsx b/src/AuthProvider.tsx index 3cc48ed..58a0978 100644 --- a/src/AuthProvider.tsx +++ b/src/AuthProvider.tsx @@ -133,20 +133,24 @@ export const AuthProvider: React.FC = ({ }, []); const logout = useCallback(async () => { + // The client reports failures through its result, so there is nothing to + // catch. The finally is deliberate: local auth state has to be cleared even + // when the server call fails, otherwise the UI keeps presenting a signed-in + // user whose session is already gone. try { await authClient.logout(); - } catch { - console.error('Error during logout'); } finally { resetAuthState(); } }, [authClient, resetAuthState]); const logoutAllSessions = useCallback(async () => { + // The client reports failures through its result, so there is nothing to + // catch. The finally is deliberate: local auth state has to be cleared even + // when the server call fails, otherwise the UI keeps presenting a signed-in + // user whose session is already gone. try { await authClient.logoutAllSessions(); - } catch { - console.error('Error during logout'); } finally { resetAuthState(); } diff --git a/src/components/MagicLinkSent.tsx b/src/components/MagicLinkSent.tsx index d6bba7b..461b143 100644 --- a/src/components/MagicLinkSent.tsx +++ b/src/components/MagicLinkSent.tsx @@ -71,7 +71,8 @@ const MagicLinkSent: React.FC = () => { navigate('/'); } } catch { - /* ignore */ + // A rejection inside a timer has no caller to surface it, so the poll + // swallows unexpected errors and simply tries again on the next tick. } }, 5000); diff --git a/src/views/EmailRegistration.tsx b/src/views/EmailRegistration.tsx index 24e57f7..8c32d0d 100644 --- a/src/views/EmailRegistration.tsx +++ b/src/views/EmailRegistration.tsx @@ -80,6 +80,8 @@ const EmailRegistration: React.FC = () => { navigate('/'); } } catch { + // Backstop for unexpected errors only. The client reports request + // failures through `error`, not by throwing. console.error('Email OTP verification failed.'); setError('Verification failed.'); } finally { diff --git a/src/views/Login.tsx b/src/views/Login.tsx index e614c1a..a34d1ba 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -72,79 +72,58 @@ const Login: React.FC = () => { const register = async () => { setFormErrors(''); - try { - const { data, error } = await authClient.register({ - email, - phone, - bootstrapToken, - }); - - if (error) { - setFormErrors('Failed to register. Please try again.'); - return; - } + const { data, error } = await authClient.register({ + email, + phone, + bootstrapToken, + }); + + if (error) { + setFormErrors('Failed to register. Please try again.'); + return; + } - if (data.message === 'Success') { - navigate(authRoutePaths.verifyEmailOtp); - return; - } - setFormErrors( - 'An unexpected error occurred. Try again. If the problem persists, contact support.' - ); - } catch { - console.error('Unexpected login error.'); + if (data.message !== 'Success') { setFormErrors( 'An unexpected error occurred. Try again. If the problem persists, contact support.' ); + return; } + + navigate(authRoutePaths.verifyEmailOtp); }; const sendMagicLink = async () => { - try { - const { error } = await authClient.requestMagicLink(); - - if (error) { - setFormErrors('Failed to send magic link.'); - return; - } + const { error } = await authClient.requestMagicLink(); - navigate(authRoutePaths.magicLinkSent, { state: { identifier } }); - } catch { - console.error('Failed to send magic link.'); + if (error) { setFormErrors('Failed to send magic link.'); + return; } + + navigate(authRoutePaths.magicLinkSent, { state: { identifier } }); }; const sendPhoneOtp = async () => { - try { - const { error } = await authClient.requestLoginPhoneOtp(); + const { error } = await authClient.requestLoginPhoneOtp(); - if (error) { - setFormErrors('Failed to send OTP.'); - return; - } - - navigate(authRoutePaths.verifyPhoneOtp, { state: { flow: 'login' } }); - } catch { - console.error('Failed to send phone OTP.'); + if (error) { setFormErrors('Failed to send OTP.'); + return; } + + navigate(authRoutePaths.verifyPhoneOtp, { state: { flow: 'login' } }); }; const sendEmailOtp = async () => { - try { - const { error } = await authClient.requestLoginEmailOtp(); + const { error } = await authClient.requestLoginEmailOtp(); - if (error) { - setFormErrors('Failed to send email code.'); - return; - } - - navigate(authRoutePaths.verifyEmailOtp, { state: { flow: 'login' } }); - } catch { - console.error('Failed to send email OTP.'); + if (error) { setFormErrors('Failed to send email code.'); + return; } + + navigate(authRoutePaths.verifyEmailOtp, { state: { flow: 'login' } }); }; const handleSubmit = async (e: React.FormEvent) => { @@ -188,6 +167,8 @@ const Login: React.FC = () => { await register(); } } catch { + // Backstop for unexpected errors only. The client reports request + // failures through `error`, not by throwing. console.error('Failed to continue sign-in.'); setFormErrors('Failed to continue sign-in. Please try again.'); } diff --git a/src/views/PhoneRegistration.tsx b/src/views/PhoneRegistration.tsx index 3a5182f..e07a1b5 100644 --- a/src/views/PhoneRegistration.tsx +++ b/src/views/PhoneRegistration.tsx @@ -41,6 +41,8 @@ const PhoneRegistration: React.FC = () => { try { await verifyPhoneOTP(); } catch { + // Backstop for unexpected errors only. The client reports request + // failures through `error`, not by throwing. setError('Unexpected error occurred.'); } finally { setLoading(false); @@ -49,27 +51,23 @@ const PhoneRegistration: React.FC = () => { const verifyPhoneOTP = async () => { setLoading(true); - try { - if (!phoneVerified) { - const { error } = isLoginFlow - ? await authClient.verifyLoginPhoneOtp(phoneOtp) - : await authClient.verifyPhoneOtp(phoneOtp); - - if (error) { - setError('Verification failed.'); - } else { - if (isLoginFlow) { - await refreshSession(); - navigate('/'); - return; - } - - setPhoneVerified(true); + + if (!phoneVerified) { + const { error } = isLoginFlow + ? await authClient.verifyLoginPhoneOtp(phoneOtp) + : await authClient.verifyPhoneOtp(phoneOtp); + + if (error) { + setError('Verification failed.'); + } else { + if (isLoginFlow) { + await refreshSession(); + navigate('/'); + return; } + + setPhoneVerified(true); } - } catch { - console.error('Phone OTP verification failed.'); - setError('Verification failed.'); } setLoading(false); diff --git a/src/views/VerifyMagicLink.tsx b/src/views/VerifyMagicLink.tsx index c28628f..ab9fbd3 100644 --- a/src/views/VerifyMagicLink.tsx +++ b/src/views/VerifyMagicLink.tsx @@ -36,25 +36,21 @@ const VerifyMagicLink: React.FC = () => { return; } - try { - const { error } = await authClient.verifyMagicLink(token); - - if (error) { - console.error('Failed to verify token'); - if (mounted) { - setError('Failed to verify token'); - } - return; - } + const { error } = await authClient.verifyMagicLink(token); - // The verify call sets the session cookie for this browser. Sync - // provider state here so the tab that completed verification lands - // authenticated instead of relying on another tab or a manual reload. - await refreshSession(); - } catch { - console.error('Failed to verify magic-link token.'); + if (error) { + console.error('Failed to verify token'); + if (mounted) { + setError('Failed to verify token'); + } + return; } + // The verify call sets the session cookie for this browser. Sync + // provider state here so the tab that completed verification lands + // authenticated instead of relying on another tab or a manual reload. + await refreshSession(); + if (!mounted) { return; } diff --git a/tests/authProvider.test.tsx b/tests/authProvider.test.tsx index 2bfef95..a12c7e7 100644 --- a/tests/authProvider.test.tsx +++ b/tests/authProvider.test.tsx @@ -171,7 +171,11 @@ describe('AuthProvider', () => { }); it('logs out if token validation fails (bad response)', async () => { - mockFetchWithAuthImpl.mockResolvedValueOnce({ ok: false } as any); + // Both calls need a response: the failed /users/me, and the logout that + // follows it. + mockFetchWithAuthImpl + .mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({}) } as any) + .mockResolvedValueOnce({ ok: true, json: async () => ({}) } as any); await act(async () => { render( @@ -187,7 +191,9 @@ describe('AuthProvider', () => { }); it('logs out if token validation throws', async () => { - mockFetchWithAuthImpl.mockRejectedValueOnce(new Error('network down')); + mockFetchWithAuthImpl + .mockRejectedValueOnce(new Error('network down')) + .mockResolvedValueOnce({ ok: true, json: async () => ({}) } as any); await act(async () => { render(