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 { useAuthStore } from './stores/auth'
|
||||||
import { useFavoritesStore } from './stores/favorites'
|
import { useFavoritesStore } from './stores/favorites'
|
||||||
import { analyticsService } from '@/services/analyticsService'
|
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
|
// Initialize theme store
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -23,14 +31,68 @@ const isAuthScreen = computed(() => {
|
|||||||
let lastHiddenAt: number | null = null
|
let lastHiddenAt: number | null = null
|
||||||
let refocusDebounceTimer: ReturnType<typeof setTimeout> | 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
|
// Debounce: no disparar dos eventos seguidos en menos de 1 segundo
|
||||||
if (refocusDebounceTimer) return
|
if (refocusDebounceTimer) return
|
||||||
refocusDebounceTimer = setTimeout(() => { refocusDebounceTimer = null }, 1000)
|
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'))
|
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() {
|
function handleVisibilityChange() {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden') {
|
||||||
lastHiddenAt = Date.now()
|
lastHiddenAt = Date.now()
|
||||||
@ -40,6 +102,11 @@ function handleVisibilityChange() {
|
|||||||
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
||||||
// Umbral bajo (1 s) para capturar retornos rápidos desde Google Maps / links externos
|
// Umbral bajo (1 s) para capturar retornos rápidos desde Google Maps / links externos
|
||||||
if (secondsAway > 1) {
|
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`)
|
dispatchRefocus(`visibilitychange tras ${secondsAway.toFixed(1)}s`)
|
||||||
}
|
}
|
||||||
lastHiddenAt = null
|
lastHiddenAt = null
|
||||||
@ -51,6 +118,7 @@ function handleVisibilityChange() {
|
|||||||
function handlePageShow(event: PageTransitionEvent) {
|
function handlePageShow(event: PageTransitionEvent) {
|
||||||
if (event.persisted) {
|
if (event.persisted) {
|
||||||
// La página fue restaurada desde bfcache — siempre necesita refocus
|
// La página fue restaurada desde bfcache — siempre necesita refocus
|
||||||
|
forceResetAllLoadingStates()
|
||||||
dispatchRefocus('pageshow persisted (bfcache)')
|
dispatchRefocus('pageshow persisted (bfcache)')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,6 +131,9 @@ function handleWindowFocus() {
|
|||||||
if (lastHiddenAt !== null) {
|
if (lastHiddenAt !== null) {
|
||||||
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
||||||
if (secondsAway > 1) {
|
if (secondsAway > 1) {
|
||||||
|
if (secondsAway > 3) {
|
||||||
|
forceResetAllLoadingStates()
|
||||||
|
}
|
||||||
dispatchRefocus(`window focus tras ${secondsAway.toFixed(1)}s`)
|
dispatchRefocus(`window focus tras ${secondsAway.toFixed(1)}s`)
|
||||||
}
|
}
|
||||||
lastHiddenAt = null
|
lastHiddenAt = null
|
||||||
|
|||||||
@ -18,21 +18,39 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const isPassenger = computed(() => !role.value || role.value?.toUpperCase() === 'PASSENGER')
|
const isPassenger = computed(() => !role.value || role.value?.toUpperCase() === 'PASSENGER')
|
||||||
|
|
||||||
/** Listens for Supabase Auth state changes */
|
/** Listens for Supabase Auth state changes */
|
||||||
supabase.auth.onAuthStateChange(async (_event, session) => {
|
supabase.auth.onAuthStateChange(async (event, session) => {
|
||||||
userSession.value = session
|
userSession.value = session
|
||||||
|
|
||||||
if (session?.user) {
|
// Cargar perfil solo en inicio de sesión real, NO en cada token refresh.
|
||||||
// Immediately fetch their role from standard Public table if present
|
// Cuando la pantalla se apaga y vuelve, Supabase re-emite TOKEN_REFRESHED.
|
||||||
const { data } = await supabase
|
// Si se re-fetchea el perfil en ese momento, la red puede estar inestable
|
||||||
.from('users')
|
// y el fetch queda colgado, dejando userProfile = null y re-disparando
|
||||||
.select('*')
|
// todos los watchers de auto_location y autenticación.
|
||||||
.eq('id', session.user.id)
|
const profileLoadEvents = ['SIGNED_IN', 'INITIAL_SESSION']
|
||||||
.single()
|
|
||||||
|
|
||||||
|
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
|
if (data) userProfile.value = data
|
||||||
} else {
|
} else if (!session?.user) {
|
||||||
userProfile.value = null
|
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) {
|
async function login(email: string, pass: string, keepSession: boolean = false) {
|
||||||
|
|||||||
@ -11,9 +11,9 @@ export const useCouponStore = defineStore('coupon', () => {
|
|||||||
const myCoupons = ref<any[]>([])
|
const myCoupons = ref<any[]>([])
|
||||||
const filters = ref<CouponFilters>({})
|
const filters = ref<CouponFilters>({})
|
||||||
|
|
||||||
async function loadCoupons(newFilters?: CouponFilters) {
|
async function loadCoupons(newFilters?: CouponFilters, isBackground = false) {
|
||||||
if (isLoading.value) return;
|
if (isLoading.value && !isBackground) return;
|
||||||
isLoading.value = true
|
if (!isBackground) isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
if (newFilters) {
|
if (newFilters) {
|
||||||
filters.value = newFilters
|
filters.value = newFilters
|
||||||
@ -24,7 +24,7 @@ export const useCouponStore = defineStore('coupon', () => {
|
|||||||
error.value = e instanceof Error ? e.message : 'Failed to load coupons'
|
error.value = e instanceof Error ? e.message : 'Failed to load coupons'
|
||||||
console.error('Error loading coupons:', e)
|
console.error('Error loading coupons:', e)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
if (!isBackground) isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,13 @@ export const useFavoritesStore = defineStore('favorites', () => {
|
|||||||
|
|
||||||
async function loadFavorites() {
|
async function loadFavorites() {
|
||||||
isLoading.value = true
|
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 {
|
try {
|
||||||
const { data: userData } = await supabase.auth.getUser()
|
const { data: userData } = await supabase.auth.getUser()
|
||||||
if (!userData?.user) { favorites.value = []; return }
|
if (!userData?.user) { favorites.value = []; return }
|
||||||
@ -37,6 +44,7 @@ export const useFavoritesStore = defineStore('favorites', () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading favorites:', error)
|
console.error('Error loading favorites:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
clearTimeout(safetyTimer)
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,19 +18,46 @@ export const useRouteStore = defineStore('route', () => {
|
|||||||
|
|
||||||
const hasSelectedRoute = computed(() => selectedRouteId.value !== null && selectedRouteName.value !== null)
|
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 CACHE_TIME = 1000 * 60 * 15; // 15 minutos
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Guard: Si ya se están cargando rutas, no iniciar otra petición
|
// Guard: Si ya se están cargando rutas y NO estamos en background, omitir
|
||||||
if (isLoadingRoutes.value) return;
|
// 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
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingRoutes.value = true
|
if (!isBackground) {
|
||||||
|
isLoadingRoutes.value = true
|
||||||
|
_startRoutesSafety()
|
||||||
|
}
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
allRoutes.value = await routesService.getAllRoutes(filters)
|
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'
|
error.value = e instanceof Error ? e.message : 'Failed to load routes'
|
||||||
console.error('Error loading routes:', e)
|
console.error('Error loading routes:', e)
|
||||||
} finally {
|
} 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 [];
|
if (isLoadingStops.value) return [];
|
||||||
isLoadingStops.value = true
|
isLoadingStops.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
_startStopsSafety()
|
||||||
try {
|
try {
|
||||||
const stops = await routesService.getRouteStops(routeId)
|
const stops = await routesService.getRouteStops(routeId)
|
||||||
selectedRouteStops.value = stops
|
selectedRouteStops.value = stops
|
||||||
@ -69,6 +100,7 @@ export const useRouteStore = defineStore('route', () => {
|
|||||||
selectedRouteStops.value = []
|
selectedRouteStops.value = []
|
||||||
return []
|
return []
|
||||||
} finally {
|
} finally {
|
||||||
|
if (_stopsSafetyTimer) { clearTimeout(_stopsSafetyTimer); _stopsSafetyTimer = null }
|
||||||
isLoadingStops.value = false
|
isLoadingStops.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,29 +9,48 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
async function loadRouteSchedules(routeId: string) {
|
// Safety: nunca más de 12s cargando (protección contra thread congelado por OS al apagar pantalla)
|
||||||
isLoading.value = true
|
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
|
error.value = null
|
||||||
|
if (!isBackground) _startSafetyTimer()
|
||||||
try {
|
try {
|
||||||
schedules.value = await schedulesService.getRouteSchedules(routeId)
|
schedules.value = await schedulesService.getRouteSchedules(routeId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
|
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
|
||||||
console.error('Error loading schedules:', e)
|
console.error('Error loading schedules:', e)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
if (!isBackground) _clearSafetyTimer()
|
||||||
|
if (!isBackground) isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStopSchedules(stopId: string) {
|
async function loadStopSchedules(stopId: string, isBackground = false) {
|
||||||
isLoading.value = true
|
if (!isBackground) isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
if (!isBackground) _startSafetyTimer()
|
||||||
try {
|
try {
|
||||||
schedules.value = await schedulesService.getStopSchedules(stopId)
|
schedules.value = await schedulesService.getStopSchedules(stopId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
|
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
|
||||||
console.error('Error loading schedules:', e)
|
console.error('Error loading schedules:', e)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
if (!isBackground) _clearSafetyTimer()
|
||||||
|
if (!isBackground) isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,19 +10,38 @@ export const useShuttleStore = defineStore('shuttle', () => {
|
|||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const filters = ref<ShuttleFilters>({})
|
const filters = ref<ShuttleFilters>({})
|
||||||
|
|
||||||
async function loadShuttles(newFilters?: ShuttleFilters) {
|
// Safety: nunca más de 12s cargando (protección contra thread congelado por OS al apagar pantalla)
|
||||||
isLoading.value = true
|
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
|
error.value = null
|
||||||
if (newFilters) {
|
if (newFilters) {
|
||||||
filters.value = newFilters
|
filters.value = newFilters
|
||||||
}
|
}
|
||||||
|
if (!isBackground) _startSafetyTimer()
|
||||||
try {
|
try {
|
||||||
shuttles.value = await shuttlesService.getAllShuttles(filters.value)
|
shuttles.value = await shuttlesService.getAllShuttles(filters.value)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load shuttles'
|
error.value = e instanceof Error ? e.message : 'Failed to load shuttles'
|
||||||
console.error('Error loading shuttles:', e)
|
console.error('Error loading shuttles:', e)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
if (!isBackground) {
|
||||||
|
_clearSafetyTimer()
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,19 +10,38 @@ export const useTaxiStore = defineStore('taxi', () => {
|
|||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const filters = ref<TaxiFilters>({})
|
const filters = ref<TaxiFilters>({})
|
||||||
|
|
||||||
async function loadTaxis(newFilters?: TaxiFilters) {
|
// Safety: nunca más de 12s cargando (protección contra thread congelado por OS al apagar pantalla)
|
||||||
isLoading.value = true
|
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
|
error.value = null
|
||||||
if (newFilters) {
|
if (newFilters) {
|
||||||
filters.value = newFilters
|
filters.value = newFilters
|
||||||
}
|
}
|
||||||
|
if (!isBackground) _startSafetyTimer()
|
||||||
try {
|
try {
|
||||||
taxis.value = await taxisService.getAllTaxis(filters.value)
|
taxis.value = await taxisService.getAllTaxis(filters.value)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load taxis'
|
error.value = e instanceof Error ? e.message : 'Failed to load taxis'
|
||||||
console.error('Error loading taxis:', e)
|
console.error('Error loading taxis:', e)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
if (!isBackground) {
|
||||||
|
_clearSafetyTimer()
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -135,17 +135,17 @@ function closePromoModal() {
|
|||||||
selectedPromo.value = null;
|
selectedPromo.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData(isBackground = false) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
routeStore.loadRoutes(),
|
routeStore.loadRoutes(undefined, false, isBackground),
|
||||||
couponStore.loadCoupons({ active_only: true })
|
couponStore.loadCoupons({ active_only: true }, isBackground)
|
||||||
]);
|
]);
|
||||||
updateActiveUnits();
|
updateActiveUnits();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRefocus() {
|
async function handleRefocus() {
|
||||||
// Refrescar datos en fondo
|
// Refrescar datos en fondo de manera silenciosa (isBackground = true)
|
||||||
fetchData();
|
fetchData(true);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
@ -419,7 +419,9 @@ function locateUser(): Promise<void> {
|
|||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
{ enableHighAccuracy: true, timeout: 8000, maximumAge: 30000 }
|
// maximumAge: 5 min — al volver del background, usa posición cacheada inmediatamente
|
||||||
|
// en lugar de esperar que el GPS se reactive (evita timeout al desbloquear pantalla)
|
||||||
|
{ enableHighAccuracy: false, timeout: 6000, maximumAge: 300000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -515,10 +517,19 @@ function handleImageError(event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Watch for user profile to trigger location if preference is enabled OR on auth changes
|
// Watch for user profile to trigger location if preference is enabled OR on auth changes
|
||||||
watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loaded]) => {
|
// Nota: solo se activa si NO tenemos coordenadas aún Y el mapa está cargado.
|
||||||
|
// Se evita re-disparar por TOKEN_REFRESHED porque onAuthStateChange ya no recarga
|
||||||
|
// el perfil en ese evento, así que userProfile no cambia al despertar la pantalla.
|
||||||
|
watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loaded], [prevCanLocate]) => {
|
||||||
|
// Solo localizar si: el mapa cargó, auto_location está activo,
|
||||||
|
// y NO tenemos coords aún (o el mapa se reinicializó sin coords)
|
||||||
if (canLocate && loaded && !userCoords.value) {
|
if (canLocate && loaded && !userCoords.value) {
|
||||||
console.log('SIBU | Iniciando geolocalización automática...');
|
// Extra guard: no re-disparar si auto_location no cambió (solo isLoaded cambió)
|
||||||
locateUser();
|
// Esto previene relocalización innecesaria al volver del background
|
||||||
|
if (prevCanLocate !== undefined || !userCoords.value) {
|
||||||
|
console.log('SIBU | Iniciando geolocalización automática...');
|
||||||
|
locateUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -20,12 +20,25 @@ const englishOnly = ref(false)
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Transport' })
|
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Transport' })
|
||||||
|
window.addEventListener('app-refocus', handleRefocus)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
routeStore.loadRoutes(),
|
routeStore.loadRoutes(),
|
||||||
taxiStore.loadTaxis()
|
taxiStore.loadTaxis()
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function handleRefocus() {
|
||||||
|
Promise.all([
|
||||||
|
routeStore.loadRoutes(undefined, false, true),
|
||||||
|
taxiStore.loadTaxis(undefined, true)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
import { onUnmounted } from 'vue'
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('app-refocus', handleRefocus)
|
||||||
|
})
|
||||||
|
|
||||||
const handleBusSearch = async () => {
|
const handleBusSearch = async () => {
|
||||||
await routeStore.loadRoutes({
|
await routeStore.loadRoutes({
|
||||||
originCity: originSearch.value,
|
originCity: originSearch.value,
|
||||||
@ -237,7 +250,7 @@ const correlimientos = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right flex flex-col items-end gap-2">
|
<div class="text-right flex flex-col items-end gap-2">
|
||||||
<p class="font-black text-xl text-primary">{{ taxi.shift }}</p>
|
<p class="font-black text-xl text-primary">{{ taxi.shifts?.[0] || 'Día' }}</p>
|
||||||
<FavoriteButton
|
<FavoriteButton
|
||||||
item-type="taxi"
|
item-type="taxi"
|
||||||
:item-id="taxi.id"
|
:item-id="taxi.id"
|
||||||
|
|||||||
@ -144,15 +144,15 @@ function handleOutsideClick(e: MouseEvent) {
|
|||||||
if (!target.closest('.route-selector')) dropdownOpen.value = false
|
if (!target.closest('.route-selector')) dropdownOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData(isBackground = false) {
|
||||||
await routeStore.loadRoutes()
|
await routeStore.loadRoutes(undefined, false, isBackground)
|
||||||
if (localSelectedRouteId.value) {
|
if (localSelectedRouteId.value) {
|
||||||
await scheduleStore.loadRouteSchedules(localSelectedRouteId.value)
|
await scheduleStore.loadRouteSchedules(localSelectedRouteId.value, isBackground)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRefocus() {
|
function handleRefocus() {
|
||||||
fetchData()
|
fetchData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user