diff --git a/app/lib/providers/wallets_provider.dart b/app/lib/providers/wallets_provider.dart index b5acbe791..0abd41604 100644 --- a/app/lib/providers/wallets_provider.dart +++ b/app/lib/providers/wallets_provider.dart @@ -81,7 +81,7 @@ class WalletsNotifier extends StateNotifier> { } void reloadBalances() async { - if (!_reload) return await TFChainService.disconnect(); + if (!_reload) return; if (!_loading) { final chainUrl = Globals().chainUrl; await _mutex.protect(() async { diff --git a/app/lib/screens/dao_screen.dart b/app/lib/screens/dao_screen.dart index 918ce089d..ab7513bad 100644 --- a/app/lib/screens/dao_screen.dart +++ b/app/lib/screens/dao_screen.dart @@ -6,6 +6,7 @@ import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:threebotlogin/widgets/dao/proposals.dart'; import 'package:threebotlogin/services/tfchain_service.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; class DaoPage extends StatefulWidget { const DaoPage({super.key}); @@ -21,13 +22,32 @@ class _DaoPageState extends State with SingleTickerProviderStateMixin { bool failed = false; late final TabController _tabController; + @override + void initState() { + super.initState(); + loadProposals(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + Future loadProposals() async { - setState(() { - loading = true; - failed = false; - }); + _setLoadingState(); try { + final connectivityResult = await (Connectivity().checkConnectivity()); + + if (connectivityResult.contains(ConnectivityResult.none)) { + _handleFailure( + 'No internet connection. Please check your network.', + ); + return; + } + final proposals = await getProposals().timeout( const Duration(minutes: 1), onTimeout: () { @@ -35,64 +55,63 @@ class _DaoPageState extends State with SingleTickerProviderStateMixin { }, ); - logger.i('Proposals loaded successfully'); - - if (activeList.isNotEmpty) activeList.clear(); - if (inactiveList.isNotEmpty) inactiveList.clear(); - activeList.addAll(proposals['activeProposals']!); - inactiveList.addAll(proposals['inactiveProposals']!); - setState(() { - loading = false; - failed = false; - }); + _handleSuccess(proposals); } on TimeoutException catch (e) { - logger.e('Loading proposals timed out: $e'); - if (context.mounted) { - final timeoutFailure = SnackBar( - content: Text( - 'Loading proposals timed out. Please try again.', - 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(timeoutFailure); - } - setState(() { - loading = false; - failed = true; - }); - } catch (e) { - logger.e('Failed to load proposals due to $e'); - if (context.mounted) { - final loadingProposalFailure = SnackBar( - content: Text( - 'Failed to load proposals', - 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(loadingProposalFailure); - } - setState(() { - loading = false; - failed = true; - }); + _handleFailure( + 'Loading proposals timed out. Please check your network', + error: e, + ); + } on Exception catch (e) { + _handleFailure( + 'Failed to load proposals. Please try again.', + error: e, + ); } } - @override - void initState() { - super.initState(); - loadProposals(); - _tabController = TabController(length: 2, vsync: this); + void _setLoadingState() { + setState(() { + loading = true; + failed = false; + }); + } + + void _handleSuccess(Map?> proposals) { + activeList.clear(); + inactiveList.clear(); + activeList.addAll(proposals['activeProposals'] ?? []); + inactiveList.addAll(proposals['inactiveProposals'] ?? []); + + setState(() { + loading = false; + failed = false; + }); + } + + void _handleFailure(String userMessage, {Object? error}) { + if (error != null) { + logger.e('Load proposals 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; + }); } @override @@ -102,9 +121,10 @@ class _DaoPageState extends State with SingleTickerProviderStateMixin { content = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), - const SizedBox(height: 15), + const SizedBox(height: 16), Text( 'Loading Proposals...', style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -117,66 +137,61 @@ class _DaoPageState extends State with SingleTickerProviderStateMixin { content = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 15), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Try Again'), - onPressed: () async { - setState(() { - failed = false; - loading = true; - }); - await loadProposals(); + onPressed: () { + loadProposals(); }, ), + const SizedBox(height: 16), ], ), ); } else { - content = DefaultTabController( - length: 2, - child: Column( - children: [ - PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: TabBar( - controller: _tabController, - labelColor: Theme.of(context).colorScheme.primary, - indicatorColor: Theme.of(context).colorScheme.primary, - unselectedLabelColor: Theme.of(context).colorScheme.onSurface, - dividerColor: Theme.of(context).scaffoldBackgroundColor, - labelStyle: Theme.of(context).textTheme.titleLarge, - unselectedLabelStyle: Theme.of(context).textTheme.titleMedium, - tabs: const [ - Tab(text: 'Active'), - Tab(text: 'Executable'), - ], - ), - ), - ), - Expanded( - child: TabBarView( + content = Column( + children: [ + PreferredSize( + preferredSize: const Size.fromHeight(50.0), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: TabBar( controller: _tabController, - children: [ - RefreshIndicator( - onRefresh: loadProposals, - child: ProposalsWidget( - proposals: activeList, - active: true, - )), - RefreshIndicator( - onRefresh: loadProposals, - child: ProposalsWidget( - proposals: inactiveList, - )), + labelColor: Theme.of(context).colorScheme.primary, + indicatorColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: Theme.of(context).colorScheme.onSurface, + dividerColor: Theme.of(context).scaffoldBackgroundColor, + labelStyle: Theme.of(context).textTheme.titleLarge, + unselectedLabelStyle: Theme.of(context).textTheme.titleMedium, + tabs: const [ + Tab(text: 'Active'), + Tab(text: 'Executable'), ], ), ), - ], - ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + RefreshIndicator( + onRefresh: loadProposals, + child: ProposalsWidget( + proposals: activeList, + active: true, + )), + RefreshIndicator( + onRefresh: loadProposals, + child: ProposalsWidget( + proposals: inactiveList, + active: false, + )), + ], + ), + ), + ], ); } return LayoutDrawer(titleText: 'Dao', content: content); diff --git a/app/lib/screens/farm_screen.dart b/app/lib/screens/farm_screen.dart index 266942fd2..0484400e2 100644 --- a/app/lib/screens/farm_screen.dart +++ b/app/lib/screens/farm_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:registrar_client/models/farm.dart' as registrarFarm; @@ -14,6 +15,7 @@ import 'package:threebotlogin/services/tfchain_service.dart'; import 'package:threebotlogin/widgets/add_farm.dart'; import 'package:threebotlogin/widgets/farm_item.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; class FarmScreen extends ConsumerStatefulWidget { const FarmScreen({super.key}); @@ -51,45 +53,84 @@ class _FarmScreenState extends ConsumerState } Future listFarms() async { - try { - setState(() { - loading = true; - failed = false; - registrarClient = null; - v3Farms.clear(); - v4Farms.clear(); - }); - await listWallets(); + _setLoadingState(); - if (wallets.isEmpty) return; - await listV3FarmsAndNodes(); - await listV4FarmsAndNodes(); - setState(() { - loading = false; - }); - } catch (e) { - logger.e('Failed to get farms due to $e'); - if (context.mounted) { - final loadingFarmsFailure = SnackBar( - content: Text( - 'Failed to load farms', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.errorContainer), - ), - duration: const Duration(seconds: 3), + try { + 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; } - setState(() { - failed = true; - loading = false; - }); + await _fetchAllFarmData().timeout( + const Duration(minutes: 2), + onTimeout: () { + throw TimeoutException('Loading farm data timed out'); + }, + ); + + _handleSuccess(); + } on TimeoutException catch (e) { + _handleFailure('Loading farms timed out. Please check your network.', error: e); + } on Exception catch (e) { + _handleFailure('Failed to load farms due to an unexpected error.', + error: e); } } + Future _fetchAllFarmData() async { + v3Farms.clear(); + v4Farms.clear(); + registrarClient = null; + + await listWallets(); + + if (wallets.isEmpty) return; + await listV3FarmsAndNodes(); + await listV4FarmsAndNodes(); + } + + void _setLoadingState() { + setState(() { + loading = true; + failed = false; + v3Farms.clear(); + v4Farms.clear(); + registrarClient = null; + }); + } + + void _handleSuccess() { + setState(() { + loading = false; + failed = false; + }); + logger.i('Farm data loaded successfully.'); + } + + void _handleFailure(String userMessage, {Object? 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; + }); + } + listV3FarmsAndNodes() async { final Map twinIdWallets = {}; await Future.wait(wallets.map((w) async { @@ -220,9 +261,10 @@ class _FarmScreenState extends ConsumerState mainWidget = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), - const SizedBox(height: 15), + const SizedBox(height: 16), Text( 'Loading Farms...', style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -235,67 +277,60 @@ class _FarmScreenState extends ConsumerState mainWidget = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 15), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Try Again'), - onPressed: () async { - setState(() { - failed = false; - loading = true; - }); - await listFarms(); + onPressed: () { + listFarms(); }, ), + const SizedBox(height: 16), ], ), ); } else { - mainWidget = DefaultTabController( - length: 2, - child: Column( - children: [ - PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: TabBar( - controller: _tabController, - labelColor: Theme.of(context).colorScheme.primary, - indicatorColor: Theme.of(context).colorScheme.primary, - unselectedLabelColor: - Theme.of(context).colorScheme.onSurface, - dividerColor: Theme.of(context).scaffoldBackgroundColor, - labelStyle: Theme.of(context).textTheme.titleLarge, - unselectedLabelStyle: - Theme.of(context).textTheme.titleMedium, - tabs: const [ - Tab(text: 'V3'), - Tab(text: 'V4'), - ], - ), - ), + mainWidget = Column( + children: [ + PreferredSize( + preferredSize: const Size.fromHeight(50.0), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: TabBar( + controller: _tabController, + labelColor: Theme.of(context).colorScheme.primary, + indicatorColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: Theme.of(context).colorScheme.onSurface, + dividerColor: Theme.of(context).scaffoldBackgroundColor, + labelStyle: Theme.of(context).textTheme.titleLarge, + unselectedLabelStyle: Theme.of(context).textTheme.titleMedium, + tabs: const [ + Tab(text: 'V3'), + Tab(text: 'V4'), + ], ), - Expanded( - child: TabBarView(controller: _tabController, children: [ - RefreshIndicator( - onRefresh: listFarms, - child: listFarmsWidget(v3Farms, false), - ), - RefreshIndicator( - onRefresh: listFarms, - child: listFarmsWidget(v4Farms, true), - ), - ]), - ) - ], - )); + ), + ), + Expanded( + child: TabBarView(controller: _tabController, children: [ + RefreshIndicator( + onRefresh: listFarms, + child: listFarmsWidget(v3Farms, false), + ), + RefreshIndicator( + onRefresh: listFarms, + child: listFarmsWidget(v4Farms, true), + ), + ]), + ) + ], + ); } return LayoutDrawer( titleText: 'Farming', content: mainWidget, - appBarActions: loading + appBarActions: loading || failed ? [] : [ IconButton( @@ -321,7 +356,7 @@ class _FarmScreenState extends ConsumerState )); } - _addFarm(Farm farm) async { + _addFarm(Farm farm) { setState(() { _tabController.index == 0 ? v3Farms.add(farm) : v4Farms.add(farm); }); diff --git a/app/lib/services/tfchain_service.dart b/app/lib/services/tfchain_service.dart index 2f075c659..0e7032753 100644 --- a/app/lib/services/tfchain_service.dart +++ b/app/lib/services/tfchain_service.dart @@ -71,14 +71,12 @@ Future getBalance(String chainUrl, String address) async { Future getBalanceByClient(TFChain.Client client) async { await client.connect(); final balance = (await client.balances.getMyBalance())!.data.free; - await client.disconnect(); return balance / BigInt.from(10).pow(7); } Future getTwinIdByClient(TFChain.Client client) async { await client.connect(); final twinId = await client.twins.getMyTwinId(); - await client.disconnect(); return twinId ?? 0; } @@ -93,7 +91,6 @@ Future getTwinIdByQueryClient(String address) async { final client = TFChain.QueryClient(chainUrl); await client.connect(); final twinId = await client.twins.getTwinIdByAccountId(address: address); - await client.disconnect(); return twinId ?? 0; } @@ -107,10 +104,6 @@ Future>> getProposals() async { return proposals; } catch (e) { throw Exception('Failed to get DAO proposals due to $e'); - } finally { - if (proposals != null) { - await client.disconnect(); - } } } @@ -123,8 +116,6 @@ Future getProposalVotes(String hash) async { return votes; } catch (e) { throw Exception('Failed to get dao proposals votes due to $e'); - } finally { - await client.disconnect(); } } @@ -138,8 +129,6 @@ Future getProposalProgress( return progress; } catch (e) { throw Exception('Failed to get dao proposals progress due to $e'); - } finally { - await client.disconnect(); } } @@ -153,8 +142,6 @@ Future vote(bool vote, String hash, int farmId, String seed) async { return daoVotes; } catch (e) { throw Exception('Failed to vote due to $e'); - } finally { - await client.disconnect(); } } @@ -192,8 +179,6 @@ activateAccount(String tfchainSeed) async { await client.twins.create(relay: relayUrl, pk: []); } catch (e) { throw Exception('Failed to activate account due to $e'); - } finally { - await client.disconnect(); } } @@ -214,8 +199,6 @@ Future createFarm( return farm; } catch (e) { throw Exception('Failed to create farm due to $e'); - } finally { - await client.disconnect(); } } @@ -234,8 +217,6 @@ Future addStellarAddress( .addStellarAddress(farmId: farmId, stellarAddress: stellarAddress); } catch (e) { throw Exception('Failed to add stellar address to farm due to $e'); - } finally { - await client.disconnect(); } } @@ -247,24 +228,14 @@ Future transfer(String secret, String dest, String amount) async { await client.balances.transfer(address: dest, amount: double.parse(amount)); } catch (e) { throw Exception('Failed to transfer due to $e'); - } finally { - await client.disconnect(); } } -Future disconnect() async { - final chainUrl = Globals().chainUrl; - final client = TFChain.QueryClient(chainUrl); - await client.connect(); - await client.disconnect(); -} - Future swapToStellar(String secret, String target, BigInt amount) async { final chainUrl = Globals().chainUrl; final client = TFChain.Client(chainUrl, secret, 'sr25519'); await client.connect(); await client.bridge.swapToStellar(target: target, amount: amount); - await client.disconnect(); } Future getMemo(String address) async { @@ -280,8 +251,6 @@ Future> getCouncilProposals(String chainUrl) async { return proposals; } catch (e) { throw Exception('Failed to get council proposals due to $e'); - } finally { - await client.disconnect(); } } @@ -293,8 +262,6 @@ Future> getCouncilMembers(String chainUrl) async { return members; } catch (e) { throw Exception('Failed to get council members due to $e'); - } finally { - await client.disconnect(); } } @@ -307,8 +274,6 @@ Future councilVote( return votes; } catch (e) { throw Exception('Failed to vote due to $e'); - } finally { - await client.disconnect(); } } @@ -320,8 +285,6 @@ Future getCouncilProposalVotes(String chainUrl, String hash) async { return votes; } catch (e) { throw Exception('Failed to get council proposals votes due to $e'); - } finally { - await client.disconnect(); } } @@ -333,7 +296,5 @@ Future getTFTPrice(String chainUrl) async { return price; } catch (e) { throw Exception('Failed to get TFT price due to $e'); - } finally { - await client.disconnect(); } } diff --git a/app/pubspec.lock b/app/pubspec.lock index 8e27de48a..d19ae3813 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -262,6 +262,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: "direct main" description: @@ -1119,6 +1135,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" octo_image: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 707dcb247..94978f047 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: flag: ^7.0.0 flutter_local_notifications: ^19.0.0 background_fetch: ^1.3.8 + connectivity_plus: ^6.1.4 dev_dependencies: flutter_test: sdk: flutter