diff --git a/app/lib/apps/news/news_screen.dart b/app/lib/apps/news/news_screen.dart index 4e3d9dad9..fdec922f7 100644 --- a/app/lib/apps/news/news_screen.dart +++ b/app/lib/apps/news/news_screen.dart @@ -1,9 +1,12 @@ +import 'dart:async'; import 'dart:convert'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:http/http.dart' as http; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:xml2json/xml2json.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -22,6 +25,7 @@ class _NewsScreenState extends State { final PagingController> _pagingController = PagingController(firstPageKey: 0); final String newsUrl = Globals().newsUrl; + bool _isLoading = false; @override void initState() { @@ -30,8 +34,25 @@ class _NewsScreenState extends State { } Future getArticles(int pageKey) async { + if (_isLoading) return; + + _isLoading = true; try { - final response = await http.get(Uri.parse(newsUrl)); + final connectivityResult = await Connectivity().checkConnectivity(); + if (connectivityResult.contains(ConnectivityResult.none)) { + throw Exception('No internet connection. Please check your network.'); + } + + final response = await http.get(Uri.parse(newsUrl)).timeout( + const Duration(seconds: 30), + onTimeout: () { + throw TimeoutException('Loading news feed timed out'); + }, + ); + + if (response.statusCode != 200) { + throw Exception('Failed to load news feed: ${response.statusCode}'); + } xml2json.parse(response.body); var data = json.decode(xml2json.toGData()); @@ -47,11 +68,48 @@ class _NewsScreenState extends State { isLastPage ? _pagingController.appendLastPage(newArticles) : _pagingController.appendPage(newArticles, pageKey + 1); - } catch (e) { - _pagingController.error = e; + + _isLoading = false; + } on TimeoutException catch (e) { + _handleError( + 'Loading news feed timed out. Please check your connection.', e); + } on Exception catch (e) { + _handleError( + e.toString().contains('No internet connection') + ? 'No internet connection. Please check your network.' + : 'Failed to load news feed. Please try again.', + e); + } + } + + void _handleError(String message, Exception error) { + logger.e('News feed error: $message', error: error); + + _isLoading = false; + + _pagingController.error = message; + + if (mounted && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ), + ); } } + Future _refreshNews() async { + _pagingController.refresh(); + return Future.delayed(const Duration(milliseconds: 300)); + } + @override void dispose() { _pagingController.dispose(); @@ -63,7 +121,7 @@ class _NewsScreenState extends State { return LayoutDrawer( titleText: 'News', content: RefreshIndicator( - onRefresh: () async => _pagingController.refresh(), + onRefresh: _refreshNews, child: PagedListView>( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate>( @@ -82,6 +140,36 @@ class _NewsScreenState extends State { ], ), ), + firstPageErrorIndicatorBuilder: (context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: _refreshNews, + icon: const Icon(Icons.refresh), + label: const Text('Try Again'), + ), + ], + ), + ), + noItemsFoundIndicatorBuilder: (context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.article_outlined, + size: 48, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 8), + Text( + 'No articles found', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ), ), ), ), diff --git a/app/lib/screens/signing/signing_mixin.dart b/app/lib/screens/signing/signing_mixin.dart index ae50e5efa..420774ee1 100644 --- a/app/lib/screens/signing/signing_mixin.dart +++ b/app/lib/screens/signing/signing_mixin.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -14,6 +17,7 @@ mixin SigningMixin on ConsumerState { bool isLoading = false; bool isLoadingWallets = false; bool loadingFailed = false; + String? walletLoadingError; String? walletError; String? destUrlError; @@ -132,20 +136,41 @@ mixin SigningMixin on ConsumerState { } Future checkWalletsListed() async { + final connectivityResult = await Connectivity().checkConnectivity(); + + if (connectivityResult.contains(ConnectivityResult.none)) { + _handleWalletLoadingFailure( + 'No internet connection. Please check your network.'); + return; + } + final walletsNotifierRef = ref.read(walletsNotifier.notifier); if (!walletsNotifierRef.isListed) { setState(() { isLoadingWallets = true; loadingFailed = false; + walletLoadingError = null; selectedWallet = null; }); + try { - await walletsNotifierRef.list(); + await walletsNotifierRef.list().timeout( + const Duration(seconds: 30), + onTimeout: () { + throw TimeoutException('Loading wallets timed out'); + }, + ); + } on TimeoutException catch (e) { + logger.e('Wallet loading timed out: $e'); + if (mounted) { + _handleWalletLoadingFailure( + 'Loading wallets timed out. Please check your connection.'); + } } catch (e) { + logger.e('Failed to load wallets: $e'); if (mounted) { - setState(() { - loadingFailed = true; - }); + _handleWalletLoadingFailure( + 'Failed to load wallets. Please try again.'); } } finally { if (mounted) { @@ -157,10 +182,42 @@ mixin SigningMixin on ConsumerState { } } + void _handleWalletLoadingFailure(String errorMessage) { + setState(() { + isLoadingWallets = false; + loadingFailed = true; + walletLoadingError = errorMessage; + }); + + if (mounted && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + errorMessage, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ), + ); + } + } + Future retryLoadingWallets() async { + final connectivityResult = await Connectivity().checkConnectivity(); + + if (connectivityResult.contains(ConnectivityResult.none)) { + _handleWalletLoadingFailure( + 'No internet connection. Please check your network.'); + return; + } + setState(() { isLoadingWallets = true; loadingFailed = false; + walletLoadingError = null; selectedWallet = null; }); @@ -168,7 +225,13 @@ mixin SigningMixin on ConsumerState { walletsNotifierRef.clear(); try { - await walletsNotifierRef.list(); + await walletsNotifierRef.list().timeout( + const Duration(seconds: 30), + onTimeout: () { + throw TimeoutException('Loading wallets timed out'); + }, + ); + if (mounted && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -181,28 +244,17 @@ mixin SigningMixin on ConsumerState { ), ); } + } on TimeoutException catch (e) { + logger.e('Wallet loading timed out on retry: $e'); + if (mounted) { + _handleWalletLoadingFailure( + 'Loading wallets timed out. Please check your connection.'); + } } catch (e) { + logger.e('Failed to load wallets on retry: $e'); if (mounted) { - setState(() { - loadingFailed = true; - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Failed to load wallets. Please check your connection.', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.errorContainer), - ), - duration: const Duration(seconds: 3), - action: SnackBarAction( - label: 'Retry', - textColor: Theme.of(context).colorScheme.errorContainer, - onPressed: () { - retryLoadingWallets(); - }, - ), - ), - ); + _handleWalletLoadingFailure( + 'Failed to load wallets. Please try again.'); } } finally { if (mounted) { @@ -258,8 +310,8 @@ mixin SigningMixin on ConsumerState { child: Text(wallet.name, style: Theme.of(context) .textTheme - .bodyMedium - !.copyWith( + .bodyMedium! + .copyWith( color: Theme.of(context).colorScheme.onSurface, )), diff --git a/app/lib/screens/wallets/wallet_screen.dart b/app/lib/screens/wallets/wallet_screen.dart index 8ed72e801..879b41438 100644 --- a/app/lib/screens/wallets/wallet_screen.dart +++ b/app/lib/screens/wallets/wallet_screen.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:threebotlogin/apps/wallet/wallet_config.dart'; @@ -108,43 +111,89 @@ class _WalletScreenState extends ConsumerState { ); } - listMyWallets() async { + Future listMyWallets() async { + _setLoadingState(); + + try { + final connectivityResult = await (Connectivity().checkConnectivity()); + + if (connectivityResult.contains(ConnectivityResult.none)) { + _handleFailure( + 'No internet connection. Please check your network.', + ); + return; + } + + await _fetchWalletData().timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException('Loading wallets timed out'); + }, + ); + + _handleSuccess(); + } on TimeoutException catch (e) { + _handleFailure( + 'Loading wallets timed out. Please check your network.', + error: e, + ); + } on Exception catch (e) { + _handleFailure( + 'Failed to load wallets. Please try again.', + error: e, + ); + } + } + + void _setLoadingState() { setState(() { loading = true; failed = false; }); - try { - await ref.read(walletsNotifier.notifier).list(); - wallets = ref.read(walletsNotifier); - if (wallets.isEmpty) { - await _addInitialWallet(); - } - } catch (e) { - setState(() { - failed = true; - }); - logger.e('Failed to get wallets due to $e'); - if (context.mounted) { - final loadingFarmsFailure = SnackBar( - content: Text( - 'Failed to load wallets', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.errorContainer), - ), - duration: const Duration(seconds: 3), - ); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); - } - } finally { - setState(() { - loading = false; - }); + } + + Future _fetchWalletData() async { + await ref.read(walletsNotifier.notifier).list(); + wallets = ref.read(walletsNotifier); + + if (wallets.isEmpty) { + await _addInitialWallet(); } } + void _handleSuccess() { + setState(() { + loading = false; + failed = false; + }); + } + + void _handleFailure(String userMessage, {Object? error}) { + if (error != null) { + logger.e('Load wallets failed', error: error); + } + + if (mounted) { + final errorSnackbar = SnackBar( + content: Text( + userMessage, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar); + } + + setState(() { + loading = false; + failed = true; + }); + } + _openAddWalletOverlay() { showModalBottomSheet( isScrollControlled: true, @@ -170,30 +219,36 @@ class _WalletScreenState extends ConsumerState { } Future handleRefresh() async { + _setLoadingState(); + try { - loading = true; - await ref.refresh(walletsNotifier.notifier).list(); - return; - } catch (e) { - failed = true; - logger.e('Failed to get wallets due to $e'); - if (context.mounted) { - final loadingFarmsFailure = SnackBar( - content: Text( - 'Failed to load wallets', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.errorContainer), - ), - duration: const Duration(seconds: 3), + final connectivityResult = await (Connectivity().checkConnectivity()); + + if (connectivityResult.contains(ConnectivityResult.none)) { + _handleFailure( + 'No internet connection. Please check your network.', ); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + return; } - } finally { - loading = false; - setState(() {}); + + await _fetchWalletData().timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException('Refreshing wallets timed out'); + }, + ); + + _handleSuccess(); + } on TimeoutException catch (e) { + _handleFailure( + 'Refreshing wallets timed out. Please check your network.', + error: e, + ); + } on Exception catch (e) { + _handleFailure( + 'Failed to refresh wallets. Please try again.', + error: e, + ); } } }