From 14eefa106280944175c5b0858b0d4544d9c8c990 Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Wed, 22 Jul 2026 23:18:32 +0800 Subject: [PATCH] Implement Background Sync Polling with Workmanager --- .../services/background_sync_service.dart | 63 +++++++++++++++++++ frontend/lib/main.dart | 18 ++++++ .../ui/providers/notification_provider.dart | 7 +++ frontend/pubspec.lock | 32 ++++++++++ frontend/pubspec.yaml | 1 + 5 files changed, 121 insertions(+) create mode 100644 frontend/lib/core/services/background_sync_service.dart diff --git a/frontend/lib/core/services/background_sync_service.dart b/frontend/lib/core/services/background_sync_service.dart new file mode 100644 index 0000000..9018cf3 --- /dev/null +++ b/frontend/lib/core/services/background_sync_service.dart @@ -0,0 +1,63 @@ +import 'dart:math' as math; +import 'package:dio/dio.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:workmanager/workmanager.dart'; +import 'notification_service.dart'; + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + try { + // 1. Retrieve the saved base URL and Auth Token + final prefs = await SharedPreferences.getInstance(); + final baseUrl = prefs.getString('server_url'); + if (baseUrl == null) return true; // No server known, wait till next run + + const storage = FlutterSecureStorage(); + final token = await storage.read(key: 'jwt_token'); + if (token == null) return true; // Not logged in + + // 2. Fetch recent notifications from server + final dio = Dio(BaseOptions( + baseUrl: baseUrl, + connectTimeout: const Duration(seconds: 5), + receiveTimeout: const Duration(seconds: 5), + headers: {'Authorization': 'Bearer $token'}, + )); + + final response = await dio.get('/notifications/recent'); + final list = (response.data as List).cast>(); + + if (list.isNotEmpty) { + // 3. Find the last seen notification ID + final highestOldId = prefs.getInt('last_seen_notification_id') ?? 0; + + // Find new ones + final newNotes = list.where((e) => (e['id'] as int) > highestOldId).toList(); + + if (newNotes.isNotEmpty) { + final notificationService = NotificationService(); + await notificationService.initialize(); // Ensure it's ready in this isolate + + // 4. Show local notification for each new one + for (var note in newNotes) { + await notificationService.showNotification( + title: note['title'] ?? 'New Notification', + body: note['message'] ?? '', + ); + } + + // 5. Update highest seen ID in prefs + final newHighestId = list.map((e) => e['id'] as int).reduce(math.max); + await prefs.setInt('last_seen_notification_id', math.max(highestOldId, newHighestId)); + } + } + + return Future.value(true); + } catch (e) { + print('Background fetch failed: $e'); + return Future.value(false); // Retries on false + } + }); +} diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index add4cda..211c782 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -12,7 +12,9 @@ import 'ui/screens/splash/splash_screen.dart'; import 'core/theme/app_theme.dart'; // Add this import import 'core/constants/app_colors.dart'; import 'core/services/notification_service.dart'; +import 'core/services/background_sync_service.dart'; import 'ui/shared/widgets/inactivity_wrapper.dart'; +import 'package:workmanager/workmanager.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -47,6 +49,22 @@ void main() async { await NotificationService().initialize(); + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: false, + ); + // Register periodic task + Workmanager().registerPeriodicTask( + "1", + "background_sync_task", + frequency: const Duration(minutes: 15), + constraints: Constraints( + networkType: NetworkType.connected, + ), + ); + } + runApp(const ProviderScope(child: TisRmsApp())); } diff --git a/frontend/lib/ui/providers/notification_provider.dart b/frontend/lib/ui/providers/notification_provider.dart index ca9822e..0f372d2 100644 --- a/frontend/lib/ui/providers/notification_provider.dart +++ b/frontend/lib/ui/providers/notification_provider.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/repositories/notification_repository.dart'; import '../../domain/entities/notification_model.dart'; import '../../core/services/notification_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'dart:math' as math; final notificationRepositoryProvider = Provider((ref) { @@ -52,6 +53,12 @@ class NotificationNotifier extends AsyncNotifier> { } } + if (list.isNotEmpty) { + final highestId = list.map((e) => e.id).reduce(math.max); + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('last_seen_notification_id', highestId); + } + state = AsyncData(list); } catch (e, st) { state = AsyncError(e, st); diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 1d237af..7ec5d19 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -1549,6 +1549,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.0" + workmanager: + dependency: "direct main" + description: + name: workmanager + sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10" + url: "https://pub.dev" + source: hosted + version: "0.9.0+3" + workmanager_android: + dependency: transitive + description: + name: workmanager_android + sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7" + url: "https://pub.dev" + source: hosted + version: "0.9.0+2" + workmanager_apple: + dependency: transitive + description: + name: workmanager_apple + sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca" + url: "https://pub.dev" + source: hosted + version: "0.9.1+2" + workmanager_platform_interface: + dependency: transitive + description: + name: workmanager_platform_interface + sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47 + url: "https://pub.dev" + source: hosted + version: "0.9.1+1" xdg_directories: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 2699ae8..9874cb4 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: pdf: ^3.12.0 intl: ^0.20.3 flutter_local_notifications: ^17.2.2 + workmanager: ^0.9.0+3 dev_dependencies: flutter_test: