From cc43b89f7f37aec22bb53b572c776be1eda92644 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 14 May 2025 06:37:25 +0300 Subject: [PATCH 01/10] Add notifications screen --- app/lib/apps/notifications/notifications.dart | 42 ++++++++++++ .../notifications_user_data.dart | 7 ++ app/lib/jrouter.dart | 10 ++- app/lib/main.dart | 2 +- app/lib/screens/notifications_screen.dart | 67 +++++++++++++++++++ app/lib/screens/registered_screen.dart | 4 ++ app/lib/services/background_service.dart | 33 +++++++-- app/lib/widgets/layout_drawer.dart | 11 +++ 8 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 app/lib/apps/notifications/notifications.dart create mode 100644 app/lib/apps/notifications/notifications_user_data.dart create mode 100644 app/lib/screens/notifications_screen.dart diff --git a/app/lib/apps/notifications/notifications.dart b/app/lib/apps/notifications/notifications.dart new file mode 100644 index 00000000..b482b145 --- /dev/null +++ b/app/lib/apps/notifications/notifications.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/app.dart'; +import 'package:threebotlogin/apps/farmers/farmers_user_data.dart'; +import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/go_home_event.dart'; +import 'package:threebotlogin/screens/notifications_screen.dart'; + +class Notifications implements App { + static final Notifications _singleton = Notifications._internal(); + static const Widget _notificationsWidget = NotificationsScreen(); + + factory Notifications() { + return _singleton; + } + + Notifications._internal(); + + @override + Future widget() async { + return _notificationsWidget; + } + + @override + void clearData() { + clearAllData(); + } + + @override + bool emailVerificationRequired() { + return true; + } + + @override + bool pinRequired() { + return true; + } + + @override + void back() { + Events().emit(GoHomeEvent()); + } +} // TODO Implement this library. diff --git a/app/lib/apps/notifications/notifications_user_data.dart b/app/lib/apps/notifications/notifications_user_data.dart new file mode 100644 index 00000000..ef3dc33a --- /dev/null +++ b/app/lib/apps/notifications/notifications_user_data.dart @@ -0,0 +1,7 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +Future?> getNotificationSettings() async { + final prefs = await SharedPreferences.getInstance(); + var notifications = prefs.getStringList('notifications'); + return notifications; +} diff --git a/app/lib/jrouter.dart b/app/lib/jrouter.dart index 15e31eb5..4bd646da 100644 --- a/app/lib/jrouter.dart +++ b/app/lib/jrouter.dart @@ -6,7 +6,7 @@ import 'package:threebotlogin/apps/wallet/wallet.dart'; import 'package:threebotlogin/screens/identity_verification_screen.dart'; import 'package:threebotlogin/screens/preference_screen.dart'; import 'package:threebotlogin/screens/registered_screen.dart'; - +import 'package:threebotlogin/apps/notifications/notifications.dart'; import 'apps/farmers/farmers.dart'; import 'apps/news/news.dart'; import 'apps/sign/sign.dart'; @@ -95,6 +95,14 @@ class JRouter { view: await Sign().widget(), ), app: Sign()), + AppInfo( + route: Route( + path: '/notifications', + name: 'Notifications', + icon: Icons.notifications, + view: await Notifications().widget(), + ), + app: null), ]; } diff --git a/app/lib/main.dart b/app/lib/main.dart index 2054121a..df41efe4 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -71,7 +71,7 @@ Future main() async { ), (String taskId) async { logger.i('[BackgroundFetch] Task: $taskId'); - await checkNodeStatus(); + await checkNodeStatus(taskId); BackgroundFetch.finish(taskId); }, (String taskId) async { diff --git a/app/lib/screens/notifications_screen.dart b/app/lib/screens/notifications_screen.dart new file mode 100644 index 00000000..5031f5d1 --- /dev/null +++ b/app/lib/screens/notifications_screen.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:threebotlogin/widgets/layout_drawer.dart'; + +class NotificationsScreen extends StatefulWidget { + const NotificationsScreen({super.key}); + + @override + _NotificationsScreenState createState() => _NotificationsScreenState(); +} + +class _NotificationsScreenState extends State { + bool _nodeStatusNotificationEnabled = true; + + static const String _nodeStatusNotificationEnabledKey = + 'nodeStatusNotificationEnabled'; + + @override + void initState() { + super.initState(); + _loadNotificationPreference(); + } + + void _loadNotificationPreference() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _nodeStatusNotificationEnabled = + prefs.getBool(_nodeStatusNotificationEnabledKey) ?? true; + }); + } + + void _setNodeStatusNotification(bool newValue) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_nodeStatusNotificationEnabledKey, newValue); + } + + @override + Widget build(BuildContext context) { + return LayoutDrawer( + titleText: 'Notifications', + content: _buildNotificationSettings(), + ); + } + + Widget _buildNotificationSettings() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + title: const Text('Enable node status notifications'), + value: _nodeStatusNotificationEnabled, + onChanged: (bool newValue) { + setState(() { + _nodeStatusNotificationEnabled = newValue; + }); + _setNodeStatusNotification(newValue); + }, + secondary: const Icon(Icons.notifications), + ), + ], + ), + ); + } +} diff --git a/app/lib/screens/registered_screen.dart b/app/lib/screens/registered_screen.dart index c9d28947..b5cbdcd8 100644 --- a/app/lib/screens/registered_screen.dart +++ b/app/lib/screens/registered_screen.dart @@ -117,6 +117,10 @@ class _RegisteredScreenState extends State children: [ HomeCardWidget( name: 'Settings', icon: Icons.settings, pageNumber: 6), + HomeCardWidget( + name: 'Notifications', + icon: Icons.notifications, + pageNumber: 9), ], ), const SizedBox(height: 40), diff --git a/app/lib/services/background_service.dart b/app/lib/services/background_service.dart index 45a7af78..5da3dd5a 100644 --- a/app/lib/services/background_service.dart +++ b/app/lib/services/background_service.dart @@ -1,6 +1,11 @@ import 'package:background_fetch/background_fetch.dart'; import 'package:threebotlogin/services/nodes_check_service.dart'; import 'notification_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:threebotlogin/helpers/logger.dart'; + +const String _nodeStatusNotificationEnabledKey = + 'nodeStatusNotificationEnabled'; void backgroundFetchHeadlessTask(HeadlessTask task) async { final String taskId = task.taskId; @@ -10,14 +15,28 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { BackgroundFetch.finish(taskId); return; } - await checkNodeStatus(); + final prefs = await SharedPreferences.getInstance(); + final bool notificationsEnabled = + prefs.getBool(_nodeStatusNotificationEnabledKey) ?? true; - BackgroundFetch.finish(taskId); + logger.i( + 'Background Fetch Headless Task: $taskId, Notifications Enabled: $notificationsEnabled'); + + if (!notificationsEnabled) { + logger.i( + '[BackgroundFetch] Node status notifications are disabled. Finishing task: $taskId'); + BackgroundFetch.finish(taskId); + return; + } + await checkNodeStatus(taskId); } -Future checkNodeStatus() async { +Future checkNodeStatus(String taskId) async { final offlineNodes = await NodeCheckService.pingNodesInBackground(); - if (offlineNodes.isEmpty) return; + if (offlineNodes.isEmpty) { + BackgroundFetch.finish(taskId); + return; + } final now = DateTime.now().millisecondsSinceEpoch; final sevenDaysAgoTimestamp = @@ -33,7 +52,10 @@ Future checkNodeStatus() async { return downtime.inMinutes % checkInterval.inMinutes < 15; }).toList(); - if (nodesToNotify.isEmpty) return; + if (nodesToNotify.isEmpty) { + BackgroundFetch.finish(taskId); + return; + } const groupKey = 'offline_nodes'; final StringBuffer bodyBuffer = StringBuffer(); @@ -54,6 +76,7 @@ Future checkNodeStatus() async { body: bodyBuffer.toString().trim(), groupKey: groupKey, ); + BackgroundFetch.finish(taskId); } Duration _getCheckInterval(Duration downtime) { diff --git a/app/lib/widgets/layout_drawer.dart b/app/lib/widgets/layout_drawer.dart index 9f6c5f91..06f76e2a 100644 --- a/app/lib/widgets/layout_drawer.dart +++ b/app/lib/widgets/layout_drawer.dart @@ -198,6 +198,17 @@ class _LayoutDrawerState extends State { globals.tabController.animateTo(7); }, ), + ListTile( + minLeadingWidth: 10, + leading: const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.notifications, size: 18)), + title: const Text('Notifications'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(9); + }, + ), ], ), ), From 92b3f631492d3233f33e2c8eca6768e24cba3cfc Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Sun, 18 May 2025 10:09:41 +0300 Subject: [PATCH 02/10] Add loading state, edit icon, refactor checkNodeStatus --- app/lib/screens/notifications_screen.dart | 58 ++++++++++----- app/lib/services/background_service.dart | 91 ++++++++++++----------- 2 files changed, 87 insertions(+), 62 deletions(-) diff --git a/app/lib/screens/notifications_screen.dart b/app/lib/screens/notifications_screen.dart index 5031f5d1..4a7ab55a 100644 --- a/app/lib/screens/notifications_screen.dart +++ b/app/lib/screens/notifications_screen.dart @@ -10,8 +10,8 @@ class NotificationsScreen extends StatefulWidget { } class _NotificationsScreenState extends State { - bool _nodeStatusNotificationEnabled = true; - + late bool loading; + late bool _nodeStatusNotificationEnabled; static const String _nodeStatusNotificationEnabledKey = 'nodeStatusNotificationEnabled'; @@ -22,10 +22,14 @@ class _NotificationsScreenState extends State { } void _loadNotificationPreference() async { + setState(() { + loading = true; + }); final prefs = await SharedPreferences.getInstance(); setState(() { _nodeStatusNotificationEnabled = prefs.getBool(_nodeStatusNotificationEnabledKey) ?? true; + loading = false; }); } @@ -45,23 +49,39 @@ class _NotificationsScreenState extends State { Widget _buildNotificationSettings() { return Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SwitchListTile( - title: const Text('Enable node status notifications'), - value: _nodeStatusNotificationEnabled, - onChanged: (bool newValue) { - setState(() { - _nodeStatusNotificationEnabled = newValue; - }); - _setNodeStatusNotification(newValue); - }, - secondary: const Icon(Icons.notifications), - ), - ], - ), + child: loading + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 15), + Text( + 'Loading notifications settings...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ), + ], + ), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + title: const Text('Enable node status notifications'), + value: _nodeStatusNotificationEnabled, + onChanged: (bool newValue) { + setState(() { + _nodeStatusNotificationEnabled = newValue; + }); + _setNodeStatusNotification(newValue); + }, + secondary: const Icon(Icons.monitor_heart), + ), + ], + ), ); } } diff --git a/app/lib/services/background_service.dart b/app/lib/services/background_service.dart index 5da3dd5a..cbb8324d 100644 --- a/app/lib/services/background_service.dart +++ b/app/lib/services/background_service.dart @@ -1,4 +1,5 @@ import 'package:background_fetch/background_fetch.dart'; +import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/services/nodes_check_service.dart'; import 'notification_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -32,51 +33,55 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { } Future checkNodeStatus(String taskId) async { - final offlineNodes = await NodeCheckService.pingNodesInBackground(); - if (offlineNodes.isEmpty) { + try { + final offlineNodes = await NodeCheckService.pingNodesInBackground(); + + if (offlineNodes.isEmpty) return; + + final StringBuffer bodyBuffer = StringBuffer(); + final List nodesToNotify = []; + final now = DateTime.now(); + final nowInMs = now.millisecondsSinceEpoch; + final sevenDaysAgoTimestampMs = + now.subtract(const Duration(days: 7)).millisecondsSinceEpoch; + + for (final node in offlineNodes) { + final nodeUpdatedAtMs = node.updatedAt! * 1000; + + if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) continue; + + final downtime = Duration(milliseconds: nowInMs - nodeUpdatedAtMs); + + final checkInterval = _getCheckInterval(downtime); + + bool passesIntervalCheck = false; + if (downtime.inMinutes > 0 && checkInterval.inMinutes > 0) { + passesIntervalCheck = downtime.inMinutes % checkInterval.inMinutes < 15; + } + + if (passesIntervalCheck) { + nodesToNotify.add(node); + final formattedDowntime = _formatDowntime(downtime); + bodyBuffer + .writeln('Node ${node.nodeId}: offline for $formattedDowntime'); + } + } + + if (nodesToNotify.isEmpty) return; + + await NotificationService().showNotification( + id: nodesToNotify.hashCode, + title: nodesToNotify.length == 1 + ? 'Node Alert ๐Ÿšจ' + : '${nodesToNotify.length} Nodes Offline ๐Ÿšจ', + body: bodyBuffer.toString().trim(), + groupKey: 'offline_nodes', + ); + } catch (e) { + logger.e('Error in checkNodeStatus for task $taskId: $e'); + } finally { BackgroundFetch.finish(taskId); - return; - } - - final now = DateTime.now().millisecondsSinceEpoch; - final sevenDaysAgoTimestamp = - DateTime.now().subtract(const Duration(days: 7)).millisecondsSinceEpoch; - - final nodesToNotify = offlineNodes.where((node) { - final nodeUpdatedAtMs = node.updatedAt! * 1000; - if (nodeUpdatedAtMs <= sevenDaysAgoTimestamp) return false; - - final downtime = Duration(milliseconds: now - nodeUpdatedAtMs); - final checkInterval = _getCheckInterval(downtime); - - return downtime.inMinutes % checkInterval.inMinutes < 15; - }).toList(); - - if (nodesToNotify.isEmpty) { - BackgroundFetch.finish(taskId); - return; } - - const groupKey = 'offline_nodes'; - final StringBuffer bodyBuffer = StringBuffer(); - - for (final node in nodesToNotify) { - final nodeUpdatedAtMs = node.updatedAt! * 1000; - final downtime = - _formatDowntime(Duration(milliseconds: now - nodeUpdatedAtMs)); - - bodyBuffer.writeln('Node ${node.nodeId}: offline for $downtime'); - } - - await NotificationService().showNotification( - id: nodesToNotify.hashCode, - title: nodesToNotify.length == 1 - ? 'Node Alert ๐Ÿšจ' - : '${nodesToNotify.length} Nodes Offline ๐Ÿšจ', - body: bodyBuffer.toString().trim(), - groupKey: groupKey, - ); - BackgroundFetch.finish(taskId); } Duration _getCheckInterval(Duration downtime) { From b6e6263fd133bda2112644b3a0bb4a585c3d63d4 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Sun, 18 May 2025 10:26:02 +0300 Subject: [PATCH 03/10] Replace node status notification logic to notifications user data --- .../notifications_user_data.dart | 22 +++++++++++++++++++ app/lib/screens/notifications_screen.dart | 21 +++++------------- app/lib/services/background_service.dart | 9 ++------ 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/lib/apps/notifications/notifications_user_data.dart b/app/lib/apps/notifications/notifications_user_data.dart index ef3dc33a..e4f572d3 100644 --- a/app/lib/apps/notifications/notifications_user_data.dart +++ b/app/lib/apps/notifications/notifications_user_data.dart @@ -1,7 +1,29 @@ import 'package:shared_preferences/shared_preferences.dart'; +import 'package:threebotlogin/helpers/logger.dart'; + +const String nodeStatusNotificationEnabledKey = 'nodeStatusNotificationEnabled'; Future?> getNotificationSettings() async { final prefs = await SharedPreferences.getInstance(); var notifications = prefs.getStringList('notifications'); return notifications; } + +Future isNodeStatusNotificationEnabled() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(nodeStatusNotificationEnabledKey) ?? true; + } catch (e) { + return true; + } +} + +Future setNodeStatusNotificationEnabled(bool value) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(nodeStatusNotificationEnabledKey, value); + print('Notification preference saved: $value'); + } catch (e) { + logger.e('Error saving notification preference: $e'); + } +} diff --git a/app/lib/screens/notifications_screen.dart b/app/lib/screens/notifications_screen.dart index 4a7ab55a..5bef1331 100644 --- a/app/lib/screens/notifications_screen.dart +++ b/app/lib/screens/notifications_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:threebotlogin/apps/notifications/notifications_user_data.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; class NotificationsScreen extends StatefulWidget { @@ -10,10 +10,8 @@ class NotificationsScreen extends StatefulWidget { } class _NotificationsScreenState extends State { - late bool loading; + bool loading = true; late bool _nodeStatusNotificationEnabled; - static const String _nodeStatusNotificationEnabledKey = - 'nodeStatusNotificationEnabled'; @override void initState() { @@ -22,22 +20,13 @@ class _NotificationsScreenState extends State { } void _loadNotificationPreference() async { + final bool enabled = await isNodeStatusNotificationEnabled(); setState(() { - loading = true; - }); - final prefs = await SharedPreferences.getInstance(); - setState(() { - _nodeStatusNotificationEnabled = - prefs.getBool(_nodeStatusNotificationEnabledKey) ?? true; + _nodeStatusNotificationEnabled = enabled; loading = false; }); } - void _setNodeStatusNotification(bool newValue) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_nodeStatusNotificationEnabledKey, newValue); - } - @override Widget build(BuildContext context) { return LayoutDrawer( @@ -76,7 +65,7 @@ class _NotificationsScreenState extends State { setState(() { _nodeStatusNotificationEnabled = newValue; }); - _setNodeStatusNotification(newValue); + setNodeStatusNotificationEnabled(newValue); }, secondary: const Icon(Icons.monitor_heart), ), diff --git a/app/lib/services/background_service.dart b/app/lib/services/background_service.dart index cbb8324d..2d6261b6 100644 --- a/app/lib/services/background_service.dart +++ b/app/lib/services/background_service.dart @@ -1,13 +1,10 @@ import 'package:background_fetch/background_fetch.dart'; +import 'package:threebotlogin/apps/notifications/notifications_user_data.dart'; import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/services/nodes_check_service.dart'; import 'notification_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:threebotlogin/helpers/logger.dart'; -const String _nodeStatusNotificationEnabledKey = - 'nodeStatusNotificationEnabled'; - void backgroundFetchHeadlessTask(HeadlessTask task) async { final String taskId = task.taskId; final bool timeout = task.timeout; @@ -16,9 +13,7 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { BackgroundFetch.finish(taskId); return; } - final prefs = await SharedPreferences.getInstance(); - final bool notificationsEnabled = - prefs.getBool(_nodeStatusNotificationEnabledKey) ?? true; + final bool notificationsEnabled = await isNodeStatusNotificationEnabled(); logger.i( 'Background Fetch Headless Task: $taskId, Notifications Enabled: $notificationsEnabled'); From 6da1244afddd281980d1c18fc651f44f003685d4 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Sun, 18 May 2025 10:50:44 +0300 Subject: [PATCH 04/10] Ignore reflectable files, remove print --- .gitignore | 2 +- app/lib/apps/notifications/notifications_user_data.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8b7172f8..7b0bccf3 100644 --- a/.gitignore +++ b/.gitignore @@ -350,7 +350,7 @@ backend/services/__pycache__/socket.cpython-39.pyc .vscode # reflected files -app/lib/main.reflectable.dart +*.reflectable.dart # pubSpec override app/pubspec_overrides.yaml diff --git a/app/lib/apps/notifications/notifications_user_data.dart b/app/lib/apps/notifications/notifications_user_data.dart index e4f572d3..3c65da1a 100644 --- a/app/lib/apps/notifications/notifications_user_data.dart +++ b/app/lib/apps/notifications/notifications_user_data.dart @@ -22,7 +22,6 @@ Future setNodeStatusNotificationEnabled(bool value) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(nodeStatusNotificationEnabledKey, value); - print('Notification preference saved: $value'); } catch (e) { logger.e('Error saving notification preference: $e'); } From 4310c8b4ddcbbdd8e699e49f00e3e5f19f7f909d Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Sun, 18 May 2025 11:28:20 +0300 Subject: [PATCH 05/10] Add contract check service --- app/lib/services/contract_check_service.dart | 43 ++++++++++++++++++++ app/lib/services/gridproxy_service.dart | 14 +++++++ 2 files changed, 57 insertions(+) create mode 100644 app/lib/services/contract_check_service.dart diff --git a/app/lib/services/contract_check_service.dart b/app/lib/services/contract_check_service.dart new file mode 100644 index 00000000..f505a3db --- /dev/null +++ b/app/lib/services/contract_check_service.dart @@ -0,0 +1,43 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gridproxy_client/models/contracts.dart'; +import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/providers/wallets_provider.dart'; +import 'package:threebotlogin/services/gridproxy_service.dart'; +import 'package:threebotlogin/services/tfchain_service.dart'; + +final contractCheckServiceProvider = Provider((ref) { + return ContractCheckService(ref); +}); + +class ContractCheckService { + final Ref _ref; + + ContractCheckService(this._ref); + + Future> checkContractState() async { + List allContracts = []; + + try { + final walletsNotifierInstance = _ref.read(walletsNotifier.notifier); + + await walletsNotifierInstance.waitUntilListed(); + + final List wallets = _ref.read(walletsNotifier); + + for (final w in wallets) { + final twinId = await getTwinId(w.tfchainSecret); + if (twinId != 0) { + List contracts = await getGracePeriodContractsByTwinId(twinId); + allContracts.addAll(contracts); + } else { + logger.w('[ContractCheckService] Could not get valid twinId for wallet: ${w.name}'); + } + } + return allContracts; + } catch (e) { + logger.e('[ContractCheckService] Error checking contract state: $e'); + return []; + } + } +} diff --git a/app/lib/services/gridproxy_service.dart b/app/lib/services/gridproxy_service.dart index 1b215d32..f6626457 100644 --- a/app/lib/services/gridproxy_service.dart +++ b/app/lib/services/gridproxy_service.dart @@ -1,4 +1,5 @@ import 'package:gridproxy_client/gridproxy_client.dart'; +import 'package:gridproxy_client/models/contracts.dart'; import 'package:gridproxy_client/models/nodes.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/main.reflectable.dart'; @@ -65,3 +66,16 @@ Future isFarmNameAvailable(String name) async { throw Exception('Failed to get farms due to $e'); } } + +Future> getGracePeriodContractsByTwinId(int twinId) async { + try { + initializeReflectable(); + final gridproxyUrl = Globals().gridproxyUrl; + GridProxyClient client = GridProxyClient(gridproxyUrl); + final contracts = + await client.contracts.list(ContractInfoQueryParams(twin_id: twinId, state: ContractState.GracePeriod)); + return contracts; + } catch (e) { + throw Exception('Failed to get contracts due to $e'); + } +} From 20ee532b5c3d9506f48d5ac6e8dcad7bbbc946c5 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Tue, 20 May 2025 16:39:03 +0300 Subject: [PATCH 06/10] Add contracts check service, edit main.dart to call backgroundFetchHeadlessTask instead of checkNodeStatus, remove finish logic from inside the background service, edit notification service, edit notification dialogue, add more logs --- .../notifications_user_data.dart | 29 ++- app/lib/main.dart | 28 +-- app/lib/screens/notifications_screen.dart | 24 +- app/lib/screens/registered_screen.dart | 25 +- app/lib/services/background_service.dart | 123 +++++++--- app/lib/services/contract_check_service.dart | 11 +- app/lib/services/notification_service.dart | 229 +++++++++++------- 7 files changed, 309 insertions(+), 160 deletions(-) diff --git a/app/lib/apps/notifications/notifications_user_data.dart b/app/lib/apps/notifications/notifications_user_data.dart index 3c65da1a..944e76b5 100644 --- a/app/lib/apps/notifications/notifications_user_data.dart +++ b/app/lib/apps/notifications/notifications_user_data.dart @@ -1,7 +1,8 @@ import 'package:shared_preferences/shared_preferences.dart'; -import 'package:threebotlogin/helpers/logger.dart'; const String nodeStatusNotificationEnabledKey = 'nodeStatusNotificationEnabled'; +const String _contractNotificationsEnabledKey = + 'contract_notifications_enabled'; Future?> getNotificationSettings() async { final prefs = await SharedPreferences.getInstance(); @@ -10,19 +11,21 @@ Future?> getNotificationSettings() async { } Future isNodeStatusNotificationEnabled() async { - try { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(nodeStatusNotificationEnabledKey) ?? true; - } catch (e) { - return true; - } + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(nodeStatusNotificationEnabledKey) ?? true; } Future setNodeStatusNotificationEnabled(bool value) async { - try { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(nodeStatusNotificationEnabledKey, value); - } catch (e) { - logger.e('Error saving notification preference: $e'); - } + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(nodeStatusNotificationEnabledKey, value); +} + +Future isContractNotificationEnabled() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_contractNotificationsEnabledKey) ?? true; +} + +Future setContractNotificationEnabled(bool enabled) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_contractNotificationsEnabledKey, enabled); } diff --git a/app/lib/main.dart b/app/lib/main.dart index df41efe4..dfd32461 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -46,19 +46,6 @@ Future main() async { await NotificationService().initNotification(); BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); - - bool initDone = await getInitDone(); - String? doubleName = await getDoubleName(); - - await setGlobalValues(); - bool registered = doubleName != null; - - runApp( - ProviderScope( - child: MyApp(initDone: initDone, registered: registered), - ), - ); - BackgroundFetch.configure( BackgroundFetchConfig( minimumFetchInterval: 15, @@ -71,14 +58,25 @@ Future main() async { ), (String taskId) async { logger.i('[BackgroundFetch] Task: $taskId'); - await checkNodeStatus(taskId); - BackgroundFetch.finish(taskId); + backgroundFetchHeadlessTask(HeadlessTask(taskId, false)); }, (String taskId) async { logger.i('[BackgroundFetch] Timeout: $taskId'); BackgroundFetch.finish(taskId); }, ); + + bool initDone = await getInitDone(); + String? doubleName = await getDoubleName(); + + await setGlobalValues(); + bool registered = doubleName != null; + + runApp( + ProviderScope( + child: MyApp(initDone: initDone, registered: registered), + ), + ); } Future setGlobalValues() async { diff --git a/app/lib/screens/notifications_screen.dart b/app/lib/screens/notifications_screen.dart index 5bef1331..93e2ad24 100644 --- a/app/lib/screens/notifications_screen.dart +++ b/app/lib/screens/notifications_screen.dart @@ -12,17 +12,21 @@ class NotificationsScreen extends StatefulWidget { class _NotificationsScreenState extends State { bool loading = true; late bool _nodeStatusNotificationEnabled; + late bool _contractNotificationsEnabled; @override void initState() { super.initState(); - _loadNotificationPreference(); + _loadNotificationPreferences(); } - void _loadNotificationPreference() async { - final bool enabled = await isNodeStatusNotificationEnabled(); + void _loadNotificationPreferences() async { + final bool nodeEnabled = await isNodeStatusNotificationEnabled(); + final bool contractEnabled = + await isContractNotificationEnabled(); setState(() { - _nodeStatusNotificationEnabled = enabled; + _nodeStatusNotificationEnabled = nodeEnabled; + _contractNotificationsEnabled = contractEnabled; loading = false; }); } @@ -69,6 +73,18 @@ class _NotificationsScreenState extends State { }, secondary: const Icon(Icons.monitor_heart), ), + SwitchListTile( + title: + const Text('Enable contract grace period notifications'), + value: _contractNotificationsEnabled, + onChanged: (bool newValue) { + setState(() { + _contractNotificationsEnabled = newValue; + }); + setContractNotificationEnabled(newValue); + }, + secondary: const Icon(Icons.description), + ), ], ), ); diff --git a/app/lib/screens/registered_screen.dart b/app/lib/screens/registered_screen.dart index b5cbdcd8..33654617 100644 --- a/app/lib/screens/registered_screen.dart +++ b/app/lib/screens/registered_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/widgets/chat_widget.dart'; import 'package:threebotlogin/widgets/home_card.dart'; import 'package:threebotlogin/widgets/home_logo.dart'; +import 'package:threebotlogin/services/notification_service.dart'; // Import NotificationService class RegisteredScreen extends StatefulWidget { static final RegisteredScreen _singleton = RegisteredScreen._internal(); @@ -123,7 +124,29 @@ class _RegisteredScreenState extends State pageNumber: 9), ], ), - const SizedBox(height: 40), + const SizedBox(height: 20), // Added some spacing + // --- START: Notification Test Button --- + ElevatedButton( + onPressed: () { + // Manually trigger a contract alert notification + NotificationService().showNotification( + id: 'test_contract_1', + title: 'Test Contract Alert! ๐Ÿงช', + body: 'This is a test contract notification.', + groupKey: 'contract_alerts', + ); + // Manually trigger an offline node notification + NotificationService().showNotification( + id: 'test_node_1', + title: 'Test Node Offline! ๐Ÿšจ', + body: 'This is a test node offline notification.', + groupKey: 'offline_nodes', + ); + }, + child: const Text('Show Test Notifications'), + ), + const SizedBox(height: 20), // Added some spacing + // --- END: Notification Test Button --- const Row( children: [Spacer(), CrispChatbot(), SizedBox(width: 20)], ) diff --git a/app/lib/services/background_service.dart b/app/lib/services/background_service.dart index 2d6261b6..bd6283b9 100644 --- a/app/lib/services/background_service.dart +++ b/app/lib/services/background_service.dart @@ -1,6 +1,9 @@ import 'package:background_fetch/background_fetch.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gridproxy_client/models/contracts.dart'; import 'package:threebotlogin/apps/notifications/notifications_user_data.dart'; import 'package:threebotlogin/models/farm.dart'; +import 'package:threebotlogin/services/contract_check_service.dart'; import 'package:threebotlogin/services/nodes_check_service.dart'; import 'notification_service.dart'; import 'package:threebotlogin/helpers/logger.dart'; @@ -9,29 +12,91 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { final String taskId = task.taskId; final bool timeout = task.timeout; - if (timeout) { - BackgroundFetch.finish(taskId); - return; - } - final bool notificationsEnabled = await isNodeStatusNotificationEnabled(); + final container = ProviderContainer(); - logger.i( - 'Background Fetch Headless Task: $taskId, Notifications Enabled: $notificationsEnabled'); + try { + if (timeout) { + logger.w('[BackgroundFetch] Task timed out: $taskId'); + BackgroundFetch.finish(taskId); + return; + } - if (!notificationsEnabled) { logger.i( - '[BackgroundFetch] Node status notifications are disabled. Finishing task: $taskId'); + '[BackgroundFetch] Headless Task: $taskId started. Time: ${DateTime.now()}'); + + // Run contract and node checks concurrently + await Future.wait([ + _checkContractsAndNotify(container, taskId), + _checkNodesAndNotify(taskId), + ]); + } catch (e, stack) { + logger.e('[BackgroundFetch] Error during task $taskId: $e', + error: e, stackTrace: stack); + } finally { + container.dispose(); BackgroundFetch.finish(taskId); - return; + logger.i('[BackgroundFetch] Task finished: $taskId'); } - await checkNodeStatus(taskId); } -Future checkNodeStatus(String taskId) async { +Future _checkContractsAndNotify( + ProviderContainer container, String taskId) async { try { - final offlineNodes = await NodeCheckService.pingNodesInBackground(); + final List allContractsInGracePeriod = await container + .read(contractCheckServiceProvider) + .checkContractsState(); + + if (allContractsInGracePeriod.isNotEmpty) { + final bool contractNotificationsEnabled = + await isContractNotificationEnabled(); + logger.i( + '[ContractsCheck] Contracts in grace period: ${allContractsInGracePeriod.length}. Contract Notifications enabled: $contractNotificationsEnabled'); + + if (contractNotificationsEnabled) { + String notificationBody = + 'You have ${allContractsInGracePeriod.length} contract(s) in grace period.'; + final String contractIds = + allContractsInGracePeriod.map((c) => c.contract_id).join(', '); + notificationBody += '\nContract IDs: $contractIds'; + + await NotificationService().showNotification( + id: 'contract_grace_period', + title: 'Contract Grace Period Alert! โณ', + body: notificationBody, + groupKey: 'contract_alerts', + ); + } + } + } catch (e, stack) { + logger.e( + '[ContractsCheck] Error during contracts check for task $taskId: $e', + error: e, + stackTrace: stack); + rethrow; + } +} - if (offlineNodes.isEmpty) return; +Future _checkNodesAndNotify(String taskId) async { + try { + final bool nodeNotificationsEnabled = + await isNodeStatusNotificationEnabled(); + logger.i( + '[NodesCheck] Node Notifications Enabled: $nodeNotificationsEnabled for task $taskId'); + + if (!nodeNotificationsEnabled) { + logger.i( + '[NodesCheck] Node notifications are disabled by user setting. Exiting _checkNodesAndNotify for task $taskId.'); + return; + } + + final offlineNodes = await NodeCheckService.pingNodesInBackground(); + if (offlineNodes.isEmpty) { + logger.i( + '[NodesCheck] No raw offline nodes found from pingNodesInBackground(). Exiting _checkNodesAndNotify for task $taskId.'); + return; + } + logger.i( + '[NodesCheck] Found ${offlineNodes.length} raw offline nodes for task $taskId.'); final StringBuffer bodyBuffer = StringBuffer(); final List nodesToNotify = []; @@ -43,39 +108,37 @@ Future checkNodeStatus(String taskId) async { for (final node in offlineNodes) { final nodeUpdatedAtMs = node.updatedAt! * 1000; - if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) continue; + // Filter out nodes updated more than 7 days ago + // if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) continue; final downtime = Duration(milliseconds: nowInMs - nodeUpdatedAtMs); - final checkInterval = _getCheckInterval(downtime); bool passesIntervalCheck = false; if (downtime.inMinutes > 0 && checkInterval.inMinutes > 0) { passesIntervalCheck = downtime.inMinutes % checkInterval.inMinutes < 15; } - - if (passesIntervalCheck) { - nodesToNotify.add(node); - final formattedDowntime = _formatDowntime(downtime); - bodyBuffer - .writeln('Node ${node.nodeId}: offline for $formattedDowntime'); - } + // if (passesIntervalCheck) { + nodesToNotify.add(node); + final formattedDowntime = _formatDowntime(downtime); + bodyBuffer.writeln('Node ${node.nodeId}: offline for $formattedDowntime'); + // } } if (nodesToNotify.isEmpty) return; await NotificationService().showNotification( - id: nodesToNotify.hashCode, + id: 'offline_nodes_alert', title: nodesToNotify.length == 1 ? 'Node Alert ๐Ÿšจ' : '${nodesToNotify.length} Nodes Offline ๐Ÿšจ', body: bodyBuffer.toString().trim(), groupKey: 'offline_nodes', ); - } catch (e) { - logger.e('Error in checkNodeStatus for task $taskId: $e'); - } finally { - BackgroundFetch.finish(taskId); + } catch (e, stack) { + logger.e('[NodesCheck] Error in node check for task $taskId: $e', + error: e, stackTrace: stack); + rethrow; } } @@ -98,7 +161,9 @@ String _formatDowntime(Duration duration) { return '${duration.inDays} days'; } else if (duration.inHours > 0) { return '${duration.inHours} hours'; - } else { + } else if (duration.inMinutes > 0) { return '${duration.inMinutes} minutes'; + } else { + return '${duration.inSeconds} seconds'; } } diff --git a/app/lib/services/contract_check_service.dart b/app/lib/services/contract_check_service.dart index f505a3db..416a1160 100644 --- a/app/lib/services/contract_check_service.dart +++ b/app/lib/services/contract_check_service.dart @@ -15,23 +15,22 @@ class ContractCheckService { ContractCheckService(this._ref); - Future> checkContractState() async { + Future> checkContractsState() async { List allContracts = []; try { final walletsNotifierInstance = _ref.read(walletsNotifier.notifier); - await walletsNotifierInstance.waitUntilListed(); final List wallets = _ref.read(walletsNotifier); + if (wallets.isEmpty) return []; for (final w in wallets) { final twinId = await getTwinId(w.tfchainSecret); if (twinId != 0) { - List contracts = await getGracePeriodContractsByTwinId(twinId); - allContracts.addAll(contracts); - } else { - logger.w('[ContractCheckService] Could not get valid twinId for wallet: ${w.name}'); + List contracts = + await getGracePeriodContractsByTwinId(twinId); + allContracts.addAll(contracts); } } return allContracts; diff --git a/app/lib/services/notification_service.dart b/app/lib/services/notification_service.dart index 2157d626..576958ac 100644 --- a/app/lib/services/notification_service.dart +++ b/app/lib/services/notification_service.dart @@ -1,126 +1,174 @@ -import 'dart:convert'; -import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/main.dart'; +import 'package:threebotlogin/helpers/logger.dart'; +import 'package:flutter/material.dart'; +import 'dart:convert'; + import 'package:threebotlogin/widgets/custom_dialog.dart'; -class NotificationService { - static final NotificationService _instance = NotificationService._internal(); - factory NotificationService() => _instance; - NotificationService._internal(); +@pragma('vm:entry-point') +void notificationTapBackground(NotificationResponse notificationResponse) { + if (notificationResponse.payload != null) { + NotificationService._handleNotificationTapStatic(notificationResponse.payload!); + } +} - final notificationsPlugin = FlutterLocalNotificationsPlugin(); - bool _isInitialized = false; +class NotificationService { + static final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); Future initNotification() async { - if (_isInitialized) return; - - final NotificationAppLaunchDetails? launchDetails = - await notificationsPlugin.getNotificationAppLaunchDetails(); - - if (launchDetails?.didNotificationLaunchApp ?? false) { - _handleNotificationTap(launchDetails?.notificationResponse); - } + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/launcher_icon'); - const initSettingsAndroid = - AndroidInitializationSettings('@mipmap/ic_launcher'); - const initSettingsIOS = DarwinInitializationSettings( + const DarwinInitializationSettings initializationSettingsIOS = + DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); - const initSettings = InitializationSettings( - android: initSettingsAndroid, - iOS: initSettingsIOS, + const InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, ); - await notificationsPlugin.initialize( - initSettings, - onDidReceiveNotificationResponse: (details) => - _handleNotificationTap(details), + await _flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: + (NotificationResponse notificationResponse) async { + if (notificationResponse.payload != null) { + NotificationService._handleNotificationTapStatic(notificationResponse.payload!); + } + }, + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); - await notificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestNotificationsPermission(); + } - _isInitialized = true; + static AndroidNotificationDetails _androidNotificationDetails(String groupKey) { + return AndroidNotificationDetails( + 'channel ID', + 'channel name', + channelDescription: 'channel description', + importance: Importance.max, + priority: Priority.high, + groupKey: groupKey, + setAsGroupSummary: false, + ); + } + + static DarwinNotificationDetails _iOSNotificationDetails() { + return const DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); } Future showNotification({ - int id = 0, + required String id, required String title, required String body, - String? groupKey, - bool isGroupSummary = false, + required String groupKey, }) async { - try { - if (!_isInitialized) { - await initNotification(); - } - - final androidDetails = AndroidNotificationDetails( - 'node_status_channel', - 'Node Status', - channelDescription: 'Notify user when node goes offline', - importance: Importance.max, - priority: Priority.high, - groupKey: groupKey, - ); - - final iosDetails = DarwinNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - threadIdentifier: groupKey, - interruptionLevel: InterruptionLevel.timeSensitive); - - final notificationDetails = NotificationDetails( - android: androidDetails, - iOS: iosDetails, - ); - - final payload = json.encode({ + final int notificationId = id.hashCode; + await _flutterLocalNotificationsPlugin.show( + notificationId, + title, + body, + NotificationDetails( + android: _androidNotificationDetails(groupKey), + iOS: _iOSNotificationDetails(), + ), + payload: jsonEncode({ 'title': title, 'body': body, - }); - - await notificationsPlugin.show( - id, - title, - body, - notificationDetails, - payload: payload, - ); - } catch (e) { - logger.e('[NotificationService] Failed to show notification: $e'); + 'groupKey': groupKey, + }), + ); + logger.i('[NotificationService] Notification shown: ID $notificationId, Title: "$title"'); + } + + static void _handleNotificationTapStatic(String payload) async { + logger.i('[NotificationService Static] Notification tapped, payload: $payload'); + + try { + final Map data = jsonDecode(payload); + final String groupKey = data['groupKey'] as String; + final String title = data['title'] as String; + final String body = data['body'] as String; + + logger.i('[NotificationService Static] Processing tapped notification with groupKey: $groupKey'); + + if (navigatorKey.currentContext != null) { + if (groupKey == 'contract_alerts') { + NotificationService.showContractAlertDialog( + title: title, + body: body, + ); + } else if (groupKey == 'offline_nodes') { + NotificationService.showNodeAlertDialog( + title: title, + body: body, + ); + } + } else { + logger.w('[NotificationService Static] navigatorKey.currentContext is null. Cannot show dialog directly from background notification tap. Payload: $payload'); + } + } catch (e, stack) { + logger.e('[NotificationService Static] Error parsing notification payload or handling tap: $e', error: e, stackTrace: stack); } } - void _handleNotificationTap(NotificationResponse? response) { - if (response?.payload != null) { - final Map payload = json.decode(response!.payload!); - showNodeStatusDialog( - navigatorKey.currentContext!, - payload['title'], - payload['body'], - ); + static Future showContractAlertDialog({ + required String title, + required String body, + }) async { + if (navigatorKey.currentContext == null) { + logger.w('[NotificationService Static] Cannot show contract alert dialog, navigatorKey.currentContext is null.'); + return; } + + await NotificationService._showAppDialog( + navigatorKey.currentContext!, + title: title, + content: Text(body), + icon: Icons.assignment_outlined, + ); + logger.i('[NotificationService Static] Dialog shown: $title'); } - void showNodeStatusDialog(BuildContext context, String title, String body) { - try { - if (!context.mounted) return; + static Future showNodeAlertDialog({ + required String title, + required String body, + }) async { + if (navigatorKey.currentContext == null) { + logger.w('[NotificationService Static] Cannot show node alert dialog, navigatorKey.currentContext is null.'); + return; + } - showDialog( + await NotificationService._showAppDialog( + navigatorKey.currentContext!, + title: title, + content: Text(body), + icon: Icons.power_off_outlined, + ); + logger.i('[NotificationService Static] Dialog shown: $title'); + } + + static Future _showAppDialog( + BuildContext context, { + required String title, + required Text content, + required IconData icon, + }) { + return showDialog( context: context, builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, + image: icon, title: title, - description: body, + description: content.data, actions: [ TextButton( child: const Text('Close'), @@ -131,8 +179,5 @@ class NotificationService { ], ), ); - } catch (e) { - logger.e('[NotificationService] Failed to show dialog: $e'); - } } -} +} \ No newline at end of file From 4026d050ec7cab8e271b50ae4d48341a68aa8115 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 21 May 2025 10:34:21 +0300 Subject: [PATCH 07/10] Remove test btn --- app/lib/screens/registered_screen.dart | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/app/lib/screens/registered_screen.dart b/app/lib/screens/registered_screen.dart index 33654617..886cef31 100644 --- a/app/lib/screens/registered_screen.dart +++ b/app/lib/screens/registered_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/widgets/chat_widget.dart'; import 'package:threebotlogin/widgets/home_card.dart'; import 'package:threebotlogin/widgets/home_logo.dart'; -import 'package:threebotlogin/services/notification_service.dart'; // Import NotificationService class RegisteredScreen extends StatefulWidget { static final RegisteredScreen _singleton = RegisteredScreen._internal(); @@ -125,28 +124,6 @@ class _RegisteredScreenState extends State ], ), const SizedBox(height: 20), // Added some spacing - // --- START: Notification Test Button --- - ElevatedButton( - onPressed: () { - // Manually trigger a contract alert notification - NotificationService().showNotification( - id: 'test_contract_1', - title: 'Test Contract Alert! ๐Ÿงช', - body: 'This is a test contract notification.', - groupKey: 'contract_alerts', - ); - // Manually trigger an offline node notification - NotificationService().showNotification( - id: 'test_node_1', - title: 'Test Node Offline! ๐Ÿšจ', - body: 'This is a test node offline notification.', - groupKey: 'offline_nodes', - ); - }, - child: const Text('Show Test Notifications'), - ), - const SizedBox(height: 20), // Added some spacing - // --- END: Notification Test Button --- const Row( children: [Spacer(), CrispChatbot(), SizedBox(width: 20)], ) From 239c8990370e6c87a18c7d21f9bfa62240bab9ba Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 21 May 2025 10:35:19 +0300 Subject: [PATCH 08/10] Revert registered screen change --- app/lib/screens/registered_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/screens/registered_screen.dart b/app/lib/screens/registered_screen.dart index 886cef31..b5cbdcd8 100644 --- a/app/lib/screens/registered_screen.dart +++ b/app/lib/screens/registered_screen.dart @@ -123,7 +123,7 @@ class _RegisteredScreenState extends State pageNumber: 9), ], ), - const SizedBox(height: 20), // Added some spacing + const SizedBox(height: 40), const Row( children: [Spacer(), CrispChatbot(), SizedBox(width: 20)], ) From 996712e7045054ab655fe79a8a44d0a2fe0b6cee Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 21 May 2025 11:01:08 +0300 Subject: [PATCH 09/10] Fix analyze workflow --- app/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/main.dart b/app/lib/main.dart index dfd32461..245ebd50 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -127,7 +127,7 @@ class MyApp extends ConsumerWidget { backgroundColor: kColorScheme.primary, foregroundColor: kColorScheme.onPrimary, ), - cardTheme: const CardTheme().copyWith( + cardTheme: const CardThemeData().copyWith( color: kColorScheme.surfaceContainer, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), elevatedButtonTheme: ElevatedButtonThemeData( @@ -154,7 +154,7 @@ class MyApp extends ConsumerWidget { backgroundColor: kDarkColorScheme.primaryContainer, foregroundColor: kDarkColorScheme.onPrimaryContainer, ), - cardTheme: const CardTheme().copyWith( + cardTheme: const CardThemeData().copyWith( color: kDarkColorScheme.surfaceContainer, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), elevatedButtonTheme: ElevatedButtonThemeData( From a63d95dc8647fde23874abcf2e3b81e3d1206913 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 21 May 2025 13:10:45 +0300 Subject: [PATCH 10/10] Fix automatic dialog dismissal --- app/lib/services/notification_service.dart | 88 ++++++++++++++-------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/app/lib/services/notification_service.dart b/app/lib/services/notification_service.dart index 576958ac..e9d827fe 100644 --- a/app/lib/services/notification_service.dart +++ b/app/lib/services/notification_service.dart @@ -9,13 +9,14 @@ import 'package:threebotlogin/widgets/custom_dialog.dart'; @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { if (notificationResponse.payload != null) { - NotificationService._handleNotificationTapStatic(notificationResponse.payload!); + NotificationService._handleNotificationTapStatic( + notificationResponse.payload!); } } class NotificationService { - static final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); + static final FlutterLocalNotificationsPlugin + _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); Future initNotification() async { const AndroidInitializationSettings initializationSettingsAndroid = @@ -39,14 +40,18 @@ class NotificationService { onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async { if (notificationResponse.payload != null) { - NotificationService._handleNotificationTapStatic(notificationResponse.payload!); + // Ensure the app is brought to foreground + await _flutterLocalNotificationsPlugin.cancelAll(); + NotificationService._handleNotificationTapStatic( + notificationResponse.payload!); } }, onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); } - static AndroidNotificationDetails _androidNotificationDetails(String groupKey) { + static AndroidNotificationDetails _androidNotificationDetails( + String groupKey) { return AndroidNotificationDetails( 'channel ID', 'channel name', @@ -87,11 +92,13 @@ class NotificationService { 'groupKey': groupKey, }), ); - logger.i('[NotificationService] Notification shown: ID $notificationId, Title: "$title"'); + logger.i( + '[NotificationService] Notification shown: ID $notificationId, Title: "$title"'); } static void _handleNotificationTapStatic(String payload) async { - logger.i('[NotificationService Static] Notification tapped, payload: $payload'); + logger.i( + '[NotificationService Static] Notification tapped, payload: $payload'); try { final Map data = jsonDecode(payload); @@ -99,25 +106,39 @@ class NotificationService { final String title = data['title'] as String; final String body = data['body'] as String; - logger.i('[NotificationService Static] Processing tapped notification with groupKey: $groupKey'); + logger.i( + '[NotificationService Static] Processing tapped notification with groupKey: $groupKey'); + + // Wait for app to be in foreground if needed + await Future.delayed(const Duration(milliseconds: 500)); + + if (navigatorKey.currentContext == null) { + logger.w( + '[NotificationService Static] Context is null, retrying in 1 second...'); + await Future.delayed(const Duration(seconds: 1)); + } if (navigatorKey.currentContext != null) { if (groupKey == 'contract_alerts') { - NotificationService.showContractAlertDialog( + await NotificationService.showContractAlertDialog( title: title, body: body, ); } else if (groupKey == 'offline_nodes') { - NotificationService.showNodeAlertDialog( + await NotificationService.showNodeAlertDialog( title: title, body: body, ); } } else { - logger.w('[NotificationService Static] navigatorKey.currentContext is null. Cannot show dialog directly from background notification tap. Payload: $payload'); + logger.w( + '[NotificationService Static] Could not get valid context after retry'); } } catch (e, stack) { - logger.e('[NotificationService Static] Error parsing notification payload or handling tap: $e', error: e, stackTrace: stack); + logger.e( + '[NotificationService Static] Error handling notification tap: $e', + error: e, + stackTrace: stack); } } @@ -126,7 +147,8 @@ class NotificationService { required String body, }) async { if (navigatorKey.currentContext == null) { - logger.w('[NotificationService Static] Cannot show contract alert dialog, navigatorKey.currentContext is null.'); + logger.w( + '[NotificationService Static] Cannot show contract alert dialog, navigatorKey.currentContext is null.'); return; } @@ -144,7 +166,8 @@ class NotificationService { required String body, }) async { if (navigatorKey.currentContext == null) { - logger.w('[NotificationService Static] Cannot show node alert dialog, navigatorKey.currentContext is null.'); + logger.w( + '[NotificationService Static] Cannot show node alert dialog, navigatorKey.currentContext is null.'); return; } @@ -164,20 +187,25 @@ class NotificationService { required IconData icon, }) { return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: icon, - title: title, - description: content.data, - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ), - ); + context: context, + builder: (BuildContext context) { + return PopScope( + canPop: false, + child: CustomDialog( + image: icon, + title: title, + description: content.data, + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ); + }, + ); } -} \ No newline at end of file +}