fix: robust mobile suspend and auth recovery
This commit is contained in:
@ -7,6 +7,14 @@ import { useThemeStore } from './stores/theme'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { useFavoritesStore } from './stores/favorites'
|
||||
import { analyticsService } from '@/services/analyticsService'
|
||||
// 🔧 FIX: Importar stores para resetear isLoading al volver del background
|
||||
import { useScheduleStore } from './stores/schedule'
|
||||
import { useRouteStore } from './stores/route'
|
||||
import { useTaxiStore } from './stores/taxi'
|
||||
import { useShuttleStore } from './stores/shuttle'
|
||||
import { useCouponStore } from './stores/coupon'
|
||||
import { supabase } from '@/supabase'
|
||||
// useFavoritesStore ya importado arriba (línea 8)
|
||||
|
||||
// Initialize theme store
|
||||
const route = useRoute()
|
||||
@ -23,14 +31,68 @@ const isAuthScreen = computed(() => {
|
||||
let lastHiddenAt: number | null = null
|
||||
let refocusDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function dispatchRefocus(reason: string) {
|
||||
async function dispatchRefocus(reason: string) {
|
||||
// Debounce: no disparar dos eventos seguidos en menos de 1 segundo
|
||||
if (refocusDebounceTimer) return
|
||||
refocusDebounceTimer = setTimeout(() => { refocusDebounceTimer = null }, 1000)
|
||||
console.log(`SIBU | App refocus — motivo: ${reason}`)
|
||||
console.log(`SIBU | App.vue dispatching auth check and refocus — motivo: ${reason}`)
|
||||
|
||||
// 🛡 HIGIENE TRANSACCIONAL: Solo verificamos sesión si el usuario estaba logueado.
|
||||
// Si Guest, omitimos el Refresh para no fallar
|
||||
if (authStore.isAuthenticated) {
|
||||
console.log(`SIBU | Verificando sanidad de sesión Supabase...`)
|
||||
const timeoutMs = 5000
|
||||
|
||||
const refreshWithTimeout = Promise.race([
|
||||
supabase.auth.refreshSession(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Auth refresh timeout")), timeoutMs)
|
||||
)
|
||||
])
|
||||
|
||||
try {
|
||||
await refreshWithTimeout
|
||||
} catch (err) {
|
||||
console.warn("SIBU | Auth zombie detectado o red muerta. Reseteando sesión y app.", err)
|
||||
// Fall fast & hard: Romper cualquier loop infinito o socket huérfano
|
||||
document.body.style.opacity = '0'
|
||||
await supabase.auth.signOut()
|
||||
window.location.reload()
|
||||
return // No despachar `app-refocus`, la app está muriendo
|
||||
}
|
||||
}
|
||||
|
||||
// Una vez que sabemos que la autenticación está sana (o no era relevante), enviamos señal a la UI
|
||||
window.dispatchEvent(new CustomEvent('app-refocus'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔧 FIX CRÍTICO: Resetear todos los isLoading antes de refocus.
|
||||
*
|
||||
* Cuando se apaga/bloquea la pantalla, el OS puede congelar el JS thread.
|
||||
* En ese caso, los fetch en vuelo son abortados por el navegador y el bloque
|
||||
* `finally` puede no ejecutarse, dejando isLoading = true para siempre.
|
||||
*
|
||||
* Este reset garantiza que la UI no quede bloqueada con un spinner infinito
|
||||
* al volver de background, mostrando los datos que ya estaban cargados.
|
||||
*/
|
||||
function forceResetAllLoadingStates() {
|
||||
try {
|
||||
useScheduleStore().$patch({ isLoading: false })
|
||||
useRouteStore().$patch({ isLoadingRoutes: false, isLoadingStops: false })
|
||||
useTaxiStore().$patch({ isLoading: false })
|
||||
useShuttleStore().$patch({ isLoading: false })
|
||||
useCouponStore().$patch({ isLoading: false })
|
||||
// Favorites: solo existe cuando hay usuario autenticado — el bug específico
|
||||
if (authStore.isAuthenticated) {
|
||||
useFavoritesStore().$patch({ isLoading: false })
|
||||
}
|
||||
console.log('SIBU | ✅ Loading states reseteados tras regreso del background')
|
||||
} catch (e) {
|
||||
console.warn('SIBU | No se pudo resetear loading states:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
lastHiddenAt = Date.now()
|
||||
@ -40,6 +102,11 @@ function handleVisibilityChange() {
|
||||
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
||||
// Umbral bajo (1 s) para capturar retornos rápidos desde Google Maps / links externos
|
||||
if (secondsAway > 1) {
|
||||
// Reset de loading states: solo si estuvo más de 3s (suficiente para que
|
||||
// el OS congele el JS thread y los fetch queden colgados)
|
||||
if (secondsAway > 3) {
|
||||
forceResetAllLoadingStates()
|
||||
}
|
||||
dispatchRefocus(`visibilitychange tras ${secondsAway.toFixed(1)}s`)
|
||||
}
|
||||
lastHiddenAt = null
|
||||
@ -51,6 +118,7 @@ function handleVisibilityChange() {
|
||||
function handlePageShow(event: PageTransitionEvent) {
|
||||
if (event.persisted) {
|
||||
// La página fue restaurada desde bfcache — siempre necesita refocus
|
||||
forceResetAllLoadingStates()
|
||||
dispatchRefocus('pageshow persisted (bfcache)')
|
||||
}
|
||||
}
|
||||
@ -63,6 +131,9 @@ function handleWindowFocus() {
|
||||
if (lastHiddenAt !== null) {
|
||||
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
||||
if (secondsAway > 1) {
|
||||
if (secondsAway > 3) {
|
||||
forceResetAllLoadingStates()
|
||||
}
|
||||
dispatchRefocus(`window focus tras ${secondsAway.toFixed(1)}s`)
|
||||
}
|
||||
lastHiddenAt = null
|
||||
|
||||
Reference in New Issue
Block a user