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"> <script setup lang="ts">
import { computed, onMounted } from 'vue' import { computed, onMounted, onUnmounted } from 'vue'
import { RouterView, useRoute } from "vue-router"; import { RouterView, useRoute } from "vue-router";
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import MainLayout from "./components/layouts/MainLayout.vue"; import MainLayout from "./components/layouts/MainLayout.vue";
@ -20,6 +20,24 @@ const isAuthScreen = computed(() => {
return route.path === '/login' || route.path === '/register' || route.name === 'auth' 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(() => { onMounted(() => {
themeStore.applyTheme() themeStore.applyTheme()
analyticsService.logEvent({ analyticsService.logEvent({
@ -30,6 +48,11 @@ onMounted(() => {
if (authStore.isAuthenticated) { if (authStore.isAuthenticated) {
favoritesStore.loadFavorites() favoritesStore.loadFavorites()
} }
document.addEventListener('visibilitychange', handleVisibilityChange)
})
onUnmounted(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}) })
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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