fix: implement global app-refocus listener and data recovery pattern in critical views to prevent infinite loading after app suspension

This commit is contained in:
2026-03-03 15:04:16 -05:00
parent cfe9286fcb
commit df0a4397f6
6 changed files with 96 additions and 12 deletions

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, onUnmounted } from 'vue'
import { RouterView, useRoute } from "vue-router";
import { useI18n } from 'vue-i18n'
import MainLayout from "./components/layouts/MainLayout.vue";
@ -20,6 +20,24 @@ const isAuthScreen = computed(() => {
return route.path === '/login' || route.path === '/register' || route.name === 'auth'
})
let lastHiddenAt: number | null = null
function handleVisibilityChange() {
if (document.visibilityState === 'hidden') {
lastHiddenAt = Date.now()
return
}
if (document.visibilityState === 'visible' && lastHiddenAt !== null) {
const secondsAway = (Date.now() - lastHiddenAt) / 1000
// Si pasaron más de 3 segundos, asumimos que el proceso pudo ser suspendido
if (secondsAway > 3) {
console.log(`SIBU | App recuperada tras ${secondsAway.toFixed(1)}s. Disparando refocus...`)
window.dispatchEvent(new CustomEvent('app-refocus'))
}
lastHiddenAt = null
}
}
onMounted(() => {
themeStore.applyTheme()
analyticsService.logEvent({
@ -30,6 +48,11 @@ onMounted(() => {
if (authStore.isAuthenticated) {
favoritesStore.loadFavorites()
}
document.addEventListener('visibilitychange', handleVisibilityChange)
})
onUnmounted(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
</script>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { businessService } from '@/services/businessService'
import type { Business } from '@/types'
import { useRouter } from 'vue-router'
@ -54,9 +54,18 @@ async function loadBusinesses() {
}
}
function handleRefocus() {
loadBusinesses()
}
onMounted(() => {
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Discover' })
loadBusinesses()
window.addEventListener('app-refocus', handleRefocus)
})
onUnmounted(() => {
window.removeEventListener('app-refocus', handleRefocus)
})
// ── Computados

View File

@ -129,14 +129,27 @@ function closePromoModal() {
selectedPromo.value = null;
}
// Map initialization & Lifecycle
onMounted(async () => {
analyticsService.logEvent({ event_name: 'screen_view', properties: { screen_name: 'Map' } })
async function fetchData() {
await Promise.all([
routeStore.loadRoutes(),
couponStore.loadCoupons({ active_only: true })
]);
updateActiveUnits();
}
function handleRefocus() {
fetchData();
if (map.value) {
google.maps.event.trigger(map.value, 'resize');
}
}
// Map initialization & Lifecycle
onMounted(async () => {
analyticsService.logEvent({ event_name: 'screen_view', properties: { screen_name: 'Map' } })
window.addEventListener('app-refocus', handleRefocus);
await fetchData();
const queryRouteId = router.currentRoute.value.query.routeId as string;
if (queryRouteId && queryRouteId !== routeStore.selectedRouteId) {
@ -158,7 +171,6 @@ onMounted(async () => {
}
unitFetchInterval.value = setInterval(updateActiveUnits, 15000);
updateActiveUnits();
startCarousel();
// 🛰️ RESIZE FIX: Trigger map resize when app becomes visible again
@ -169,7 +181,7 @@ onUnmounted(() => {
if (unitFetchInterval.value) clearInterval(unitFetchInterval.value);
if (carouselTimer.value) clearInterval(carouselTimer.value);
document.removeEventListener('visibilitychange', handleVisibilityChange);
// NOTA: No llamamos a clearMapMarkers() para mantener la ruta si el usuario vuelve
window.removeEventListener('app-refocus', handleRefocus);
});
function handleVisibilityChange() {

View File

@ -144,8 +144,21 @@ function handleOutsideClick(e: MouseEvent) {
if (!target.closest('.route-selector')) dropdownOpen.value = false
}
async function fetchData() {
await routeStore.loadRoutes()
if (localSelectedRouteId.value) {
await scheduleStore.loadRouteSchedules(localSelectedRouteId.value)
}
}
function handleRefocus() {
fetchData()
}
onMounted(async () => {
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Schedules' })
window.addEventListener('app-refocus', handleRefocus)
await routeStore.loadRoutes()
document.addEventListener('click', handleOutsideClick)
@ -177,6 +190,7 @@ const stopWatch = watch(
onUnmounted(() => {
stopWatch()
document.removeEventListener('click', handleOutsideClick)
window.removeEventListener('app-refocus', handleRefocus)
})
</script>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTaxiStore } from '@/stores/taxi'
import { analyticsService } from '@/services/analyticsService'
@ -18,13 +18,26 @@ const onlyEnglish = ref(false)
const corregimientos = ['all', 'Boquete', 'David - Boquete', 'Boquete - David', 'Aeropuerto - Boquete']
const shifts = ['all', 'dia', 'tarde', 'noche']
function fetchData() {
taxiStore.loadTaxis()
}
function handleRefocus() {
fetchData()
}
onMounted(async () => {
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'TaxisLocales' })
window.addEventListener('app-refocus', handleRefocus)
if(taxiStore.taxis.length === 0) {
await taxiStore.loadTaxis()
await fetchData()
}
})
onUnmounted(() => {
window.removeEventListener('app-refocus', handleRefocus)
})
const filteredTaxis = computed(() => {
return taxiStore.taxis.filter(taxi => {
const matchesZone = selectedZone.value === 'all' || taxi.corregimiento === selectedZone.value

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useShuttleStore } from '@/stores/shuttle'
@ -29,12 +29,25 @@ const verDetalle = (shuttleId: string) => {
})
}
function fetchData() {
shuttleStore.loadShuttles()
}
function handleRefocus() {
fetchData()
}
onMounted(async () => {
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'ViajesTuristicos' })
window.addEventListener('app-refocus', handleRefocus)
if(shuttleStore.shuttles.length === 0) {
await shuttleStore.loadShuttles()
await fetchData()
}
})
onUnmounted(() => {
window.removeEventListener('app-refocus', handleRefocus)
})
</script>
<template>