From 9a5bfc4a7025cc6b555fe3085b8cc201a6da8bb7 Mon Sep 17 00:00:00 2001 From: Mozart299 Date: Mon, 8 Jun 2026 14:24:15 +0300 Subject: [PATCH] fix: show accurate error messages when API fails with device online - Dashboard, map, and KYA screens were showing "Couldn't connect to the internet" / cloud_off icon for all errors including API 401/500, not just actual connectivity failures - Map page was stuck on loading spinner forever when MapLoadingError was emitted (isInitializing never cleared on error) - Add isOffline flag to LessonsLoadingError state so KYA page can differentiate connectivity vs API errors - Increase ML Kit model download timeout from 30s to 3 minutes; distinguish TimeoutException from SocketException in error copy --- .../lib/src/app/dashboard/pages/dashboard_page.dart | 11 ++++++++--- src/mobile/lib/src/app/learn/bloc/kya_bloc.dart | 10 +++++++++- src/mobile/lib/src/app/learn/bloc/kya_state.dart | 8 +++++--- src/mobile/lib/src/app/learn/pages/kya_page.dart | 10 ++++++++-- src/mobile/lib/src/app/map/pages/map_page.dart | 5 ++++- .../lib/src/app/map/widgets/map_error_view.dart | 9 ++++++--- .../profile/pages/languages/select_language_page.dart | 8 +++++--- 7 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/mobile/lib/src/app/dashboard/pages/dashboard_page.dart b/src/mobile/lib/src/app/dashboard/pages/dashboard_page.dart index 822626efe7..32e16b9bfb 100644 --- a/src/mobile/lib/src/app/dashboard/pages/dashboard_page.dart +++ b/src/mobile/lib/src/app/dashboard/pages/dashboard_page.dart @@ -263,18 +263,21 @@ class _DashboardPageState extends State with UiLoggy { } if (state is DashboardLoadingError && !state.hasCache) { + final isOffline = state.isOffline; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.cloud_off, + isOffline ? Icons.cloud_off : Icons.error_outline, size: 64, color: Colors.grey, ), SizedBox(height: 16), TranslatedText( - "Couldn't connect to the internet", + isOffline + ? "Couldn't connect to the internet" + : "Couldn't load air quality data", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -283,7 +286,9 @@ class _DashboardPageState extends State with UiLoggy { ), SizedBox(height: 8), TranslatedText( - "Please check your connection and try again", + isOffline + ? "Please check your connection and try again" + : "Something went wrong. Please try again later", style: TextStyle( fontSize: 16, color: Theme.of(context).textTheme.bodyMedium?.color, diff --git a/src/mobile/lib/src/app/learn/bloc/kya_bloc.dart b/src/mobile/lib/src/app/learn/bloc/kya_bloc.dart index beb52611b4..68a13c470f 100644 --- a/src/mobile/lib/src/app/learn/bloc/kya_bloc.dart +++ b/src/mobile/lib/src/app/learn/bloc/kya_bloc.dart @@ -1,5 +1,6 @@ import 'package:airqo/src/app/learn/models/lesson_response_model.dart'; import 'package:airqo/src/app/learn/repository/kya_repository.dart'; +import 'package:airqo/src/app/shared/services/cache_manager.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:loggy/loggy.dart'; @@ -9,6 +10,8 @@ part 'kya_state.dart'; class KyaBloc extends Bloc with UiLoggy { final KyaRepository repository; + final CacheManager _cacheManager = CacheManager(); + KyaBloc(this.repository) : super(KyaInitial()) { on(_onLoadLessons); on(_onRefreshLessons); @@ -32,10 +35,14 @@ class KyaBloc extends Bloc with UiLoggy { emit(LessonsLoadingError( message: e.toString(), cachedModel: cachedModel, + isOffline: !_cacheManager.isConnected, )); } catch (cacheError) { loggy.error('Error fetching cached lessons: $cacheError'); - emit(LessonsLoadingError(message: e.toString())); + emit(LessonsLoadingError( + message: e.toString(), + isOffline: !_cacheManager.isConnected, + )); } } } @@ -62,6 +69,7 @@ class KyaBloc extends Bloc with UiLoggy { emit(LessonsLoadingError( message: e.toString(), cachedModel: cachedModel, + isOffline: !_cacheManager.isConnected, )); } } diff --git a/src/mobile/lib/src/app/learn/bloc/kya_state.dart b/src/mobile/lib/src/app/learn/bloc/kya_state.dart index de5c8a01ca..71bb64c556 100644 --- a/src/mobile/lib/src/app/learn/bloc/kya_state.dart +++ b/src/mobile/lib/src/app/learn/bloc/kya_state.dart @@ -33,12 +33,14 @@ class LessonsLoaded extends KyaState { class LessonsLoadingError extends KyaState { final String message; final LessonResponseModel? cachedModel; - + final bool isOffline; + const LessonsLoadingError({ required this.message, this.cachedModel, + this.isOffline = false, }); - + @override - List get props => [message, cachedModel]; + List get props => [message, cachedModel, isOffline]; } \ No newline at end of file diff --git a/src/mobile/lib/src/app/learn/pages/kya_page.dart b/src/mobile/lib/src/app/learn/pages/kya_page.dart index 5cc2edc755..e5834122e4 100644 --- a/src/mobile/lib/src/app/learn/pages/kya_page.dart +++ b/src/mobile/lib/src/app/learn/pages/kya_page.dart @@ -198,7 +198,11 @@ class _KyaPageState extends State with UiLoggy { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.cloud_off, size: 64, color: Colors.grey), + Icon( + state.isOffline ? Icons.cloud_off : Icons.error_outline, + size: 64, + color: Colors.grey, + ), const SizedBox(height: 16), TranslatedText( "Unable to load content", @@ -212,7 +216,9 @@ class _KyaPageState extends State with UiLoggy { Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: TranslatedText( - "Please check your connection and try again", + state.isOffline + ? "Please check your connection and try again" + : "Something went wrong. Please try again later", textAlign: TextAlign.center, style: TextStyle( fontSize: 16, diff --git a/src/mobile/lib/src/app/map/pages/map_page.dart b/src/mobile/lib/src/app/map/pages/map_page.dart index 1d129e5f02..a15d97c78d 100644 --- a/src/mobile/lib/src/app/map/pages/map_page.dart +++ b/src/mobile/lib/src/app/map/pages/map_page.dart @@ -13,6 +13,7 @@ import 'package:airqo/src/app/map/widgets/map_overlay_controls.dart'; import 'package:airqo/src/app/map/widgets/map_search_sheet.dart'; import 'package:airqo/src/app/map/widgets/map_style_picker.dart'; import 'package:airqo/src/app/other/places/bloc/google_places_bloc.dart'; +import 'package:airqo/src/app/shared/services/cache_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -348,6 +349,8 @@ class _MapScreenState extends State (markers.isEmpty || allMeasurements.isEmpty)) { _initializeWithData(response); } + } else if (state is MapLoadingError) { + if (mounted) setState(() => isInitializing = false); } }, ), @@ -383,7 +386,7 @@ class _MapScreenState extends State markers.isEmpty && allMeasurements.isEmpty && !isRetrying) - ? MapErrorView(onRetry: _retryLoading) + ? MapErrorView(onRetry: _retryLoading, isOffline: !CacheManager().isConnected) : _buildMapView(), ), ); diff --git a/src/mobile/lib/src/app/map/widgets/map_error_view.dart b/src/mobile/lib/src/app/map/widgets/map_error_view.dart index 4594449190..dffbbdd1f9 100644 --- a/src/mobile/lib/src/app/map/widgets/map_error_view.dart +++ b/src/mobile/lib/src/app/map/widgets/map_error_view.dart @@ -4,8 +4,9 @@ import 'package:flutter/material.dart'; class MapErrorView extends StatelessWidget { final VoidCallback onRetry; + final bool isOffline; - const MapErrorView({super.key, required this.onRetry}); + const MapErrorView({super.key, required this.onRetry, this.isOffline = false}); @override Widget build(BuildContext context) { @@ -13,7 +14,7 @@ class MapErrorView extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.map_outlined, size: 64, color: Colors.grey), + Icon(isOffline ? Icons.cloud_off : Icons.map_outlined, size: 64, color: Colors.grey), const SizedBox(height: 16), TranslatedText( "Unable to load map data", @@ -24,7 +25,9 @@ class MapErrorView extends StatelessWidget { ), const SizedBox(height: 8), TranslatedText( - "Please check your connection and try again", + isOffline + ? "Please check your connection and try again" + : "Something went wrong. Please try again later", style: TextStyle( fontSize: 16, color: Theme.of(context).textTheme.bodyMedium?.color), diff --git a/src/mobile/lib/src/app/profile/pages/languages/select_language_page.dart b/src/mobile/lib/src/app/profile/pages/languages/select_language_page.dart index 702cb53472..f28cabf752 100644 --- a/src/mobile/lib/src/app/profile/pages/languages/select_language_page.dart +++ b/src/mobile/lib/src/app/profile/pages/languages/select_language_page.dart @@ -70,7 +70,7 @@ class _SelectLanguagePageState extends State { if (needsMlKitDownload) { await MlKitTranslationService() .prepareModel(language.code) - .timeout(const Duration(seconds: 30)); + .timeout(const Duration(minutes: 3)); await MlKitTranslationService() .prepareCriticalStrings(language.code) .timeout(const Duration(seconds: 30)); @@ -88,10 +88,12 @@ class _SelectLanguagePageState extends State { debugPrint('Language preparation failed: $e\n$stackTrace'); if (!mounted) return; setState(() => _preparingCode = null); - final isNetworkError = e is SocketException || e is TimeoutException; + final isNetworkError = e is SocketException; final message = isNetworkError ? 'No internet connection. Please check your network and try again.' - : 'Failed to prepare language. Please try again.'; + : e is TimeoutException + ? 'Download is taking too long. Please check your connection and try again.' + : 'Failed to prepare language. Please try again.'; messenger.showSnackBar( SnackBar( content: Text(message),