fix: robust mobile suspend and auth recovery

This commit is contained in:
2026-03-04 00:51:32 -05:00
parent bdfcd55370
commit 90bb93be17
11 changed files with 257 additions and 47 deletions

View File

@ -18,21 +18,39 @@ export const useAuthStore = defineStore('auth', () => {
const isPassenger = computed(() => !role.value || role.value?.toUpperCase() === 'PASSENGER')
/** Listens for Supabase Auth state changes */
supabase.auth.onAuthStateChange(async (_event, session) => {
supabase.auth.onAuthStateChange(async (event, session) => {
userSession.value = session
if (session?.user) {
// Immediately fetch their role from standard Public table if present
const { data } = await supabase
.from('users')
.select('*')
.eq('id', session.user.id)
.single()
// Cargar perfil solo en inicio de sesión real, NO en cada token refresh.
// Cuando la pantalla se apaga y vuelve, Supabase re-emite TOKEN_REFRESHED.
// Si se re-fetchea el perfil en ese momento, la red puede estar inestable
// y el fetch queda colgado, dejando userProfile = null y re-disparando
// todos los watchers de auto_location y autenticación.
const profileLoadEvents = ['SIGNED_IN', 'INITIAL_SESSION']
if (session?.user && profileLoadEvents.includes(event)) {
// Timeout de seguridad: si la red está inestable al despertar,
// no bloquear indefinidamente
const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), 10000))
const profileFetch = (async () => {
try {
const { data } = await supabase
.from('users')
.select('*')
.eq('id', session.user!.id)
.single()
return data ?? null
} catch {
return null
}
})()
const data = await Promise.race([profileFetch, timeout])
if (data) userProfile.value = data
} else {
} else if (!session?.user) {
userProfile.value = null
}
// TOKEN_REFRESHED: no se re-fetchea el perfil, userSession ya se actualizó arriba
})
async function login(email: string, pass: string, keepSession: boolean = false) {

View File

@ -11,9 +11,9 @@ export const useCouponStore = defineStore('coupon', () => {
const myCoupons = ref<any[]>([])
const filters = ref<CouponFilters>({})
async function loadCoupons(newFilters?: CouponFilters) {
if (isLoading.value) return;
isLoading.value = true
async function loadCoupons(newFilters?: CouponFilters, isBackground = false) {
if (isLoading.value && !isBackground) return;
if (!isBackground) isLoading.value = true
error.value = null
if (newFilters) {
filters.value = newFilters
@ -24,7 +24,7 @@ export const useCouponStore = defineStore('coupon', () => {
error.value = e instanceof Error ? e.message : 'Failed to load coupons'
console.error('Error loading coupons:', e)
} finally {
isLoading.value = false
if (!isBackground) isLoading.value = false
}
}

View File

@ -24,6 +24,13 @@ export const useFavoritesStore = defineStore('favorites', () => {
async function loadFavorites() {
isLoading.value = true
// Safety: si la red está inestable al awakening del background, no quedar cargando
const safetyTimer = setTimeout(() => {
if (isLoading.value) {
console.warn('SIBU | favoritesStore: safety timeout — reseteando isLoading')
isLoading.value = false
}
}, 12000)
try {
const { data: userData } = await supabase.auth.getUser()
if (!userData?.user) { favorites.value = []; return }
@ -37,6 +44,7 @@ export const useFavoritesStore = defineStore('favorites', () => {
} catch (error) {
console.error('Error loading favorites:', error)
} finally {
clearTimeout(safetyTimer)
isLoading.value = false
}
}

View File

@ -18,19 +18,46 @@ export const useRouteStore = defineStore('route', () => {
const hasSelectedRoute = computed(() => selectedRouteId.value !== null && selectedRouteName.value !== null)
async function loadRoutes(filters?: { originCity?: string, destinationCity?: string }, force = false) {
// Safety: nunca más de 12s cargando (protección contra thread congelado por OS al apagar pantalla)
let _routesSafetyTimer: ReturnType<typeof setTimeout> | null = null
let _stopsSafetyTimer: ReturnType<typeof setTimeout> | null = null
function _startRoutesSafety() {
if (_routesSafetyTimer) clearTimeout(_routesSafetyTimer)
_routesSafetyTimer = setTimeout(() => {
if (isLoadingRoutes.value) {
console.warn('SIBU | routeStore: routes safety timeout — reseteando isLoadingRoutes')
isLoadingRoutes.value = false
}
}, 12000)
}
function _startStopsSafety() {
if (_stopsSafetyTimer) clearTimeout(_stopsSafetyTimer)
_stopsSafetyTimer = setTimeout(() => {
if (isLoadingStops.value) {
console.warn('SIBU | routeStore: stops safety timeout — reseteando isLoadingStops')
isLoadingStops.value = false
}
}, 12000)
}
async function loadRoutes(filters?: { originCity?: string, destinationCity?: string }, force = false, isBackground = false) {
const CACHE_TIME = 1000 * 60 * 15; // 15 minutos
const now = Date.now();
// Guard: Si ya se están cargando rutas, no iniciar otra petición
if (isLoadingRoutes.value) return;
// Guard: Si ya se están cargando rutas y NO estamos en background, omitir
// Si estamos en background, podemos sobreescribir la carga sin mostrar spinner
if (isLoadingRoutes.value && !isBackground) return;
// Si no forzamos, no hay filtros raros, ya tenemos rutas y aún no expira el caché, omitir llamada
if (!force && !filters && allRoutes.value.length > 0 && (now - lastFetched.value < CACHE_TIME)) {
// Excepción: isBackground de refocus obliga a actualizar para asegurar frescura después de mucho rato apagado.
if (!force && !isBackground && !filters && allRoutes.value.length > 0 && (now - lastFetched.value < CACHE_TIME)) {
return
}
isLoadingRoutes.value = true
if (!isBackground) {
isLoadingRoutes.value = true
_startRoutesSafety()
}
error.value = null
try {
allRoutes.value = await routesService.getAllRoutes(filters)
@ -39,7 +66,10 @@ export const useRouteStore = defineStore('route', () => {
error.value = e instanceof Error ? e.message : 'Failed to load routes'
console.error('Error loading routes:', e)
} finally {
isLoadingRoutes.value = false
if (!isBackground) {
if (_routesSafetyTimer) { clearTimeout(_routesSafetyTimer); _routesSafetyTimer = null }
isLoadingRoutes.value = false
}
}
}
@ -58,6 +88,7 @@ export const useRouteStore = defineStore('route', () => {
if (isLoadingStops.value) return [];
isLoadingStops.value = true
error.value = null
_startStopsSafety()
try {
const stops = await routesService.getRouteStops(routeId)
selectedRouteStops.value = stops
@ -69,6 +100,7 @@ export const useRouteStore = defineStore('route', () => {
selectedRouteStops.value = []
return []
} finally {
if (_stopsSafetyTimer) { clearTimeout(_stopsSafetyTimer); _stopsSafetyTimer = null }
isLoadingStops.value = false
}
}

View File

@ -9,29 +9,48 @@ export const useScheduleStore = defineStore('schedule', () => {
const isLoading = ref(false)
const error = ref<string | null>(null)
async function loadRouteSchedules(routeId: string) {
isLoading.value = true
// Safety: nunca más de 12s cargando (protección contra thread congelado por OS al apagar pantalla)
let _safetyTimer: ReturnType<typeof setTimeout> | null = null
function _startSafetyTimer() {
_clearSafetyTimer()
_safetyTimer = setTimeout(() => {
if (isLoading.value) {
console.warn('SIBU | scheduleStore: safety timeout — reseteando isLoading')
isLoading.value = false
}
}, 12000)
}
function _clearSafetyTimer() {
if (_safetyTimer) { clearTimeout(_safetyTimer); _safetyTimer = null }
}
async function loadRouteSchedules(routeId: string, isBackground = false) {
if (!isBackground) isLoading.value = true
error.value = null
if (!isBackground) _startSafetyTimer()
try {
schedules.value = await schedulesService.getRouteSchedules(routeId)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
console.error('Error loading schedules:', e)
} finally {
isLoading.value = false
if (!isBackground) _clearSafetyTimer()
if (!isBackground) isLoading.value = false
}
}
async function loadStopSchedules(stopId: string) {
isLoading.value = true
async function loadStopSchedules(stopId: string, isBackground = false) {
if (!isBackground) isLoading.value = true
error.value = null
if (!isBackground) _startSafetyTimer()
try {
schedules.value = await schedulesService.getStopSchedules(stopId)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
console.error('Error loading schedules:', e)
} finally {
isLoading.value = false
if (!isBackground) _clearSafetyTimer()
if (!isBackground) isLoading.value = false
}
}

View File

@ -10,19 +10,38 @@ export const useShuttleStore = defineStore('shuttle', () => {
const error = ref<string | null>(null)
const filters = ref<ShuttleFilters>({})
async function loadShuttles(newFilters?: ShuttleFilters) {
isLoading.value = true
// Safety: nunca más de 12s cargando (protección contra thread congelado por OS al apagar pantalla)
let _safetyTimer: ReturnType<typeof setTimeout> | null = null
function _startSafetyTimer() {
_clearSafetyTimer()
_safetyTimer = setTimeout(() => {
if (isLoading.value) {
console.warn('SIBU | shuttleStore: safety timeout — reseteando isLoading')
isLoading.value = false
}
}, 12000)
}
function _clearSafetyTimer() {
if (_safetyTimer) { clearTimeout(_safetyTimer); _safetyTimer = null }
}
async function loadShuttles(newFilters?: ShuttleFilters, isBackground = false) {
if (!isBackground) isLoading.value = true
error.value = null
if (newFilters) {
filters.value = newFilters
}
if (!isBackground) _startSafetyTimer()
try {
shuttles.value = await shuttlesService.getAllShuttles(filters.value)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load shuttles'
console.error('Error loading shuttles:', e)
} finally {
isLoading.value = false
if (!isBackground) {
_clearSafetyTimer()
isLoading.value = false
}
}
}

View File

@ -10,19 +10,38 @@ export const useTaxiStore = defineStore('taxi', () => {
const error = ref<string | null>(null)
const filters = ref<TaxiFilters>({})
async function loadTaxis(newFilters?: TaxiFilters) {
isLoading.value = true
// Safety: nunca más de 12s cargando (protección contra thread congelado por OS al apagar pantalla)
let _safetyTimer: ReturnType<typeof setTimeout> | null = null
function _startSafetyTimer() {
_clearSafetyTimer()
_safetyTimer = setTimeout(() => {
if (isLoading.value) {
console.warn('SIBU | taxiStore: safety timeout — reseteando isLoading')
isLoading.value = false
}
}, 12000)
}
function _clearSafetyTimer() {
if (_safetyTimer) { clearTimeout(_safetyTimer); _safetyTimer = null }
}
async function loadTaxis(newFilters?: TaxiFilters, isBackground = false) {
if (!isBackground) isLoading.value = true
error.value = null
if (newFilters) {
filters.value = newFilters
}
if (!isBackground) _startSafetyTimer()
try {
taxis.value = await taxisService.getAllTaxis(filters.value)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load taxis'
console.error('Error loading taxis:', e)
} finally {
isLoading.value = false
if (!isBackground) {
_clearSafetyTimer()
isLoading.value = false
}
}
}