fix: resolve blank screen on return from external links

This commit is contained in:
2026-03-03 20:51:17 -05:00
parent 7ff205b12a
commit af7464be43
7 changed files with 108 additions and 18 deletions

View File

@ -21,6 +21,15 @@ const isAuthScreen = computed(() => {
}) })
let lastHiddenAt: number | null = null let lastHiddenAt: number | null = null
let refocusDebounceTimer: ReturnType<typeof setTimeout> | null = null
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}`)
window.dispatchEvent(new CustomEvent('app-refocus'))
}
function handleVisibilityChange() { function handleVisibilityChange() {
if (document.visibilityState === 'hidden') { if (document.visibilityState === 'hidden') {
@ -29,10 +38,32 @@ function handleVisibilityChange() {
} }
if (document.visibilityState === 'visible' && lastHiddenAt !== null) { if (document.visibilityState === 'visible' && lastHiddenAt !== null) {
const secondsAway = (Date.now() - lastHiddenAt) / 1000 const secondsAway = (Date.now() - lastHiddenAt) / 1000
// Si pasaron más de 3 segundos, asumimos que el proceso pudo ser suspendido // Umbral bajo (1 s) para capturar retornos rápidos desde Google Maps / links externos
if (secondsAway > 3) { if (secondsAway > 1) {
console.log(`SIBU | App recuperada tras ${secondsAway.toFixed(1)}s. Disparando refocus...`) dispatchRefocus(`visibilitychange tras ${secondsAway.toFixed(1)}s`)
window.dispatchEvent(new CustomEvent('app-refocus')) }
lastHiddenAt = null
}
}
// Fallback: 'pageshow' se dispara al volver desde el bfcache (back-forward cache)
// Muy común en Safari/iOS cuando se abre un link externo y se regresa
function handlePageShow(event: PageTransitionEvent) {
if (event.persisted) {
// La página fue restaurada desde bfcache — siempre necesita refocus
dispatchRefocus('pageshow persisted (bfcache)')
}
}
// Fallback: 'focus' en window cubre casos donde visibilitychange no se dispara
// (algunos navegadores Android al volver de otra app)
let windowFocusTimer: ReturnType<typeof setTimeout> | null = null
function handleWindowFocus() {
// Solo si la pestaña estuvo oculta antes
if (lastHiddenAt !== null) {
const secondsAway = (Date.now() - lastHiddenAt) / 1000
if (secondsAway > 1) {
dispatchRefocus(`window focus tras ${secondsAway.toFixed(1)}s`)
} }
lastHiddenAt = null lastHiddenAt = null
} }
@ -49,10 +80,16 @@ onMounted(() => {
favoritesStore.loadFavorites() favoritesStore.loadFavorites()
} }
document.addEventListener('visibilitychange', handleVisibilityChange) document.addEventListener('visibilitychange', handleVisibilityChange)
window.addEventListener('pageshow', handlePageShow)
window.addEventListener('focus', handleWindowFocus)
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange) document.removeEventListener('visibilitychange', handleVisibilityChange)
window.removeEventListener('pageshow', handlePageShow)
window.removeEventListener('focus', handleWindowFocus)
if (refocusDebounceTimer) clearTimeout(refocusDebounceTimer)
if (windowFocusTimer) clearTimeout(windowFocusTimer)
}) })
</script> </script>

View File

@ -26,12 +26,25 @@ export const useShuttleStore = defineStore('shuttle', () => {
} }
} }
// Recarga silenciosa: no activa el spinner (para refocus desde background)
async function silentReload() {
if (shuttles.value.length === 0) { return loadShuttles() }
error.value = null
try {
shuttles.value = await shuttlesService.getAllShuttles(filters.value)
} catch (e) {
console.error('Error silent-reloading shuttles:', e)
// No mostrar error si ya hay datos
}
}
return { return {
shuttles, shuttles,
isLoading, isLoading,
error, error,
filters, filters,
loadShuttles, loadShuttles,
silentReload,
} }
}, { }, {
persist: { persist: {

View File

@ -31,6 +31,18 @@ export const useTaxiStore = defineStore('taxi', () => {
loadTaxis() loadTaxis()
} }
// Recarga silenciosa: no activa el spinner (para refocus desde background)
async function silentReload() {
if (taxis.value.length === 0) { return loadTaxis() }
error.value = null
try {
taxis.value = await taxisService.getAllTaxis(filters.value)
} catch (e) {
console.error('Error silent-reloading taxis:', e)
// No mostrar error si ya hay datos
}
}
return { return {
taxis, taxis,
isLoading, isLoading,
@ -38,6 +50,7 @@ export const useTaxiStore = defineStore('taxi', () => {
filters, filters,
loadTaxis, loadTaxis,
setFilters, setFilters,
silentReload,
} }
}, { }, {
persist: { persist: {

View File

@ -12,7 +12,7 @@ import AuthGuard from '@/components/common/AuthGuard.vue'
const router = useRouter() const router = useRouter()
const { t } = useI18n() const { t } = useI18n()
const businesses = ref<Business[]>([]) const businesses = ref<Business[]>([])
const isLoading = ref(true) const isLoading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const searchQuery = ref('') const searchQuery = ref('')
const selectedCategory = ref('Todas') const selectedCategory = ref('Todas')
@ -41,21 +41,28 @@ function catIcon(cat: string) {
return CATEGORY_META[cat]?.icon ?? 'place' return CATEGORY_META[cat]?.icon ?? 'place'
} }
async function loadBusinesses() { async function loadBusinesses(silent = false) {
isLoading.value = true // Modo 'silencioso': si ya tenemos datos, no mostrar spinner — solo refrescar en fondo
if (!silent || businesses.value.length === 0) {
isLoading.value = true
}
error.value = null error.value = null
try { try {
businesses.value = await businessService.getAllBusinesses() businesses.value = await businessService.getAllBusinesses()
} catch (e) { } catch (e) {
console.error('Error loading businesses:', e) console.error('Error loading businesses:', e)
error.value = t('discover.error') // Solo mostrar error si no hay datos previos
if (businesses.value.length === 0) {
error.value = t('discover.error')
}
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
} }
function handleRefocus() { function handleRefocus() {
loadBusinesses() // Recarga silenciosa: no congela la UI si ya hay datos visibles
loadBusinesses(true)
} }
onMounted(() => { onMounted(() => {
@ -179,7 +186,7 @@ function resetFilters() {
<div v-else-if="error" class="state-center"> <div v-else-if="error" class="state-center">
<span class="material-icons" style="font-size: 3.5rem; color: #ef4444; opacity: 0.8; margin-bottom: 0.5rem;">error_outline</span> <span class="material-icons" style="font-size: 3.5rem; color: #ef4444; opacity: 0.8; margin-bottom: 0.5rem;">error_outline</span>
<p style="font-weight: 600; color: var(--text-secondary);">{{ error }}</p> <p style="font-weight: 600; color: var(--text-secondary);">{{ error }}</p>
<button class="cta-btn" style="margin-top: 1rem;" @click="loadBusinesses"> <button class="cta-btn" style="margin-top: 1rem;" @click="loadBusinesses()">
<span class="material-icons">refresh</span> <span class="material-icons">refresh</span>
{{ t('common.retry') }} {{ t('common.retry') }}
</button> </button>

View File

@ -143,10 +143,28 @@ async function fetchData() {
updateActiveUnits(); updateActiveUnits();
} }
function handleRefocus() { async function handleRefocus() {
// Refrescar datos en fondo
fetchData(); fetchData();
await nextTick();
if (map.value) { if (map.value) {
google.maps.event.trigger(map.value, 'resize'); // El mapa sigue vivo — solo redimensionar y actualizar
try {
google.maps.event.trigger(map.value, 'resize');
} catch (_) { /* ignorar si google no disponible */ }
updateActiveUnits();
} else {
// El mapa fue destruido por el browser al suspender la pestaña — reinicializar
console.log('SIBU | Mapa perdido tras refocus, reinicializando...');
if (isLoaded.value) {
await initializeMap();
} else {
const unwatch = watch(isLoaded, async (loaded) => {
if (loaded) { await initializeMap(); unwatch(); }
});
}
} }
} }
@ -154,7 +172,7 @@ function handleRefocus() {
onMounted(async () => { onMounted(async () => {
analyticsService.logEvent({ event_name: 'screen_view', properties: { screen_name: 'Map' } }) analyticsService.logEvent({ event_name: 'screen_view', properties: { screen_name: 'Map' } })
window.addEventListener('app-refocus', handleRefocus); window.addEventListener('app-refocus', handleRefocus);
await fetchData(); await fetchData();
const queryRouteId = router.currentRoute.value.query.routeId as string; const queryRouteId = router.currentRoute.value.query.routeId as string;
@ -179,7 +197,6 @@ onMounted(async () => {
unitFetchInterval.value = setInterval(updateActiveUnits, 15000); unitFetchInterval.value = setInterval(updateActiveUnits, 15000);
startCarousel(); startCarousel();
// 🛰️ RESIZE FIX: Trigger map resize when app becomes visible again
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
}); });
@ -192,8 +209,9 @@ onUnmounted(() => {
function handleVisibilityChange() { function handleVisibilityChange() {
if (document.visibilityState === 'visible' && map.value) { if (document.visibilityState === 'visible' && map.value) {
console.log('SIBU | App visible, redimensionando mapa...'); try {
google.maps.event.trigger(map.value, 'resize'); google.maps.event.trigger(map.value, 'resize');
} catch (_) { /* ignorar */ }
updateActiveUnits(); updateActiveUnits();
} }
} }

View File

@ -23,7 +23,8 @@ function fetchData() {
} }
function handleRefocus() { function handleRefocus() {
fetchData() // Recarga silenciosa: no congela la UI si ya hay datos
taxiStore.silentReload()
} }
onMounted(async () => { onMounted(async () => {

View File

@ -34,7 +34,8 @@ function fetchData() {
} }
function handleRefocus() { function handleRefocus() {
fetchData() // Recarga silenciosa: no congela la UI si ya hay datos
shuttleStore.silentReload()
} }
onMounted(async () => { onMounted(async () => {