diff --git a/apps/expo/app/(app)/_layout.tsx b/apps/expo/app/(app)/_layout.tsx index 81ed80ec82..7c7fff7121 100644 --- a/apps/expo/app/(app)/_layout.tsx +++ b/apps/expo/app/(app)/_layout.tsx @@ -199,7 +199,7 @@ export default function AppLayout() { options={{ headerShown: false, presentation: 'card', - animation: 'slide_from_bottom', + animation: 'default', }} /> - + (null); const { t } = useTranslation(); const { alerts, loading } = useWeatherAlerts(); @@ -22,47 +19,32 @@ export function WeatherAlertsTile() { }; return ( - <> - - - - + + + - } - rightView={ - - - {loading ? '...' : t('weather.activeCount', { count: weatherAlertCount })} - - - - } - item={{ - title: t('weather.weatherAlerts'), - }} - onPress={handlePress} - target="Cell" - index={0} - removeSeparator={Platform.OS === 'ios'} - /> - - + + } + rightView={ + + + {loading ? '...' : t('weather.activeCount', { count: weatherAlertCount })} + + + + } + item={{ + title: t('weather.weatherAlerts'), + }} + onPress={handlePress} + target="Cell" + index={0} + removeSeparator={Platform.OS === 'ios'} + /> ); } diff --git a/apps/expo/features/weather/hooks/useWeatherAlert.ts b/apps/expo/features/weather/hooks/useWeatherAlert.ts index 3278bec3ae..2ea6e79866 100644 --- a/apps/expo/features/weather/hooks/useWeatherAlert.ts +++ b/apps/expo/features/weather/hooks/useWeatherAlert.ts @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react-native'; import { getWeatherData } from 'expo-app/features/weather/lib/weatherService'; import { useAtomValue } from 'jotai'; import { useEffect, useState } from 'react'; @@ -232,40 +233,63 @@ export function generateAlerts({ export function useWeatherAlerts() { const activeLocation = useAtomValue(activeLocationAtom); + // Use primitive values as effect dependencies to avoid re-running on new + // object references emitted by the derived Jotai atom's .find() call. + const locationId = activeLocation?.id; + const locationName = activeLocation?.name; + const [alerts, setAlerts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { - if (!activeLocation?.id) { + if (!locationId) { setLoading(false); return; } - const locationId = activeLocation.id; + let cancelled = false; async function fetchAlerts() { setLoading(true); setError(null); + Sentry.addBreadcrumb({ + category: 'weather', + message: 'Fetching weather alerts', + level: 'info', + data: { locationId, locationName }, + }); + try { const data = await getWeatherData(locationId); + if (cancelled) return; + const formatted = generateAlerts({ // safe-cast: getWeatherData returns WeatherApiForecastResponse; WeatherApiData is a structural subset used only by this alert generator. data: data as unknown as WeatherApiData, - activeLocation, + activeLocation: { name: locationName }, }); setAlerts(formatted); } catch (err) { + if (cancelled) return; console.error('Weather alerts error:', err); setError('Failed to fetch weather alerts'); + Sentry.captureException(err, { + tags: { feature: 'weatherAlerts', action: 'fetchAlerts' }, + extra: { locationId, locationName }, + }); } finally { - setLoading(false); + if (!cancelled) setLoading(false); } } fetchAlerts(); - }, [activeLocation]); + + return () => { + cancelled = true; + }; + }, [locationId, locationName]); return { alerts,