diff --git a/android/app/build.gradle b/android/app/build.gradle index ac58f70..134dc2b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,7 +39,7 @@ android { // Specify your unique Application ID. This identifies your app on Google Play. applicationId = "com.marco.aiotstage" // Set minimum and target SDK versions based on Flutter's configuration - minSdk = flutter.minSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion // Set version code and name based on Flutter's configuration (from pubspec.yaml) versionCode = flutter.versionCode @@ -81,5 +81,6 @@ flutter { // ✅ Add required dependencies for desugaring dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' } + diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7bb2df6..3c85cfe 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index acf9d07..2953f1c 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.1" apply false + id "com.android.application" version "8.6.0" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false id("com.google.gms.google-services") version "4.4.2" apply false } diff --git a/lib/controller/auth/login_controller.dart b/lib/controller/auth/login_controller.dart index a004081..0d46c50 100644 --- a/lib/controller/auth/login_controller.dart +++ b/lib/controller/auth/login_controller.dart @@ -7,7 +7,6 @@ import 'package:marco/helpers/widgets/my_validators.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/services/app_logger.dart'; -import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; class LoginController extends MyController { final MyFormValidator basicValidator = MyFormValidator(); @@ -56,17 +55,14 @@ class LoginController extends MyController { try { final loginData = basicValidator.getData(); - - // ✅ Get FCM token - final fcmToken = await FirebaseNotificationService().getFcmToken(); - loginData['fcmToken'] = fcmToken ?? ''; - - logSafe("Attempting login for user: ${loginData['username']} with FCM token: ${loginData['fcmToken']}"); + logSafe("Attempting login for user: ${loginData['username']}"); final errors = await AuthService.loginUser(loginData); if (errors != null) { - logSafe("Login failed for user: ${loginData['username']} with errors: $errors", level: LogLevel.warning); + logSafe( + "Login failed for user: ${loginData['username']} with errors: $errors", + level: LogLevel.warning); showAppSnackbar( title: "Login Failed", @@ -79,11 +75,24 @@ class LoginController extends MyController { basicValidator.clearErrors(); } else { await _handleRememberMe(); + + // ✅ After login success → register saved FCM token with server + final fcmToken = await LocalStorage.getFcmToken(); + if (fcmToken?.isNotEmpty ?? false) { + final success = await AuthService.registerDeviceToken(fcmToken!); + logSafe( + success + ? "✅ FCM token registered after login." + : "⚠️ Failed to register FCM token after login.", + level: LogLevel.warning); + } + logSafe("Login successful for user: ${loginData['username']}"); Get.toNamed('/home'); } } catch (e, stacktrace) { - logSafe("Exception during login", level: LogLevel.error, error: e, stackTrace: stacktrace); + logSafe("Exception during login", + level: LogLevel.error, error: e, stackTrace: stacktrace); showAppSnackbar( title: "Login Error", message: "An unexpected error occurred", @@ -96,8 +105,10 @@ class LoginController extends MyController { Future _handleRememberMe() async { if (isChecked.value) { - await LocalStorage.setToken('username', basicValidator.getController('username')!.text); - await LocalStorage.setToken('password', basicValidator.getController('password')!.text); + await LocalStorage.setToken( + 'username', basicValidator.getController('username')!.text); + await LocalStorage.setToken( + 'password', basicValidator.getController('password')!.text); await LocalStorage.setBool('remember_me', true); } else { await LocalStorage.removeToken('username'); diff --git a/lib/controller/expense/add_expense_controller.dart b/lib/controller/expense/add_expense_controller.dart index cd16b1d..ba2dcc2 100644 --- a/lib/controller/expense/add_expense_controller.dart +++ b/lib/controller/expense/add_expense_controller.dart @@ -189,7 +189,7 @@ class AddExpenseController extends GetxController { ); if (pickedDate != null) { - final now = DateTime.now(); // get current time + final now = DateTime.now(); final finalDateTime = DateTime( pickedDate.year, pickedDate.month, @@ -201,7 +201,7 @@ class AddExpenseController extends GetxController { selectedTransactionDate.value = finalDateTime; transactionDateController.text = - DateFormat('dd-MM-yyyy HH:mm').format(finalDateTime); + DateFormat('dd MMM yyyy').format(finalDateTime); } } diff --git a/lib/helpers/services/firebase/firebase_messaging_service.dart b/lib/helpers/services/firebase/firebase_messaging_service.dart index 6d876c4..697a4ea 100644 --- a/lib/helpers/services/firebase/firebase_messaging_service.dart +++ b/lib/helpers/services/firebase/firebase_messaging_service.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:googleapis_auth/auth_io.dart'; import 'package:logger/logger.dart'; @@ -9,7 +8,7 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:marco/helpers/services/local_notification_service.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/auth_service.dart'; -import 'package:marco/helpers/services/notification_action_handler.dart'; // NEW +import 'package:marco/helpers/services/notification_action_handler.dart'; /// Firebase Notification Service class FirebaseNotificationService { @@ -20,17 +19,16 @@ class FirebaseNotificationService { 'https://www.googleapis.com/auth/firebase.messaging', ]; - /// Initialize Firebase and FCM + /// Initialize FCM (Firebase.initializeApp() should be called once globally) Future initialize() async { - await Firebase.initializeApp(); - _logger.i('✅ Firebase initialized'); + _logger.i('✅ FirebaseMessaging initializing...'); await _requestNotificationPermission(); _registerMessageListeners(); _registerTokenRefreshListener(); - // Fetch token on app start - await getFcmToken(); + // Fetch token on app start (but only register with server if JWT available) + await getFcmToken(registerOnServer: true); } /// Request notification permission @@ -45,9 +43,10 @@ class FirebaseNotificationService { _logger.i('📩 Foreground Notification'); _logNotificationDetails(message); - // Handle UI updates + // Handle custom actions NotificationActionHandler.handle(message.data); + // Show local notification if (message.notification != null) { LocalNotificationService.showNotification( title: message.notification!.title ?? "No title", @@ -76,24 +75,31 @@ class FirebaseNotificationService { ? '✅ Device token updated on server after refresh.' : '⚠️ Failed to update device token on server.'); } else { - _logger.w( - '⚠️ JWT not available — will register token after login or refresh.'); + _logger.w('⚠️ JWT not available — will retry after login.'); } }); } - /// Get current token - Future getFcmToken() async { + /// Get current token (optionally sync to server if logged in) + Future getFcmToken({bool registerOnServer = false}) async { try { final token = await _firebaseMessaging.getToken(); _logger.i('🔑 FCM token: $token'); if (token?.isNotEmpty ?? false) { await LocalStorage.setFcmToken(token!); - final success = await AuthService.registerDeviceToken(token); - _logger.i(success - ? '✅ Device token registered on server.' - : '⚠️ Failed to register device token on server.'); + + if (registerOnServer) { + final jwt = await LocalStorage.getJwtToken(); + if (jwt?.isNotEmpty ?? false) { + final success = await AuthService.registerDeviceToken(token); + _logger.i(success + ? '✅ Device token registered on server.' + : '⚠️ Failed to register device token on server.'); + } else { + _logger.w('⚠️ JWT not available — skipping server registration.'); + } + } } return token; } catch (e, s) { @@ -102,6 +108,17 @@ class FirebaseNotificationService { } } + /// Re-register token with server (useful after login) + Future registerTokenAfterLogin() async { + final token = await LocalStorage.getFcmToken(); + if (token?.isNotEmpty ?? false) { + final success = await AuthService.registerDeviceToken(token!); + _logger.i(success + ? "✅ FCM token registered after login." + : "⚠️ Failed to register FCM token after login."); + } + } + /// Send a test notification using FCM v1 API Future sendTestNotification(String deviceToken) async { try { @@ -192,14 +209,12 @@ class FirebaseNotificationService { } } -/// Background handler +/// Background handler (required by Firebase) Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { - await Firebase.initializeApp(); final logger = Logger(); logger ..i('⚡ Handling background notification...') ..i('📦 Data: ${message.data}'); - // Pass to helper NotificationActionHandler.handle(message.data); } diff --git a/lib/helpers/widgets/expense_main_components.dart b/lib/helpers/widgets/expense_main_components.dart index 31c3ac9..5f19f8a 100644 --- a/lib/helpers/widgets/expense_main_components.dart +++ b/lib/helpers/widgets/expense_main_components.dart @@ -284,7 +284,7 @@ class ExpenseList extends StatelessWidget { final expense = expenseList[index]; final formattedDate = DateTimeUtils.convertUtcToLocal( expense.transactionDate.toIso8601String(), - format: 'dd MMM yyyy, hh:mm a', + format: 'dd MMM yyyy', ); return Material( diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index 4a7e89e..1d520f4 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -451,7 +451,7 @@ class _InvoiceHeader extends StatelessWidget { Widget build(BuildContext context) { final dateString = DateTimeUtils.convertUtcToLocal( expense.transactionDate.toString(), - format: 'dd-MM-yyyy'); + format: 'dd MMM yyyy'); final statusColor = getExpenseStatusColor(expense.status.name, colorCode: expense.status.color); return Column( @@ -514,10 +514,10 @@ class _InvoiceDetailsTable extends StatelessWidget { Widget build(BuildContext context) { final transactionDate = DateTimeUtils.convertUtcToLocal( expense.transactionDate.toString(), - format: 'dd-MM-yyyy hh:mm a'); + format: 'dd MMM yyyy'); final createdAt = DateTimeUtils.convertUtcToLocal( expense.createdAt.toString(), - format: 'dd-MM-yyyy hh:mm a'); + format: 'dd MMM yyyy'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/pubspec.yaml b/pubspec.yaml index 0e60924..c3d4ddc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,17 +50,17 @@ dependencies: loading_animation_widget: ^1.3.0 flutter_quill: ^10.8.5 intl: ^0.19.0 - syncfusion_flutter_core: ^28.1.33 - syncfusion_flutter_sliders: ^28.1.33 - file_picker: ^9.2.3 + syncfusion_flutter_core: ^29.1.40 + syncfusion_flutter_sliders: ^29.1.40 + file_picker: ^10.3.2 timelines_plus: ^1.0.4 - syncfusion_flutter_charts: ^28.1.33 + syncfusion_flutter_charts: ^29.1.40 appflowy_board: ^0.1.2 - syncfusion_flutter_calendar: ^28.2.6 - syncfusion_flutter_maps: ^28.1.33 + syncfusion_flutter_calendar: ^29.1.40 + syncfusion_flutter_maps: ^29.1.40 http: ^1.2.2 - geolocator: ^9.0.1 - permission_handler: ^11.3.0 + geolocator: ^14.0.2 + permission_handler: ^12.0.1 image: ^4.0.17 image_picker: ^1.0.7 logger: ^2.0.2 @@ -79,10 +79,10 @@ dependencies: quill_delta: ^3.0.0-nullsafety.2 connectivity_plus: ^6.1.4 geocoding: ^4.0.0 - firebase_core: ^3.14.0 - firebase_messaging: ^15.2.7 + firebase_core: ^4.0.0 + firebase_messaging: ^16.0.0 googleapis_auth: ^2.0.0 - device_info_plus: ^10.1.0 + device_info_plus: ^11.3.0 flutter_local_notifications: 19.4.0 timeline_tile: ^2.0.0