Fix session expiration handling

This commit is contained in:
2026-04-23 01:10:15 -05:00
parent da4b06094e
commit 820e335d9a
22 changed files with 308 additions and 51 deletions
+3
View File
@@ -1,6 +1,7 @@
import axios from 'axios';
import { API_BASE_URL } from '../config/env';
import { loadSession, saveSession, clearSession } from '../storage/session';
import { notifySessionExpired } from '../utils/sessionEvents';
const client = axios.create({
baseURL: API_BASE_URL,
@@ -38,6 +39,7 @@ client.interceptors.response.use(
const session = await loadSession();
if (!session.refreshToken) {
await clearSession();
notifySessionExpired();
throw error;
}
@@ -68,6 +70,7 @@ client.interceptors.response.use(
} catch (refreshError) {
resolveQueue(refreshError, null);
await clearSession();
notifySessionExpired();
throw refreshError;
} finally {
isRefreshing = false;
+57 -7
View File
@@ -1,8 +1,9 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Alert } from 'react-native';
import { Alert, AppState } from 'react-native';
import { changePassword as apiChangePassword, getMe, login as apiLogin, logout as apiLogout, register as apiRegister } from '../api/auth';
import { initLocalDb, saveProfileCache } from '../storage/localDb';
import { clearSession, loadSession, saveSession } from '../storage/session';
import { SESSION_EXPIRED_MESSAGE, setSessionExpiredHandler } from '../utils/sessionEvents';
const AuthContext = createContext(null);
@@ -11,6 +12,15 @@ export const AuthProvider = ({ children }) => {
const [tokens, setTokens] = useState({ accessToken: null, refreshToken: null });
const [loading, setLoading] = useState(true);
const clearLocalAuthState = useCallback(async ({ showAlert = false } = {}) => {
await clearSession();
setUser(null);
setTokens({ accessToken: null, refreshToken: null });
if (showAlert) {
Alert.alert('Sesión expirada', SESSION_EXPIRED_MESSAGE);
}
}, []);
useEffect(() => {
const bootstrap = async () => {
initLocalDb();
@@ -27,14 +37,56 @@ export const AuthProvider = ({ children }) => {
setUser(data.data.user);
saveProfileCache(data.data.user);
} catch (error) {
await clearSession();
await clearLocalAuthState();
} finally {
setLoading(false);
}
};
bootstrap();
}, []);
}, [clearLocalAuthState]);
useEffect(() => {
return setSessionExpiredHandler(() => {
clearLocalAuthState({ showAlert: true });
});
}, [clearLocalAuthState]);
useEffect(() => {
if (!user || !tokens.accessToken) return undefined;
let timeoutId = null;
let isMounted = true;
const enforceExpiry = async ({ showAlert = false } = {}) => {
const session = await loadSession();
if (!session.accessToken || !session.expiresAt || session.expiresAt <= Date.now()) {
if (isMounted) {
await clearLocalAuthState({ showAlert });
}
return;
}
timeoutId = setTimeout(() => {
clearLocalAuthState({ showAlert: true });
}, Math.max(session.expiresAt - Date.now(), 0));
};
enforceExpiry();
const subscription = AppState.addEventListener('change', (state) => {
if (state === 'active') {
if (timeoutId) clearTimeout(timeoutId);
enforceExpiry({ showAlert: true });
}
});
return () => {
isMounted = false;
if (timeoutId) clearTimeout(timeoutId);
subscription.remove();
};
}, [clearLocalAuthState, tokens.accessToken, user]);
const login = useCallback(async (email, password) => {
const { data } = await apiLogin({ email, password });
@@ -59,11 +111,9 @@ export const AuthProvider = ({ children }) => {
} catch (error) {
Alert.alert('Cerrar sesión', 'No se pudo notificar el cierre al backend. La sesión local se cerrará igual.');
} finally {
await clearSession();
setUser(null);
setTokens({ accessToken: null, refreshToken: null });
await clearLocalAuthState();
}
}, []);
}, [clearLocalAuthState]);
const requestPasswordChange = useCallback(async ({ currentPassword, newPassword }) => {
return apiChangePassword({ currentPassword, newPassword });
+18
View File
@@ -0,0 +1,18 @@
export const SESSION_EXPIRED_MESSAGE = 'Tu sesión expiró. Vuelve a iniciar sesión.';
let sessionExpiredHandler = null;
export const setSessionExpiredHandler = (handler) => {
sessionExpiredHandler = handler;
return () => {
if (sessionExpiredHandler === handler) {
sessionExpiredHandler = null;
}
};
};
export const notifySessionExpired = () => {
if (sessionExpiredHandler) {
sessionExpiredHandler();
}
};