Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,20 +133,24 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
}, []);

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();
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/MagicLinkSent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions src/views/EmailRegistration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
81 changes: 31 additions & 50 deletions src/views/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLFormElement>) => {
Expand Down Expand Up @@ -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.');
}
Expand Down
36 changes: 17 additions & 19 deletions src/views/PhoneRegistration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
28 changes: 12 additions & 16 deletions src/views/VerifyMagicLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
10 changes: 8 additions & 2 deletions tests/authProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Loading