diff --git a/app/ios/Podfile b/app/ios/Podfile index 68788916c..a32df4044 100644 --- a/app/ios/Podfile +++ b/app/ios/Podfile @@ -38,6 +38,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end + installer.pods_project.targets.each do |target| if target.name == "lottie-ios" target.build_configurations.each do |config| diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 153fcc8ea..6cccdf257 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -390,7 +390,9 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APPLICATION_EXTENSION_API_ONLY = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 191; @@ -535,7 +537,9 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APPLICATION_EXTENSION_API_ONLY = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 191; @@ -570,7 +574,9 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APPLICATION_EXTENSION_API_ONLY = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 191; diff --git a/app/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift index 033ed23b0..d42d7f850 100644 --- a/app/ios/Runner/AppDelegate.swift +++ b/app/ios/Runner/AppDelegate.swift @@ -1,6 +1,5 @@ import UIKit import Flutter -import flutter_local_notifications @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -12,9 +11,6 @@ import flutter_local_notifications _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in - GeneratedPluginRegistrant.register(with: registry) - } if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate diff --git a/app/lib/screens/app_lifecycle_observer.dart b/app/lib/screens/app_lifecycle_observer.dart index efd05e08f..8af857bd8 100644 --- a/app/lib/screens/app_lifecycle_observer.dart +++ b/app/lib/screens/app_lifecycle_observer.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/main.dart'; import 'package:threebotlogin/screens/home_screen.dart'; +import 'package:threebotlogin/services/notification_service.dart'; class AppLifecycleObserver extends ConsumerStatefulWidget { final Widget child; @@ -47,6 +48,11 @@ class _AppLifecycleObserverState extends ConsumerState ref.read(lastPausedProvider.notifier).state = now; logger.i('App Paused. Last paused time updated: $now'); } else if (state == AppLifecycleState.resumed) { + logger.i('App Resumed. Notifying notification service.'); + WidgetsBinding.instance.addPostFrameCallback((_) { + NotificationService().onAppResumed(); + }); + if (now - lastPaused >= pinCheckTimeout) { logger.i('Timeout expired. Requiring authentication.'); // Navigate to Home Screen diff --git a/app/lib/screens/farm_screen.dart b/app/lib/screens/farm_screen.dart index 3c84a552a..068ec9609 100644 --- a/app/lib/screens/farm_screen.dart +++ b/app/lib/screens/farm_screen.dart @@ -72,7 +72,8 @@ class _FarmScreenState extends ConsumerState _handleSuccess(); } on TimeoutException catch (e) { - _handleFailure('Loading farms timed out. Please check your network.', error: 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); @@ -177,26 +178,34 @@ class _FarmScreenState extends ConsumerState await registrarClient!.accounts.getByPublicKey(publicKey); final farms = await registrarClient!.farms .list(registrarFarm.FarmFilter(twinID: account.twinID)); - final nodes = await registrarClient!.nodes - .list(registrarNode.NodeFilter(twinID: account.twinID)); - v4Farms.addAll(farms.map((f) => Farm( - name: f.farmName, - walletAddress: f.stellarAddress!, - tfchainWalletSecret: w.tfchainSecret, - walletName: w.name, - twinId: f.twinID, - farmId: f.farmID!, - nodes: nodes - .where((n) => n.farmID == f.farmID) - .map((n) => Node( - nodeId: n.nodeID, - status: NodeStatus.Up, - country: n.location.country, - uptime: - (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0)) - .toList(), - ))); + for (var f in farms) { + final farmNodes = await registrarClient!.nodes + .list(registrarNode.NodeFilter(farmID: f.farmID)); + final nodes = farmNodes.map((n) { + // Convert ISO timestamp to Unix timestamp + final lastSeenDate = DateTime.parse(n.lastSeen); + final unixTimestamp = lastSeenDate.millisecondsSinceEpoch ~/ 1000; + + return Node( + nodeId: n.nodeID, + status: n.online ? NodeStatus.Up : NodeStatus.Down, + country: n.location.country, + uptime: (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0, + updatedAt: unixTimestamp, + ); + }).toList(); + + v4Farms.addAll(farms.map((f) => Farm( + name: f.farmName, + walletAddress: f.stellarAddress!, + tfchainWalletSecret: w.tfchainSecret, + walletName: w.name, + twinId: f.twinID, + farmId: f.farmID!, + nodes: nodes, + ))); + } } catch (e) { continue; } diff --git a/app/lib/services/background_service.dart b/app/lib/services/background_service.dart index 2d6261b67..3471a36a8 100644 --- a/app/lib/services/background_service.dart +++ b/app/lib/services/background_service.dart @@ -16,7 +16,7 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { final bool notificationsEnabled = await isNodeStatusNotificationEnabled(); logger.i( - 'Background Fetch Headless Task: $taskId, Notifications Enabled: $notificationsEnabled'); + '[BackgroundFetch] Headless Task: $taskId, Notifications Enabled: $notificationsEnabled'); if (!notificationsEnabled) { logger.i( @@ -29,9 +29,18 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { Future checkNodeStatus(String taskId) async { try { - final offlineNodes = await NodeCheckService.pingNodesInBackground(); + final v3OfflineNodes = await NodeCheckService.pingV3NodesInBackground(); + final v4OfflineNodes = await NodeCheckService.pingV4NodesInBackground(); + final offlineNodes = [...v3OfflineNodes, ...v4OfflineNodes]; - if (offlineNodes.isEmpty) return; + logger.i( + '[BackgroundFetch] Total offline nodes found: ${offlineNodes.length} for task $taskId'); + + if (offlineNodes.isEmpty) { + logger.i( + '[BackgroundFetch] No offline nodes found, finishing task $taskId'); + return; + } final StringBuffer bodyBuffer = StringBuffer(); final List nodesToNotify = []; @@ -43,7 +52,11 @@ Future checkNodeStatus(String taskId) async { for (final node in offlineNodes) { final nodeUpdatedAtMs = node.updatedAt! * 1000; - if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) continue; + if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) { + logger.i( + '[BackgroundFetch] Skipping node ${node.nodeId} - offline for more than 7 days'); + continue; + } final downtime = Duration(milliseconds: nowInMs - nodeUpdatedAtMs); @@ -62,7 +75,11 @@ Future checkNodeStatus(String taskId) async { } } - if (nodesToNotify.isEmpty) return; + if (nodesToNotify.isEmpty) { + logger.i( + '[BackgroundFetch] No nodes to notify after interval check, finishing task $taskId'); + return; + } await NotificationService().showNotification( id: nodesToNotify.hashCode, @@ -73,8 +90,9 @@ Future checkNodeStatus(String taskId) async { groupKey: 'offline_nodes', ); } catch (e) { - logger.e('Error in checkNodeStatus for task $taskId: $e'); + logger.e('[BackgroundFetch] Error in checkNodeStatus for task $taskId: $e'); } finally { + logger.i('[BackgroundFetch] Finishing task $taskId'); BackgroundFetch.finish(taskId); } } diff --git a/app/lib/services/nodes_check_service.dart b/app/lib/services/nodes_check_service.dart index ca618d92a..ea7b2f488 100644 --- a/app/lib/services/nodes_check_service.dart +++ b/app/lib/services/nodes_check_service.dart @@ -1,13 +1,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/models/wallet.dart'; import 'package:threebotlogin/providers/wallets_provider.dart'; +import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/gridproxy_service.dart'; import 'package:threebotlogin/services/tfchain_service.dart'; +import 'package:registrar_client/models/farm.dart' as registrarFarm; +import 'package:registrar_client/models/node.dart' as registrarNode; +import 'package:registrar_client/registrar_client.dart' as registrar; class NodeCheckService { - static Future> pingNodesInBackground() async { + static Future> pingV3NodesInBackground() async { final container = ProviderContainer(); try { final walletsNotifierInstance = container.read(walletsNotifier.notifier); @@ -41,11 +46,10 @@ class NodeCheckService { .toList(); allNodes.addAll(nodes); } - final offlineNodes = + final offlineNodes = allNodes.where((n) => n.status != NodeStatus.Up).toList(); return offlineNodes; - } catch (e) { logger.e('[NodeCheckService] Error: $e'); return []; @@ -53,4 +57,70 @@ class NodeCheckService { container.dispose(); } } + + static Future> pingV4NodesInBackground() async { + final container = ProviderContainer(); + final List allOfflineNodes = []; + + try { + final walletsNotifierInstance = container.read(walletsNotifier.notifier); + + await walletsNotifierInstance.waitUntilListed(); + + final List wallets = container.read(walletsNotifier); + + if (wallets.isEmpty) { + logger.i('[NodeCheckService] No wallets found'); + return []; + } + + registrar.RegistrarClient? registrarClient = registrar.RegistrarClient( + baseUrl: Globals().registrarURL, + mnemonicOrSeed: wallets.first.tfchainSecret); + for (var w in wallets) { + try { + final publicKey = await derivePublicKey(w.tfchainSecret); + final account = + await registrarClient.accounts.getByPublicKey(publicKey); + final farms = await registrarClient.farms + .list(registrarFarm.FarmFilter(twinID: account.twinID)); + + for (var f in farms) { + final farmNodes = await registrarClient.nodes + .list(registrarNode.NodeFilter(farmID: f.farmID)); + + final nodes = farmNodes.map((n) { + // Convert ISO timestamp to Unix timestamp + final lastSeenDate = DateTime.parse(n.lastSeen); + final unixTimestamp = lastSeenDate.millisecondsSinceEpoch ~/ 1000; + + return Node( + nodeId: n.nodeID, + status: n.online ? NodeStatus.Up : NodeStatus.Down, + country: n.location.country, + uptime: (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0, + updatedAt: unixTimestamp, + ); + }).toList(); + + final offlineNodes = + nodes.where((n) => n.status != NodeStatus.Up).toList(); + allOfflineNodes.addAll(offlineNodes); + } + } catch (e) { + logger.e('[NodeCheckService] Error: $e'); + continue; + } + } + + logger.i( + '[NodeCheckService] Found ${allOfflineNodes.length} offline nodes'); + return allOfflineNodes; + } catch (e) { + logger.e('[NodeCheckService] Error in pingV4NodesInBackground: $e'); + return []; + } finally { + container.dispose(); + } + } } diff --git a/app/lib/services/notification_service.dart b/app/lib/services/notification_service.dart index 2157d6262..59200bb4a 100644 --- a/app/lib/services/notification_service.dart +++ b/app/lib/services/notification_service.dart @@ -1,50 +1,69 @@ import 'dart:convert'; +import 'dart:async'; +import 'package:awesome_notifications/awesome_notifications.dart'; 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/widgets/custom_dialog.dart'; +@pragma('vm:entry-point') +Future onActionReceivedMethod(ReceivedAction receivedAction) async { + logger.i('[NotificationService] Action received: ${receivedAction.title}'); + + if (receivedAction.id != null) { + await AwesomeNotifications().dismiss(receivedAction.id!); + } + + final payload = receivedAction.payload?['data']; + if (payload != null) { + final Map data = json.decode(payload); + logger.i('[NotificationService] Processing notification payload: $data'); + NotificationService()._handleNotificationTap(data); + } +} + class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; NotificationService._internal(); - final notificationsPlugin = FlutterLocalNotificationsPlugin(); bool _isInitialized = false; + Map? _pendingPayload; + bool _isAppResumed = false; + int _notificationCount = 0; Future initNotification() async { if (_isInitialized) return; - final NotificationAppLaunchDetails? launchDetails = - await notificationsPlugin.getNotificationAppLaunchDetails(); - - if (launchDetails?.didNotificationLaunchApp ?? false) { - _handleNotificationTap(launchDetails?.notificationResponse); - } - - const initSettingsAndroid = - AndroidInitializationSettings('@mipmap/ic_launcher'); - const initSettingsIOS = DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, - requestSoundPermission: true, + await AwesomeNotifications().initialize( + null, + [ + NotificationChannel( + channelKey: 'node_status_channel', + channelName: 'Node Status', + channelDescription: 'Notify user when node goes offline', + importance: NotificationImportance.High, + channelShowBadge: true, + enableVibration: true, + enableLights: true, + criticalAlerts: true, + ), + ], + debug: true, ); - const initSettings = InitializationSettings( - android: initSettingsAndroid, - iOS: initSettingsIOS, + AwesomeNotifications().setListeners( + onActionReceivedMethod: onActionReceivedMethod, + onNotificationCreatedMethod: _onNotificationCreated, + onNotificationDisplayedMethod: _onNotificationDisplayed, + onDismissActionReceivedMethod: _onDismissActionReceived, ); - await notificationsPlugin.initialize( - initSettings, - onDidReceiveNotificationResponse: (details) => - _handleNotificationTap(details), - ); - await notificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestNotificationsPermission(); + await AwesomeNotifications().isNotificationAllowed().then((isAllowed) { + if (!isAllowed) { + AwesomeNotifications().requestPermissionToSendNotifications(); + } + }); _isInitialized = true; } @@ -61,78 +80,159 @@ class NotificationService { 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({ 'title': title, 'body': body, }); - await notificationsPlugin.show( - id, - title, - body, - notificationDetails, - payload: payload, + _notificationCount++; + await _updateBadgeCount(); + + await AwesomeNotifications().createNotification( + content: NotificationContent( + id: id, + channelKey: 'node_status_channel', + title: title, + body: body, + payload: {'data': payload}, + notificationLayout: NotificationLayout.Default, + category: NotificationCategory.Message, + wakeUpScreen: true, + fullScreenIntent: true, + criticalAlert: true, + autoDismissible: true, + displayOnForeground: true, + displayOnBackground: true, + actionType: ActionType.Default, + badge: _notificationCount, + ), + actionButtons: [ + NotificationActionButton( + key: 'SHOW_DIALOG', + label: 'Show Details', + actionType: ActionType.Default, + autoDismissible: true, + ), + ], ); } catch (e) { logger.e('[NotificationService] Failed to show notification: $e'); } } - void _handleNotificationTap(NotificationResponse? response) { - if (response?.payload != null) { - final Map payload = json.decode(response!.payload!); - showNodeStatusDialog( - navigatorKey.currentContext!, - payload['title'], - payload['body'], - ); + Future _updateBadgeCount() async { + try { + await AwesomeNotifications().setGlobalBadgeCounter(_notificationCount); + logger.i( + '[NotificationService] Updated badge count to: $_notificationCount'); + } catch (e) { + logger.e('[NotificationService] Failed to update badge count: $e'); } } - void showNodeStatusDialog(BuildContext context, String title, String body) { - try { - if (!context.mounted) return; + void _handleNotificationTap(Map data) { + logger + .i('[NotificationService] Handling notification tap with data: $data'); + _pendingPayload = data; + _isAppResumed = false; - showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: title, - description: body, - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ), - ); - } catch (e) { - logger.e('[NotificationService] Failed to show dialog: $e'); + if (_isAppResumed && navigatorKey.currentContext != null) { + _showDialog(); + } + } + + void onAppResumed() { + logger.i('[NotificationService] App resumed'); + _isAppResumed = true; + + if (_pendingPayload != null) { + _showDialog(); } } + + void _showDialog() { + if (_pendingPayload == null || navigatorKey.currentContext == null) { + logger.w( + '[NotificationService] Cannot show dialog: missing payload or context'); + return; + } + + logger.i( + '[NotificationService] Showing dialog with title: ${_pendingPayload!['title']}'); + + WidgetsBinding.instance.ensureVisualUpdate(); + + Future.delayed(const Duration(milliseconds: 100), () { + if (!navigatorKey.currentContext!.mounted) { + logger.w('[NotificationService] Context not mounted after delay'); + return; + } + + try { + logger.i('[NotificationService] Attempting to show dialog...'); + showDialog( + context: navigatorKey.currentContext!, + barrierDismissible: false, + routeSettings: const RouteSettings(name: 'node_status_dialog'), + builder: (BuildContext context) { + logger.i('[NotificationService] Building dialog widget'); + return WillPopScope( + onWillPop: () async => false, + child: CustomDialog( + type: DialogType.Warning, + image: Icons.warning, + title: _pendingPayload!['title'], + description: _pendingPayload!['body'], + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + logger.i( + '[NotificationService] Dialog close button pressed'); + Navigator.of(context).pop(); + _decrementNotificationCount(); + }, + ), + ], + ), + ); + }, + ).then((_) { + logger.i('[NotificationService] Dialog shown successfully'); + _pendingPayload = null; + }).catchError((error) { + logger.e('[NotificationService] Error showing dialog: $error'); + _pendingPayload = null; + }); + } catch (e) { + logger.e('[NotificationService] Failed to show dialog: $e'); + _pendingPayload = null; + } + }); + } + + Future _decrementNotificationCount() async { + if (_notificationCount > 0) { + _notificationCount--; + await _updateBadgeCount(); + } + } + + @pragma('vm:entry-point') + Future _onNotificationCreated( + ReceivedNotification receivedNotification) async { + logger.i('Notification created: ${receivedNotification.title}'); + } + + @pragma('vm:entry-point') + Future _onNotificationDisplayed( + ReceivedNotification receivedNotification) async { + logger.i('Notification displayed: ${receivedNotification.title}'); + } + + @pragma('vm:entry-point') + Future _onDismissActionReceived(ReceivedAction receivedAction) async { + logger.i('Notification dismissed: ${receivedAction.title}'); + _decrementNotificationCount(); + } } diff --git a/app/pubspec.lock b/app/pubspec.lock index fc185e87b..18212cc15 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -70,6 +70,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.25" + awesome_notifications: + dependency: "direct main" + description: + name: awesome_notifications + sha256: "0d5fa4457f2ba4e536adc3ef6af709cdcecf4a05a1f3035981e9afa2f899b2a8" + url: "https://pub.dev" + source: hosted + version: "0.10.1" background_fetch: dependency: "direct main" description: @@ -643,38 +651,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "33b3e0269ae9d51669957a923f2376bee96299b09915d856395af8c4238aebfa" - url: "https://pub.dev" - source: hosted - version: "19.1.0" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c" - url: "https://pub.dev" - source: hosted - version: "9.0.0" - flutter_local_notifications_windows: - dependency: transitive - description: - name: flutter_local_notifications_windows - sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e - url: "https://pub.dev" - source: hosted - version: "1.0.0" flutter_pkid: dependency: "direct main" description: @@ -920,7 +896,7 @@ packages: source: hosted version: "4.1.0" intl: - dependency: "direct main" + dependency: "direct overridden" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf @@ -1472,7 +1448,7 @@ packages: source: hosted version: "4.1.0" reflectable: - dependency: "direct main" + dependency: transitive description: name: reflectable sha256: "35ee17c3b759fa935cc7e9247445903384520fd174e0d6c142d8288e5439fd5b" @@ -1484,7 +1460,7 @@ packages: description: path: "packages/registrar_client" ref: development - resolved-ref: "075a179b1fcdd997bc4aa4b7c900036f44d14f7f" + resolved-ref: "3a3edb642c1f2976a3d1649fad8694e5c26b8229" url: "https://github.com/threefoldtech/tfgridv4-sdk-dart" source: git version: "0.1.0" @@ -1904,14 +1880,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.0" - timezone: - dependency: transitive - description: - name: timezone - sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d - url: "https://pub.dev" - source: hosted - version: "0.10.0" timing: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f1e83c19c..bff522333 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -85,9 +85,9 @@ dependencies: intl_mobile_field: ^1.1.1 mobile_scanner: 5.2.3 flag: ^7.0.0 - flutter_local_notifications: ^19.0.0 background_fetch: ^1.3.8 connectivity_plus: ^6.1.4 + awesome_notifications: ^0.10.1 dev_dependencies: flutter_test: sdk: flutter