Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
6 changes: 6 additions & 0 deletions app/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 0 additions & 4 deletions app/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import UIKit
import Flutter
import flutter_local_notifications

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions app/lib/screens/app_lifecycle_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,6 +48,11 @@ class _AppLifecycleObserverState extends ConsumerState<AppLifecycleObserver>
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
Expand Down
49 changes: 29 additions & 20 deletions app/lib/screens/farm_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class _FarmScreenState extends ConsumerState<FarmScreen>

_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);
Expand Down Expand Up @@ -177,26 +178,34 @@ class _FarmScreenState extends ConsumerState<FarmScreen>
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;
}
Expand Down
30 changes: 24 additions & 6 deletions app/lib/services/background_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -29,9 +29,18 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async {

Future<void> 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<Node> nodesToNotify = [];
Expand All @@ -43,7 +52,11 @@ Future<void> 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);

Expand All @@ -62,7 +75,11 @@ Future<void> 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,
Expand All @@ -73,8 +90,9 @@ Future<void> 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);
}
}
Expand Down
76 changes: 73 additions & 3 deletions app/lib/services/nodes_check_service.dart
Original file line number Diff line number Diff line change
@@ -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<List<Node>> pingNodesInBackground() async {
static Future<List<Node>> pingV3NodesInBackground() async {
final container = ProviderContainer();
try {
final walletsNotifierInstance = container.read(walletsNotifier.notifier);
Expand Down Expand Up @@ -41,16 +46,81 @@ 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 [];
} finally {
container.dispose();
}
}

static Future<List<Node>> pingV4NodesInBackground() async {
final container = ProviderContainer();
final List<Node> allOfflineNodes = [];

try {
final walletsNotifierInstance = container.read(walletsNotifier.notifier);

await walletsNotifierInstance.waitUntilListed();

final List<Wallet> 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();
}
}
}
Loading