added firebase code
This commit is contained in:
parent
98d2dd4c46
commit
3ba3129b18
@ -3,6 +3,7 @@ plugins {
|
|||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load keystore properties from key.properties file
|
// Load keystore properties from key.properties file
|
||||||
@ -24,6 +25,8 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
// ✅ Enable core library desugaring for Java 8+ APIs
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure Kotlin options for JVM target
|
// Configure Kotlin options for JVM target
|
||||||
@ -75,3 +78,8 @@ android {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Add required dependencies for desugaring
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||||
|
}
|
||||||
|
29
android/app/google-services.json
Normal file
29
android/app/google-services.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "626581282477",
|
||||||
|
"project_id": "mtest-a0635",
|
||||||
|
"storage_bucket": "mtest-a0635.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.marco.aiotstage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyCBkDQRpbSdR0bo6pO4Bm0ZIdXkdaE3z-A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
@ -4,6 +4,8 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -38,6 +40,9 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="high_importance_channel"/>
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
@ -20,6 +20,7 @@ plugins {
|
|||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
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.2.1" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" 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
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
13
assets/service-account.json
Normal file
13
assets/service-account.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "mtest-a0635",
|
||||||
|
"private_key_id": "39a69f7d2a64234784e0d0ce6c113052296d6dc1",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCimbxktO7PeQ4h\n81Ye2ZBcZjltDhqqD0o9XyLmNdHszzM056bwpJkvgoyyTJAIvR2fcBF3YQFyuC+1\nddLHtchP48FjflZ+zZzLp7oaA/Zh28OZLbCsu+Nm8vO3WJVoIaJYgi+jEz21G128\ncOIbgkKIpLMz1wQhPPOwDTuSdQ+WajWJb04/aNrmTRH1hMreyhHiIFmalcavUgc1\nY5FvgrGs7EaKjYBevoFN3dwmEXjfyHjfBSxnt1yytl9tbtINqdrYLYAMm1l3+KqO\nCGxicQE5kjI1osI2wRjsk105RHnpxPg2GZnI4vTIOkEY5czhRSOs94g2d628H6fq\nVzf9UqwtAgMBAAECggEARluLf3AjHbdd/CbVDwhJRRIeqye9NfTjxOaTrVWAfp2x\npKTQQbSXbE1rIAOtF3rthH3zsNpSzBcS3cwb5rqr8JW2qpySRNAnlp//ER7Bz9pO\nKsvwdO3gGj3qY117WNGk8/NxNXkv7FvpFY8q54hXzdSmjjnt2YwMThOLwXXRxt2B\nFxN3FpBWqw12epqS162nW2nIRJ34Jloil4J5x61Sc79MCFyCxyhMlrBkY+Ni/xb1\nigBXBjczxNiJqqDie0mc16WB1HMEcBP9Yjtb46Hhfs3NDDWNqDkoM8QmEMSg8EHy\nyjcSlf0Wj8I9Kf+0PZo+2FB2DbuhfA8IVR9U/c00KQKBgQDd/OULx6QpmUev1Gl/\nrwwN67ZUMJ72cRuwvLFsMTIzZ+oItO0AR1uMkRZ1crOMc490XNUvSCGP6piZQAn1\nro8qNAh+0Q/UvKHM1khOj/4DxEGZRnNOhe6QLZM9QNygENuEYfdYDD9wcQI9Xs+B\nMIOBsuuqUVHlsbvYkeYNS8M8swKBgQC7g3i1dYRC/bkNMthVS4GTlFRuLscyIjTi\nhruhdaSE+fBZ5RO3XDzz6oDHYcdo/z5ySqI7EIsckNRbwFsMCOjSP3xJapadPYwU\nIhZBU7lgNlPnHJ/BIUwA5JZqRqGTNWrFINUHZFp2RK/x2bYdfoqY8bq08eWs9gmR\nc7U7i+6jnwKBgGaO3isxExD89fewBQWuk70it1vyEp785rQimT3JBM5nJeLb49sL\nHKq2pU+hrH4pLY+vC/cKNidNVS8IPRG6kf4HiB0+7Td15rLCFSnmsI6A72Wm/MK8\ncdk+lRXpj4SMBT8GG8Yb8ns6WrSLxwaCqV8UkHhhlZqvIIAP998Qr6StAoGAQTwr\n8nU/3k6G4qCdwo7SNZWVCgAcLMTZwTU+cZ2L7vdFNwELKu9cBT/ALZ1G0rB5+Skd\n546J1xZLyt/QzQ8McJjFlIUQgQO4iAiT1YZbJ62+4tiCe54p4uWjrrWD4MLkslAJ\nzNiM4DhlPa6QPRKZBTyTx/+f99xg18l5c43rJ+ECgYBkXMfjdn8SOaG6ggJrf1xx\nas49vwAscx4AJaOdVu3D8lCwoNCuAJhBHcFqsJ0wEHWpsqKAdXxqX/Nt2x8t7zL0\nPoRCvfsq5P7GdRrNhrHxLwjDqh+OS+Ow6t0esPQ5RPBgtjvthAlb7bV2nIfkpmdl\nFbjML8vkXk9iPJsbAfO2jw==\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "firebase-adminsdk-fbsvc@mtest-a0635.iam.gserviceaccount.com",
|
||||||
|
"client_id": "111097905744982732087",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40mtest-a0635.iam.gserviceaccount.com",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
@ -6,7 +6,8 @@ import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart'; // <-- logging
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
||||||
|
|
||||||
class LoginController extends MyController {
|
class LoginController extends MyController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -55,12 +56,17 @@ class LoginController extends MyController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final loginData = basicValidator.getData();
|
final loginData = basicValidator.getData();
|
||||||
logSafe("Attempting login for user: ${loginData['username']}", );
|
|
||||||
|
// ✅ Get FCM token
|
||||||
|
final fcmToken = await FirebaseNotificationService().getFcmToken();
|
||||||
|
loginData['fcmToken'] = fcmToken ?? '';
|
||||||
|
|
||||||
|
logSafe("Attempting login for user: ${loginData['username']} with FCM token: ${loginData['fcmToken']}");
|
||||||
|
|
||||||
final errors = await AuthService.loginUser(loginData);
|
final errors = await AuthService.loginUser(loginData);
|
||||||
|
|
||||||
if (errors != null) {
|
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(
|
showAppSnackbar(
|
||||||
title: "Login Failed",
|
title: "Login Failed",
|
||||||
@ -73,7 +79,7 @@ class LoginController extends MyController {
|
|||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
} else {
|
} else {
|
||||||
await _handleRememberMe();
|
await _handleRememberMe();
|
||||||
logSafe("Login successful for user: ${loginData['username']}", );
|
logSafe("Login successful for user: ${loginData['username']}");
|
||||||
Get.toNamed('/home');
|
Get.toNamed('/home');
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
|
@ -60,6 +60,18 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Project & Employee ------------------
|
// ------------------ Project & Employee ------------------
|
||||||
|
/// Called when a notification says attendance has been updated
|
||||||
|
Future<void> refreshDataFromNotification({String? projectId}) async {
|
||||||
|
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
||||||
|
if (projectId == null) {
|
||||||
|
logSafe("No project selected for attendance refresh from notification",
|
||||||
|
level: LogLevel.warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fetchProjectData(projectId);
|
||||||
|
logSafe(
|
||||||
|
"Attendance data refreshed from notification for project $projectId");
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
@ -94,7 +106,6 @@ class AttendanceController extends GetxController {
|
|||||||
logSafe("Failed to fetch employees for project $projectId",
|
logSafe("Failed to fetch employees for project $projectId",
|
||||||
level: LogLevel.error);
|
level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
isLoadingEmployees.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
class ApiEndpoints {
|
class ApiEndpoints {
|
||||||
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||||
static const String baseUrl = "https://api.marcoaiot.com/api";
|
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||||
|
|
||||||
// Dashboard Module API Endpoints
|
// Dashboard Module API Endpoints
|
||||||
static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview";
|
static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview";
|
||||||
|
@ -1,79 +1,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:url_strategy/url_strategy.dart';
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
||||||
|
import 'package:marco/helpers/services/device_info_service.dart';
|
||||||
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
|
|
||||||
Future<void> initializeApp() async {
|
Future<void> initializeApp() async {
|
||||||
try {
|
try {
|
||||||
logSafe("💡 Starting app initialization...");
|
logSafe("💡 Starting app initialization...");
|
||||||
|
|
||||||
// UI Setup
|
await Future.wait([
|
||||||
setPathUrlStrategy();
|
_setupUI(),
|
||||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
_setupFirebase(),
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
_setupLocalStorage(),
|
||||||
const SystemUiOverlayStyle(
|
]);
|
||||||
statusBarColor: Colors.transparent,
|
|
||||||
systemNavigationBarColor: Colors.transparent,
|
|
||||||
statusBarIconBrightness: Brightness.light,
|
|
||||||
systemNavigationBarIconBrightness: Brightness.dark,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
logSafe("💡 UI setup completed.");
|
|
||||||
|
|
||||||
// Local storage
|
await _setupDeviceInfo();
|
||||||
await LocalStorage.init();
|
await _handleAuthTokens();
|
||||||
logSafe("💡 Local storage initialized.");
|
await _setupTheme();
|
||||||
|
await _setupControllers();
|
||||||
|
await _setupFirebaseMessaging();
|
||||||
|
|
||||||
// Token handling
|
_finalizeAppStyle();
|
||||||
final refreshToken = await LocalStorage.getRefreshToken();
|
|
||||||
final hasRefreshToken = refreshToken?.isNotEmpty ?? false;
|
|
||||||
|
|
||||||
if (hasRefreshToken) {
|
|
||||||
logSafe("🔁 Refresh token found. Attempting to refresh JWT...");
|
|
||||||
final success = await AuthService.refreshToken();
|
|
||||||
if (!success) {
|
|
||||||
logSafe("⚠️ Refresh token invalid or expired. Skipping controller injection.");
|
|
||||||
// Optionally clear tokens or handle logout here
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logSafe("❌ No refresh token found. Skipping refresh.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme setup
|
|
||||||
await ThemeCustomizer.init();
|
|
||||||
logSafe("💡 Theme customizer initialized.");
|
|
||||||
|
|
||||||
// Controller setup
|
|
||||||
final token = LocalStorage.getString('jwt_token');
|
|
||||||
final hasJwt = token?.isNotEmpty ?? false;
|
|
||||||
|
|
||||||
if (hasJwt) {
|
|
||||||
if (!Get.isRegistered<PermissionController>()) {
|
|
||||||
Get.put(PermissionController());
|
|
||||||
logSafe("💡 PermissionController injected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Get.isRegistered<ProjectController>()) {
|
|
||||||
Get.put(ProjectController(), permanent: true);
|
|
||||||
logSafe("💡 ProjectController injected as permanent.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await Get.find<PermissionController>().loadData(token!);
|
|
||||||
await Get.find<ProjectController>().fetchProjects();
|
|
||||||
} else {
|
|
||||||
logSafe("⚠️ No valid JWT token found. Skipping controller initialization.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final style setup
|
|
||||||
AppStyle.init();
|
|
||||||
logSafe("💡 AppStyle initialized.");
|
|
||||||
|
|
||||||
logSafe("✅ App initialization completed successfully.");
|
logSafe("✅ App initialization completed successfully.");
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
@ -86,3 +43,83 @@ Future<void> initializeApp() async {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _setupUI() async {
|
||||||
|
setPathUrlStrategy();
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
systemNavigationBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
systemNavigationBarIconBrightness: Brightness.dark,
|
||||||
|
));
|
||||||
|
logSafe("💡 UI setup completed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupFirebase() async {
|
||||||
|
await Firebase.initializeApp();
|
||||||
|
logSafe("💡 Firebase initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupLocalStorage() async {
|
||||||
|
await LocalStorage.init();
|
||||||
|
logSafe("💡 Local storage initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupDeviceInfo() async {
|
||||||
|
final deviceInfoService = DeviceInfoService();
|
||||||
|
await deviceInfoService.init();
|
||||||
|
logSafe("📱 Device Info: ${deviceInfoService.deviceData}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleAuthTokens() async {
|
||||||
|
final refreshToken = await LocalStorage.getRefreshToken();
|
||||||
|
if (refreshToken?.isNotEmpty ?? false) {
|
||||||
|
logSafe("🔁 Refresh token found. Attempting to refresh JWT...");
|
||||||
|
final success = await AuthService.refreshToken();
|
||||||
|
if (!success) {
|
||||||
|
logSafe(
|
||||||
|
"⚠️ Refresh token invalid or expired. Skipping controller injection.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logSafe("❌ No refresh token found. Skipping refresh.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupTheme() async {
|
||||||
|
await ThemeCustomizer.init();
|
||||||
|
logSafe("💡 Theme customizer initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupControllers() async {
|
||||||
|
final token = LocalStorage.getString('jwt_token');
|
||||||
|
if (token?.isEmpty ?? true) {
|
||||||
|
logSafe("⚠️ No valid JWT token found. Skipping controller initialization.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<PermissionController>()) {
|
||||||
|
Get.put(PermissionController());
|
||||||
|
logSafe("💡 PermissionController injected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<ProjectController>()) {
|
||||||
|
Get.put(ProjectController(), permanent: true);
|
||||||
|
logSafe("💡 ProjectController injected as permanent.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
Get.find<PermissionController>().loadData(token!),
|
||||||
|
Get.find<ProjectController>().fetchProjects(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupFirebaseMessaging() async {
|
||||||
|
await FirebaseNotificationService().initialize();
|
||||||
|
logSafe("💡 Firebase Messaging initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finalizeAppStyle() {
|
||||||
|
AppStyle.init();
|
||||||
|
logSafe("💡 AppStyle initialized.");
|
||||||
|
}
|
||||||
|
@ -16,277 +16,265 @@ class AuthService {
|
|||||||
|
|
||||||
static bool isLoggedIn = false;
|
static bool isLoggedIn = false;
|
||||||
|
|
||||||
/// Login with email and password
|
/* -------------------------------------------------------------------------- */
|
||||||
static Future<Map<String, String>?> loginUser(Map<String, dynamic> data) async {
|
/* Public Methods */
|
||||||
try {
|
/* -------------------------------------------------------------------------- */
|
||||||
logSafe("Attempting login...");
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse("$_baseUrl/auth/login-mobile"),
|
|
||||||
headers: _headers,
|
|
||||||
body: jsonEncode(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
final responseData = jsonDecode(response.body);
|
static Future<bool> registerDeviceToken(String fcmToken) async {
|
||||||
if (response.statusCode == 200 && responseData['data'] != null) {
|
final token = await LocalStorage.getJwtToken();
|
||||||
await _handleLoginSuccess(responseData['data']);
|
if (token == null || token.isEmpty) {
|
||||||
return null;
|
logSafe("❌ Cannot register device token: missing JWT token",
|
||||||
} else if (response.statusCode == 401) {
|
level: LogLevel.warning);
|
||||||
logSafe("Invalid login credentials.", level: LogLevel.warning);
|
return false;
|
||||||
return {"password": "Invalid email or password"};
|
|
||||||
} else {
|
|
||||||
logSafe("Login error: ${responseData['message']}", level: LogLevel.warning);
|
|
||||||
return {"error": responseData['message'] ?? "Unexpected error occurred"};
|
|
||||||
}
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Login exception", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
return {"error": "Network error. Please check your connection."};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final body = {"fcmToken": fcmToken};
|
||||||
|
final data = await _post("/auth/set/device-token", body, authToken: token);
|
||||||
|
if (data != null && data['success'] == true) {
|
||||||
|
logSafe("✅ Device token registered successfully.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logSafe("⚠️ Failed to register device token: ${data?['message']}",
|
||||||
|
level: LogLevel.warning);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, String>?> loginUser(
|
||||||
|
Map<String, dynamic> data) async {
|
||||||
|
logSafe("Attempting login...");
|
||||||
|
logSafe("Login payload (raw): $data");
|
||||||
|
logSafe("Login payload (JSON): ${jsonEncode(data)}");
|
||||||
|
|
||||||
|
final responseData = await _post("/auth/login-mobile", data);
|
||||||
|
if (responseData == null)
|
||||||
|
return {"error": "Network error. Please check your connection."};
|
||||||
|
|
||||||
|
if (responseData['data'] != null) {
|
||||||
|
await _handleLoginSuccess(responseData['data']);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (responseData['statusCode'] == 401) {
|
||||||
|
return {"password": "Invalid email or password"};
|
||||||
|
}
|
||||||
|
return {"error": responseData['message'] ?? "Unexpected error occurred"};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh JWT token
|
|
||||||
static Future<bool> refreshToken() async {
|
static Future<bool> refreshToken() async {
|
||||||
final accessToken = await LocalStorage.getJwtToken();
|
final accessToken = await LocalStorage.getJwtToken();
|
||||||
final refreshToken = await LocalStorage.getRefreshToken();
|
final refreshToken = await LocalStorage.getRefreshToken();
|
||||||
|
|
||||||
if (accessToken == null || refreshToken == null || accessToken.isEmpty || refreshToken.isEmpty) {
|
if ([accessToken, refreshToken].any((t) => t == null || t.isEmpty)) {
|
||||||
logSafe("Missing access or refresh token.", level: LogLevel.warning);
|
logSafe("Missing access or refresh token.", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final requestBody = {
|
final body = {"token": accessToken, "refreshToken": refreshToken};
|
||||||
"token": accessToken,
|
final data = await _post("/auth/refresh-token", body);
|
||||||
"refreshToken": refreshToken,
|
if (data != null && data['success'] == true) {
|
||||||
};
|
await LocalStorage.setJwtToken(data['data']['token']);
|
||||||
|
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
||||||
|
await LocalStorage.setLoggedInUser(true);
|
||||||
|
logSafe("Token refreshed successfully.");
|
||||||
|
|
||||||
try {
|
// 🔹 Retry FCM token registration after token refresh
|
||||||
logSafe("Refreshing token...");
|
final newFcmToken = await LocalStorage.getFcmToken();
|
||||||
final response = await http.post(
|
if (newFcmToken?.isNotEmpty ?? false) {
|
||||||
Uri.parse("$_baseUrl/auth/refresh-token"),
|
final success = await registerDeviceToken(newFcmToken!);
|
||||||
headers: _headers,
|
logSafe(
|
||||||
body: jsonEncode(requestBody),
|
success
|
||||||
);
|
? "✅ FCM token re-registered after JWT refresh."
|
||||||
|
: "⚠️ Failed to register FCM token after JWT refresh.",
|
||||||
final data = jsonDecode(response.body);
|
level: success ? LogLevel.info : LogLevel.warning);
|
||||||
if (response.statusCode == 200 && data['success'] == true) {
|
|
||||||
await LocalStorage.setJwtToken(data['data']['token']);
|
|
||||||
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
|
||||||
await LocalStorage.setLoggedInUser(true);
|
|
||||||
logSafe("Token refreshed successfully.");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
logSafe("Refresh token failed: ${data['message']}", level: LogLevel.warning);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Token refresh exception", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
return true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
logSafe("Refresh token failed: ${data?['message']}",
|
||||||
|
level: LogLevel.warning);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forgot password
|
static Future<Map<String, String>?> forgotPassword(String email) =>
|
||||||
static Future<Map<String, String>?> forgotPassword(String email) async {
|
_wrapErrorHandling(() => _post("/auth/forgot-password", {"email": email}),
|
||||||
try {
|
successCondition: (data) => data['success'] == true,
|
||||||
logSafe("Forgot password requested.");
|
defaultError: "Failed to send reset link.");
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse("$_baseUrl/auth/forgot-password"),
|
|
||||||
headers: _headers,
|
|
||||||
body: jsonEncode({"email": email}),
|
|
||||||
);
|
|
||||||
|
|
||||||
final data = jsonDecode(response.body);
|
static Future<Map<String, String>?> requestDemo(
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
Map<String, dynamic> demoData) =>
|
||||||
return {"error": data['message'] ?? "Failed to send reset link."};
|
_wrapErrorHandling(() => _post("/market/inquiry", demoData),
|
||||||
} catch (e, stacktrace) {
|
successCondition: (data) => data['success'] == true,
|
||||||
logSafe("Forgot password error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
defaultError: "Failed to submit demo request.");
|
||||||
return {"error": "Network error. Please check your connection."};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request demo
|
|
||||||
static Future<Map<String, String>?> requestDemo(Map<String, dynamic> demoData) async {
|
|
||||||
try {
|
|
||||||
logSafe("Submitting demo request...");
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse("$_baseUrl/market/inquiry"),
|
|
||||||
headers: _headers,
|
|
||||||
body: jsonEncode(demoData),
|
|
||||||
);
|
|
||||||
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
|
||||||
return {"error": data['message'] ?? "Failed to submit demo request."};
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Request demo error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
return {"error": "Network error. Please check your connection."};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get list of industries
|
|
||||||
static Future<List<Map<String, dynamic>>?> getIndustries() async {
|
static Future<List<Map<String, dynamic>>?> getIndustries() async {
|
||||||
try {
|
final data = await _get("/market/industries");
|
||||||
logSafe("Fetching industries list...");
|
if (data != null && data['success'] == true) {
|
||||||
final response = await http.get(
|
return List<Map<String, dynamic>>.from(data['data']);
|
||||||
Uri.parse("$_baseUrl/market/industries"),
|
|
||||||
headers: _headers,
|
|
||||||
);
|
|
||||||
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) {
|
|
||||||
return List<Map<String, dynamic>>.from(data['data']);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Get industries error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate MPIN
|
|
||||||
static Future<Map<String, String>?> generateMpin({
|
static Future<Map<String, String>?> generateMpin({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
required String mpin,
|
required String mpin,
|
||||||
}) async {
|
}) =>
|
||||||
final token = await LocalStorage.getJwtToken();
|
_wrapErrorHandling(
|
||||||
logSafe("Generating MPIN for employeeId: $employeeId");
|
() async {
|
||||||
logSafe("MPIN: $mpin");
|
final token = await LocalStorage.getJwtToken();
|
||||||
try {
|
return _post(
|
||||||
logSafe("Generating MPIN...");
|
"/auth/generate-mpin",
|
||||||
final response = await http.post(
|
{"employeeId": employeeId, "mpin": mpin},
|
||||||
Uri.parse("$_baseUrl/auth/generate-mpin"),
|
authToken: token,
|
||||||
headers: {
|
);
|
||||||
..._headers,
|
|
||||||
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
|
|
||||||
},
|
},
|
||||||
body: jsonEncode({"employeeId": employeeId, "mpin": mpin}),
|
successCondition: (data) => data['success'] == true,
|
||||||
|
defaultError: "Failed to generate MPIN.",
|
||||||
);
|
);
|
||||||
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
|
||||||
return {"error": data['message'] ?? "Failed to generate MPIN."};
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Generate MPIN error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
return {"error": "Network error. Please check your connection."};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify MPIN
|
|
||||||
static Future<Map<String, String>?> verifyMpin({
|
static Future<Map<String, String>?> verifyMpin({
|
||||||
required String mpin,
|
required String mpin,
|
||||||
required String mpinToken,
|
required String mpinToken,
|
||||||
}) async {
|
}) =>
|
||||||
final employeeInfo = LocalStorage.getEmployeeInfo();
|
_wrapErrorHandling(
|
||||||
if (employeeInfo == null) return {"error": "Employee info not found."};
|
() async {
|
||||||
|
final employeeInfo = LocalStorage.getEmployeeInfo();
|
||||||
final token = await LocalStorage.getJwtToken();
|
if (employeeInfo == null) return null;
|
||||||
|
final token = await LocalStorage.getJwtToken();
|
||||||
try {
|
return _post(
|
||||||
logSafe("Verifying MPIN...");
|
"/auth/login-mpin",
|
||||||
final response = await http.post(
|
{
|
||||||
Uri.parse("$_baseUrl/auth/login-mpin"),
|
"employeeId": employeeInfo.id,
|
||||||
headers: {
|
"mpin": mpin,
|
||||||
..._headers,
|
"mpinToken": mpinToken
|
||||||
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
|
},
|
||||||
|
authToken: token,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
body: jsonEncode({
|
successCondition: (data) => data['success'] == true,
|
||||||
"employeeId": employeeInfo.id,
|
defaultError: "MPIN verification failed.",
|
||||||
"mpin": mpin,
|
|
||||||
"mpinToken": mpinToken,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final data = jsonDecode(response.body);
|
static Future<Map<String, String>?> generateOtp(String email) =>
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
_wrapErrorHandling(() => _post("/auth/send-otp", {"email": email}),
|
||||||
return {"error": data['message'] ?? "MPIN verification failed."};
|
successCondition: (data) => data['success'] == true,
|
||||||
} catch (e, stacktrace) {
|
defaultError: "Failed to generate OTP.");
|
||||||
logSafe("Verify MPIN error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
return {"error": "Network error. Please check your connection."};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate OTP
|
|
||||||
static Future<Map<String, String>?> generateOtp(String email) async {
|
|
||||||
try {
|
|
||||||
logSafe("Generating OTP for email...");
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse("$_baseUrl/auth/send-otp"),
|
|
||||||
headers: _headers,
|
|
||||||
body: jsonEncode({"email": email}),
|
|
||||||
);
|
|
||||||
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
|
||||||
return {"error": data['message'] ?? "Failed to generate OTP."};
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Generate OTP error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
return {"error": "Network error. Please check your connection."};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify OTP and login
|
|
||||||
static Future<Map<String, String>?> verifyOtp({
|
static Future<Map<String, String>?> verifyOtp({
|
||||||
required String email,
|
required String email,
|
||||||
required String otp,
|
required String otp,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
final data = await _post("/auth/login-otp", {"email": email, "otp": otp});
|
||||||
logSafe("Verifying OTP...");
|
if (data != null && data['data'] != null) {
|
||||||
final response = await http.post(
|
await _handleLoginSuccess(data['data']);
|
||||||
Uri.parse("$_baseUrl/auth/login-otp"),
|
return null;
|
||||||
headers: _headers,
|
}
|
||||||
body: jsonEncode({"email": email, "otp": otp}),
|
return {"error": data?['message'] ?? "OTP verification failed."};
|
||||||
);
|
}
|
||||||
|
|
||||||
final data = jsonDecode(response.body);
|
/* -------------------------------------------------------------------------- */
|
||||||
if (response.statusCode == 200 && data['data'] != null) {
|
/* Private Utilities */
|
||||||
await _handleLoginSuccess(data['data']);
|
/* -------------------------------------------------------------------------- */
|
||||||
return null;
|
|
||||||
}
|
static Future<Map<String, dynamic>?> _post(
|
||||||
return {"error": data['message'] ?? "OTP verification failed."};
|
String path,
|
||||||
} catch (e, stacktrace) {
|
Map<String, dynamic> body, {
|
||||||
logSafe("Verify OTP error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
String? authToken,
|
||||||
return {"error": "Network error. Please check your connection."};
|
}) async {
|
||||||
|
try {
|
||||||
|
final headers = {
|
||||||
|
..._headers,
|
||||||
|
if (authToken?.isNotEmpty ?? false)
|
||||||
|
'Authorization': 'Bearer $authToken',
|
||||||
|
};
|
||||||
|
final response = await http.post(Uri.parse("$_baseUrl$path"),
|
||||||
|
headers: headers, body: jsonEncode(body));
|
||||||
|
return {
|
||||||
|
...jsonDecode(response.body),
|
||||||
|
"statusCode": response.statusCode,
|
||||||
|
};
|
||||||
|
} catch (e, st) {
|
||||||
|
_handleError("$path POST error", e, st);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle login success flow
|
static Future<Map<String, dynamic>?> _get(
|
||||||
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
|
String path, {
|
||||||
logSafe("Processing login success...");
|
String? authToken,
|
||||||
|
}) async {
|
||||||
final jwtToken = data['token'];
|
try {
|
||||||
final refreshToken = data['refreshToken'];
|
final headers = {
|
||||||
final mpinToken = data['mpinToken'];
|
..._headers,
|
||||||
|
if (authToken?.isNotEmpty ?? false)
|
||||||
// Save tokens
|
'Authorization': 'Bearer $authToken',
|
||||||
await LocalStorage.setJwtToken(jwtToken);
|
};
|
||||||
await LocalStorage.setLoggedInUser(true);
|
final response =
|
||||||
|
await http.get(Uri.parse("$_baseUrl$path"), headers: headers);
|
||||||
if (refreshToken != null) {
|
return {
|
||||||
await LocalStorage.setRefreshToken(refreshToken);
|
...jsonDecode(response.body),
|
||||||
|
"statusCode": response.statusCode,
|
||||||
|
};
|
||||||
|
} catch (e, st) {
|
||||||
|
_handleError("$path GET error", e, st);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mpinToken != null && mpinToken.isNotEmpty) {
|
static Future<Map<String, String>?> _wrapErrorHandling(
|
||||||
await LocalStorage.setMpinToken(mpinToken);
|
Future<Map<String, dynamic>?> Function() request, {
|
||||||
await LocalStorage.setIsMpin(true);
|
required bool Function(Map<String, dynamic> data) successCondition,
|
||||||
} else {
|
required String defaultError,
|
||||||
await LocalStorage.setIsMpin(false);
|
}) async {
|
||||||
await LocalStorage.removeMpinToken();
|
final data = await request();
|
||||||
|
if (data != null && successCondition(data)) return null;
|
||||||
|
return {"error": data?['message'] ?? defaultError};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject controllers if not already registered
|
static void _handleError(String message, Object error, StackTrace st) {
|
||||||
if (!Get.isRegistered<PermissionController>()) {
|
logSafe(message, level: LogLevel.error, error: error, stackTrace: st);
|
||||||
Get.put(PermissionController());
|
|
||||||
logSafe("✅ PermissionController injected after login.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Get.isRegistered<ProjectController>()) {
|
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
|
||||||
Get.put(ProjectController(), permanent: true);
|
logSafe("Processing login success...");
|
||||||
logSafe("✅ ProjectController injected after login.");
|
|
||||||
|
await LocalStorage.setJwtToken(data['token']);
|
||||||
|
await LocalStorage.setLoggedInUser(true);
|
||||||
|
|
||||||
|
if (data['refreshToken'] != null) {
|
||||||
|
await LocalStorage.setRefreshToken(data['refreshToken']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data['mpinToken']?.isNotEmpty ?? false) {
|
||||||
|
await LocalStorage.setMpinToken(data['mpinToken']);
|
||||||
|
await LocalStorage.setIsMpin(true);
|
||||||
|
} else {
|
||||||
|
await LocalStorage.setIsMpin(false);
|
||||||
|
await LocalStorage.removeMpinToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<PermissionController>()) {
|
||||||
|
Get.put(PermissionController());
|
||||||
|
logSafe("✅ PermissionController injected after login.");
|
||||||
|
}
|
||||||
|
if (!Get.isRegistered<ProjectController>()) {
|
||||||
|
Get.put(ProjectController(), permanent: true);
|
||||||
|
logSafe("✅ ProjectController injected after login.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Get.find<PermissionController>().loadData(data['token']);
|
||||||
|
await Get.find<ProjectController>().fetchProjects();
|
||||||
|
|
||||||
|
// 🔹 Always try to register FCM token after login
|
||||||
|
final fcmToken = await LocalStorage.getFcmToken();
|
||||||
|
if (fcmToken?.isNotEmpty ?? false) {
|
||||||
|
final success = await registerDeviceToken(fcmToken!);
|
||||||
|
logSafe(
|
||||||
|
success
|
||||||
|
? "✅ FCM token registered after login."
|
||||||
|
: "⚠️ Failed to register FCM token after login.",
|
||||||
|
level: success ? LogLevel.info : LogLevel.warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggedIn = true;
|
||||||
|
logSafe("✅ Login flow completed and controllers initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load data into controllers
|
|
||||||
await Get.find<PermissionController>().loadData(jwtToken);
|
|
||||||
await Get.find<ProjectController>().fetchProjects();
|
|
||||||
|
|
||||||
isLoggedIn = true;
|
|
||||||
logSafe("✅ Login flow completed and controllers initialized.");
|
|
||||||
}
|
}
|
||||||
}
|
|
51
lib/helpers/services/device_info_service.dart
Normal file
51
lib/helpers/services/device_info_service.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
|
||||||
|
class DeviceInfoService {
|
||||||
|
static final DeviceInfoService _instance = DeviceInfoService._internal();
|
||||||
|
factory DeviceInfoService() => _instance;
|
||||||
|
DeviceInfoService._internal();
|
||||||
|
|
||||||
|
final DeviceInfoPlugin _deviceInfoPlugin = DeviceInfoPlugin();
|
||||||
|
Map<String, dynamic> _deviceData = {};
|
||||||
|
|
||||||
|
/// Initialize device info (call this in main before runApp)
|
||||||
|
Future<void> init() async {
|
||||||
|
try {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final androidInfo = await _deviceInfoPlugin.androidInfo;
|
||||||
|
_deviceData = {
|
||||||
|
'platform': 'Android',
|
||||||
|
'manufacturer': androidInfo.manufacturer,
|
||||||
|
'model': androidInfo.model,
|
||||||
|
'version': androidInfo.version.release,
|
||||||
|
'sdkInt': androidInfo.version.sdkInt,
|
||||||
|
'brand': androidInfo.brand,
|
||||||
|
'device': androidInfo.device,
|
||||||
|
'androidId': androidInfo.id,
|
||||||
|
};
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
final iosInfo = await _deviceInfoPlugin.iosInfo;
|
||||||
|
_deviceData = {
|
||||||
|
'platform': 'iOS',
|
||||||
|
'name': iosInfo.name,
|
||||||
|
'systemName': iosInfo.systemName,
|
||||||
|
'systemVersion': iosInfo.systemVersion,
|
||||||
|
'model': iosInfo.model,
|
||||||
|
'localizedModel': iosInfo.localizedModel,
|
||||||
|
'identifierForVendor': iosInfo.identifierForVendor,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
_deviceData = {'platform': 'Unknown'};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_deviceData = {'error': 'Failed to get device info: $e'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the whole device info map
|
||||||
|
Map<String, dynamic> get deviceData => _deviceData;
|
||||||
|
|
||||||
|
/// Get a specific property from device info
|
||||||
|
String? getProperty(String key) => _deviceData[key]?.toString();
|
||||||
|
}
|
205
lib/helpers/services/firebase/firebase_messaging_service.dart
Normal file
205
lib/helpers/services/firebase/firebase_messaging_service.dart
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
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';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
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
|
||||||
|
|
||||||
|
/// Firebase Notification Service
|
||||||
|
class FirebaseNotificationService {
|
||||||
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
||||||
|
final Logger _logger = Logger();
|
||||||
|
|
||||||
|
static const _fcmScopes = [
|
||||||
|
'https://www.googleapis.com/auth/firebase.messaging',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Initialize Firebase and FCM
|
||||||
|
Future<void> initialize() async {
|
||||||
|
await Firebase.initializeApp();
|
||||||
|
_logger.i('✅ Firebase initialized');
|
||||||
|
|
||||||
|
await _requestNotificationPermission();
|
||||||
|
_registerMessageListeners();
|
||||||
|
_registerTokenRefreshListener();
|
||||||
|
|
||||||
|
// Fetch token on app start
|
||||||
|
await getFcmToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request notification permission
|
||||||
|
Future<void> _requestNotificationPermission() async {
|
||||||
|
final settings = await _firebaseMessaging.requestPermission();
|
||||||
|
_logger.i('📩 Permission Status: ${settings.authorizationStatus}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Foreground, background, and tap listeners
|
||||||
|
void _registerMessageListeners() {
|
||||||
|
FirebaseMessaging.onMessage.listen((message) {
|
||||||
|
_logger.i('📩 Foreground Notification');
|
||||||
|
_logNotificationDetails(message);
|
||||||
|
|
||||||
|
// Handle UI updates
|
||||||
|
NotificationActionHandler.handle(message.data);
|
||||||
|
|
||||||
|
if (message.notification != null) {
|
||||||
|
LocalNotificationService.showNotification(
|
||||||
|
title: message.notification!.title ?? "No title",
|
||||||
|
body: message.notification!.body ?? "No body",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Token refresh handler
|
||||||
|
void _registerTokenRefreshListener() {
|
||||||
|
_firebaseMessaging.onTokenRefresh.listen((newToken) async {
|
||||||
|
_logger.i('🔄 Token refreshed: $newToken');
|
||||||
|
if (newToken.isEmpty) return;
|
||||||
|
|
||||||
|
await LocalStorage.setFcmToken(newToken);
|
||||||
|
|
||||||
|
final jwt = await LocalStorage.getJwtToken();
|
||||||
|
if (jwt?.isNotEmpty ?? false) {
|
||||||
|
final success = await AuthService.registerDeviceToken(newToken);
|
||||||
|
_logger.i(success
|
||||||
|
? '✅ 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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current token
|
||||||
|
Future<String?> getFcmToken() 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.');
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.e('❌ Failed to get FCM token', error: e, stackTrace: s);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a test notification using FCM v1 API
|
||||||
|
Future<void> sendTestNotification(String deviceToken) async {
|
||||||
|
try {
|
||||||
|
final client = await _getAuthenticatedHttpClient();
|
||||||
|
if (client == null) return;
|
||||||
|
|
||||||
|
final projectId = await _getProjectId();
|
||||||
|
if (projectId == null) return;
|
||||||
|
|
||||||
|
_logger.i('🏗 Firebase Project ID: $projectId');
|
||||||
|
|
||||||
|
final url = Uri.parse(
|
||||||
|
'https://fcm.googleapis.com/v1/projects/$projectId/messages:send');
|
||||||
|
final payload = _buildNotificationPayload(deviceToken);
|
||||||
|
|
||||||
|
final response = await client.post(
|
||||||
|
url,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode(payload),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
_logger.i('✅ Test notification sent successfully');
|
||||||
|
} else {
|
||||||
|
_logger.e('❌ Send failed: ${response.statusCode} ${response.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.e('❌ Error sending notification', error: e, stackTrace: s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticated HTTP client using service account
|
||||||
|
Future<http.Client?> _getAuthenticatedHttpClient() async {
|
||||||
|
try {
|
||||||
|
final credentials = ServiceAccountCredentials.fromJson(
|
||||||
|
json.decode(await rootBundle.loadString('assets/service-account.json')),
|
||||||
|
);
|
||||||
|
return clientViaServiceAccount(credentials, _fcmScopes);
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.e('❌ Failed to authenticate', error: e, stackTrace: s);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Project ID from service account
|
||||||
|
Future<String?> _getProjectId() async {
|
||||||
|
try {
|
||||||
|
final jsonMap = json
|
||||||
|
.decode(await rootBundle.loadString('assets/service-account.json'));
|
||||||
|
return jsonMap['project_id'];
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('❌ Failed to load project_id: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build FCM v1 payload
|
||||||
|
Map<String, dynamic> _buildNotificationPayload(String token) => {
|
||||||
|
"message": {
|
||||||
|
"token": token,
|
||||||
|
"notification": {
|
||||||
|
"title": "Test Notification",
|
||||||
|
"body": "This is a test message from Flutter (v1 API)"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"click_action": "FLUTTER_NOTIFICATION_CLICK",
|
||||||
|
"type": "expense_updated", // Example
|
||||||
|
"expense_id": "1234"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Handle tap on notification
|
||||||
|
void _handleNotificationTap(RemoteMessage message) {
|
||||||
|
_logger.i('📌 Notification tapped: ${message.data}');
|
||||||
|
NotificationActionHandler.handle(message.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log notification details
|
||||||
|
void _logNotificationDetails(RemoteMessage message) {
|
||||||
|
_logger
|
||||||
|
..i('🆔 ID: ${message.messageId}')
|
||||||
|
..i('📜 Title: ${message.notification?.title}')
|
||||||
|
..i('📜 Body: ${message.notification?.body}')
|
||||||
|
..i('📦 Data: ${message.data}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Background handler
|
||||||
|
Future<void> _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);
|
||||||
|
}
|
42
lib/helpers/services/local_notification_service.dart
Normal file
42
lib/helpers/services/local_notification_service.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
|
class LocalNotificationService {
|
||||||
|
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||||
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
const AndroidInitializationSettings androidInitSettings =
|
||||||
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
|
const InitializationSettings initSettings = InitializationSettings(
|
||||||
|
android: androidInitSettings,
|
||||||
|
iOS: DarwinInitializationSettings(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notificationsPlugin.initialize(initSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> showNotification({
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
}) async {
|
||||||
|
const AndroidNotificationDetails androidDetails =
|
||||||
|
AndroidNotificationDetails(
|
||||||
|
'default_channel_id',
|
||||||
|
'Default Channel',
|
||||||
|
importance: Importance.max,
|
||||||
|
priority: Priority.high,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
);
|
||||||
|
|
||||||
|
const NotificationDetails notificationDetails =
|
||||||
|
NotificationDetails(android: androidDetails);
|
||||||
|
|
||||||
|
await _notificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
notificationDetails,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
52
lib/helpers/services/notification_action_handler.dart
Normal file
52
lib/helpers/services/notification_action_handler.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||||
|
|
||||||
|
/// Handles incoming FCM notification actions and updates UI/controllers.
|
||||||
|
class NotificationActionHandler {
|
||||||
|
static final Logger _logger = Logger();
|
||||||
|
|
||||||
|
/// Main entry point — call this for any notification `data` map.
|
||||||
|
static void handle(Map<String, dynamic> data) {
|
||||||
|
_logger.i('📲 Handling notification action: $data');
|
||||||
|
|
||||||
|
if (data.isEmpty) {
|
||||||
|
_logger.w('⚠️ Empty notification data received.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final type = data['type'];
|
||||||
|
final action = data['Action'];
|
||||||
|
final keyword = data['Keyword'];
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
switch (type) {
|
||||||
|
case 'expense_updated':
|
||||||
|
break;
|
||||||
|
case 'attendance_updated':
|
||||||
|
_handleAttendanceUpdated(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_logger.w('⚠️ Unknown notification type: $type');
|
||||||
|
}
|
||||||
|
} else if (keyword == 'Attendance' && action == 'CHECK_IN' || action == 'CHECK_OUT' || action == 'REQUEST_REGULARIZE ' || action == 'REQUEST_DELETE '|| action == 'REGULARIZE ' || action == 'REGULARIZE_REJECT ') {
|
||||||
|
// Matches your current logs
|
||||||
|
_handleAttendanceUpdated(data);
|
||||||
|
} else {
|
||||||
|
_logger.w('⚠️ Unhandled notification: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _handleAttendanceUpdated(Map<String, dynamic> data) {
|
||||||
|
try {
|
||||||
|
final controller = Get.find<AttendanceController>();
|
||||||
|
controller.refreshDataFromNotification(
|
||||||
|
projectId: data['ProjectId'],
|
||||||
|
);
|
||||||
|
_logger.i('✅ AttendanceController refreshed from notification.');
|
||||||
|
} catch (e) {
|
||||||
|
_logger.w('⚠️ AttendanceController not found, cannot update.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,12 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/localizations/language.dart';
|
import 'package:marco/helpers/services/localizations/language.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:marco/model/user_permission.dart';
|
|
||||||
import 'package:marco/model/employee_info.dart';
|
import 'package:marco/model/employee_info.dart';
|
||||||
import 'dart:convert';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class LocalStorage {
|
class LocalStorage {
|
||||||
static const String _loggedInUserKey = "user";
|
static const String _loggedInUserKey = "user";
|
||||||
@ -19,181 +18,162 @@ class LocalStorage {
|
|||||||
static const String _employeeInfoKey = "employee_info";
|
static const String _employeeInfoKey = "employee_info";
|
||||||
static const String _mpinTokenKey = "mpinToken";
|
static const String _mpinTokenKey = "mpinToken";
|
||||||
static const String _isMpinKey = "isMpin";
|
static const String _isMpinKey = "isMpin";
|
||||||
|
static const String _fcmTokenKey = 'fcm_token';
|
||||||
|
|
||||||
static SharedPreferences? _preferencesInstance;
|
static SharedPreferences? _preferencesInstance;
|
||||||
|
|
||||||
static SharedPreferences get preferences {
|
static SharedPreferences get preferences {
|
||||||
if (_preferencesInstance == null) {
|
if (_preferencesInstance == null) {
|
||||||
throw ("Call LocalStorage.init() to initialize local storage");
|
throw ("Call LocalStorage.init() before using it");
|
||||||
}
|
}
|
||||||
return _preferencesInstance!;
|
return _preferencesInstance!;
|
||||||
}
|
}
|
||||||
// In LocalStorage class
|
|
||||||
|
|
||||||
static Future<bool> setUserPermissions(
|
/// Initialization
|
||||||
List<UserPermission> permissions) async {
|
|
||||||
// Convert the list of UserPermission objects to a List<Map<String, dynamic>>
|
|
||||||
final jsonList = permissions.map((e) => e.toJson()).toList();
|
|
||||||
|
|
||||||
// Save as a JSON string
|
|
||||||
return preferences.setString(_userPermissionsKey, jsonEncode(jsonList));
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<UserPermission> getUserPermissions() {
|
|
||||||
final storedJson = preferences.getString(_userPermissionsKey);
|
|
||||||
|
|
||||||
if (storedJson != null) {
|
|
||||||
final List<dynamic> parsedList = jsonDecode(storedJson);
|
|
||||||
return parsedList
|
|
||||||
.map((e) => UserPermission.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> removeUserPermissions() async {
|
|
||||||
return preferences.remove(_userPermissionsKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store EmployeeInfo
|
|
||||||
static Future<bool> setEmployeeInfo(EmployeeInfo employeeInfo) async {
|
|
||||||
final jsonData = employeeInfo.toJson();
|
|
||||||
return preferences.setString(_employeeInfoKey, jsonEncode(jsonData));
|
|
||||||
}
|
|
||||||
|
|
||||||
static EmployeeInfo? getEmployeeInfo() {
|
|
||||||
final storedJson = preferences.getString(_employeeInfoKey);
|
|
||||||
if (storedJson != null) {
|
|
||||||
final Map<String, dynamic> json = jsonDecode(storedJson);
|
|
||||||
return EmployeeInfo.fromJson(json);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> removeEmployeeInfo() async {
|
|
||||||
return preferences.remove(_employeeInfoKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other methods for handling JWT, refresh token, etc.
|
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
_preferencesInstance = await SharedPreferences.getInstance();
|
_preferencesInstance = await SharedPreferences.getInstance();
|
||||||
await initData();
|
await initData();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> initData() async {
|
static Future<void> initData() async {
|
||||||
SharedPreferences preferences = await SharedPreferences.getInstance();
|
AuthService.isLoggedIn =
|
||||||
AuthService.isLoggedIn = preferences.getBool(_loggedInUserKey) ?? false;
|
preferences.getBool(_loggedInUserKey) ?? false;
|
||||||
ThemeCustomizer.fromJSON(preferences.getString(_themeCustomizerKey));
|
ThemeCustomizer.fromJSON(
|
||||||
|
preferences.getString(_themeCustomizerKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> setLoggedInUser(bool loggedIn) async {
|
/// ================== User Permissions ==================
|
||||||
return preferences.setBool(_loggedInUserKey, loggedIn);
|
static Future<bool> setUserPermissions(
|
||||||
|
List<UserPermission> permissions) async {
|
||||||
|
final jsonList = permissions.map((e) => e.toJson()).toList();
|
||||||
|
return preferences.setString(
|
||||||
|
_userPermissionsKey, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> setCustomizer(ThemeCustomizer themeCustomizer) {
|
static List<UserPermission> getUserPermissions() {
|
||||||
return preferences.setString(_themeCustomizerKey, themeCustomizer.toJSON());
|
final storedJson = preferences.getString(_userPermissionsKey);
|
||||||
|
if (storedJson == null) return [];
|
||||||
|
return (jsonDecode(storedJson) as List)
|
||||||
|
.map((e) => UserPermission.fromJson(
|
||||||
|
e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> setLanguage(Language language) {
|
static Future<bool> removeUserPermissions() =>
|
||||||
return preferences.setString(_languageKey, language.locale.languageCode);
|
preferences.remove(_userPermissionsKey);
|
||||||
|
|
||||||
|
/// ================== Employee Info ==================
|
||||||
|
static Future<bool> setEmployeeInfo(EmployeeInfo employeeInfo) =>
|
||||||
|
preferences.setString(
|
||||||
|
_employeeInfoKey, jsonEncode(employeeInfo.toJson()));
|
||||||
|
|
||||||
|
static EmployeeInfo? getEmployeeInfo() {
|
||||||
|
final storedJson = preferences.getString(_employeeInfoKey);
|
||||||
|
return storedJson == null
|
||||||
|
? null
|
||||||
|
: EmployeeInfo.fromJson(jsonDecode(storedJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? getLanguage() {
|
static Future<bool> removeEmployeeInfo() =>
|
||||||
return preferences.getString(_languageKey);
|
preferences.remove(_employeeInfoKey);
|
||||||
|
|
||||||
|
/// ================== Login / Logout ==================
|
||||||
|
static Future<bool> setLoggedInUser(bool loggedIn) =>
|
||||||
|
preferences.setBool(_loggedInUserKey, loggedIn);
|
||||||
|
|
||||||
|
static Future<bool> removeLoggedInUser() =>
|
||||||
|
preferences.remove(_loggedInUserKey);
|
||||||
|
|
||||||
|
static Future<void> logout() async {
|
||||||
|
await removeLoggedInUser();
|
||||||
|
await removeToken(_jwtTokenKey);
|
||||||
|
await removeToken(_refreshTokenKey);
|
||||||
|
await removeUserPermissions();
|
||||||
|
await removeEmployeeInfo();
|
||||||
|
await removeMpinToken();
|
||||||
|
await removeIsMpin();
|
||||||
|
await preferences.remove("mpin_verified");
|
||||||
|
await preferences.remove(_languageKey);
|
||||||
|
await preferences.remove(_themeCustomizerKey);
|
||||||
|
await preferences.remove('selectedProjectId');
|
||||||
|
|
||||||
|
if (Get.isRegistered<ProjectController>()) {
|
||||||
|
Get.find<ProjectController>().clearProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.offAllNamed('/auth/login-option');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> removeLoggedInUser() async {
|
/// ================== Theme & Language ==================
|
||||||
return preferences.remove(_loggedInUserKey);
|
static Future<bool> setCustomizer(
|
||||||
}
|
ThemeCustomizer themeCustomizer) =>
|
||||||
|
preferences.setString(
|
||||||
|
_themeCustomizerKey, themeCustomizer.toJSON());
|
||||||
|
|
||||||
// Add methods to handle JWT and Refresh Token
|
static Future<bool> setLanguage(Language language) =>
|
||||||
static Future<bool> setToken(String key, String token) {
|
preferences.setString(
|
||||||
return preferences.setString(key, token);
|
_languageKey, language.locale.languageCode);
|
||||||
}
|
|
||||||
|
|
||||||
static String? getToken(String key) {
|
static String? getLanguage() =>
|
||||||
return preferences.getString(key);
|
preferences.getString(_languageKey);
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> removeToken(String key) {
|
/// ================== Tokens ==================
|
||||||
return preferences.remove(key);
|
static Future<bool> setToken(String key, String token) =>
|
||||||
}
|
preferences.setString(key, token);
|
||||||
|
|
||||||
// Convenience methods for getting the JWT and Refresh tokens
|
static String? getToken(String key) =>
|
||||||
static String? getJwtToken() {
|
preferences.getString(key);
|
||||||
return getToken(_jwtTokenKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String? getRefreshToken() {
|
static Future<bool> removeToken(String key) =>
|
||||||
return getToken(_refreshTokenKey);
|
preferences.remove(key);
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> setJwtToken(String jwtToken) {
|
static Future<bool> setJwtToken(String jwtToken) =>
|
||||||
return setToken(_jwtTokenKey, jwtToken);
|
setToken(_jwtTokenKey, jwtToken);
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> setRefreshToken(String refreshToken) {
|
static Future<bool> setRefreshToken(String refreshToken) =>
|
||||||
return setToken(_refreshTokenKey, refreshToken);
|
setToken(_refreshTokenKey, refreshToken);
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> logout() async {
|
static String? getJwtToken() => getToken(_jwtTokenKey);
|
||||||
await removeLoggedInUser();
|
|
||||||
await removeToken(_jwtTokenKey);
|
|
||||||
await removeToken(_refreshTokenKey);
|
|
||||||
await removeUserPermissions();
|
|
||||||
await removeEmployeeInfo();
|
|
||||||
await removeMpinToken();
|
|
||||||
await removeIsMpin();
|
|
||||||
await preferences.remove("mpin_verified");
|
|
||||||
await preferences.remove(_languageKey);
|
|
||||||
await preferences.remove(_themeCustomizerKey);
|
|
||||||
await preferences.remove('selectedProjectId');
|
|
||||||
if (Get.isRegistered<ProjectController>()) {
|
|
||||||
Get.find<ProjectController>().clearProjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
Get.offAllNamed('/auth/login-option');
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> setMpinToken(String token) {
|
|
||||||
return preferences.setString(_mpinTokenKey, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String? getMpinToken() {
|
|
||||||
return preferences.getString(_mpinTokenKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> removeMpinToken() {
|
|
||||||
return preferences.remove(_mpinTokenKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MPIN Enabled flag
|
|
||||||
static Future<bool> setIsMpin(bool value) {
|
|
||||||
return preferences.setBool(_isMpinKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool getIsMpin() {
|
|
||||||
return preferences.getBool(_isMpinKey) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> removeIsMpin() {
|
|
||||||
return preferences.remove(_isMpinKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> setBool(String key, bool value) async {
|
|
||||||
return preferences.setBool(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool? getBool(String key) {
|
|
||||||
return preferences.getBool(key);
|
|
||||||
}
|
|
||||||
// Save and retrieve String values
|
|
||||||
static String? getString(String key) {
|
|
||||||
return preferences.getString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> saveString(String key, String value) async {
|
|
||||||
return preferences.setString(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
static String? getRefreshToken() =>
|
||||||
|
getToken(_refreshTokenKey);
|
||||||
|
|
||||||
|
/// ================== FCM Token ==================
|
||||||
|
static Future<void> setFcmToken(String token) =>
|
||||||
|
preferences.setString(_fcmTokenKey, token);
|
||||||
|
|
||||||
|
static String? getFcmToken() =>
|
||||||
|
preferences.getString(_fcmTokenKey);
|
||||||
|
|
||||||
|
/// ================== MPIN ==================
|
||||||
|
static Future<bool> setMpinToken(String token) =>
|
||||||
|
preferences.setString(_mpinTokenKey, token);
|
||||||
|
|
||||||
|
static String? getMpinToken() =>
|
||||||
|
preferences.getString(_mpinTokenKey);
|
||||||
|
|
||||||
|
static Future<bool> removeMpinToken() =>
|
||||||
|
preferences.remove(_mpinTokenKey);
|
||||||
|
|
||||||
|
static Future<bool> setIsMpin(bool value) =>
|
||||||
|
preferences.setBool(_isMpinKey, value);
|
||||||
|
|
||||||
|
static bool getIsMpin() =>
|
||||||
|
preferences.getBool(_isMpinKey) ?? false;
|
||||||
|
|
||||||
|
static Future<bool> removeIsMpin() =>
|
||||||
|
preferences.remove(_isMpinKey);
|
||||||
|
|
||||||
|
/// ================== Generic Set/Get ==================
|
||||||
|
static Future<bool> setBool(String key, bool value) =>
|
||||||
|
preferences.setBool(key, value);
|
||||||
|
|
||||||
|
static bool? getBool(String key) =>
|
||||||
|
preferences.getBool(key);
|
||||||
|
|
||||||
|
static String? getString(String key) =>
|
||||||
|
preferences.getString(key);
|
||||||
|
|
||||||
|
static Future<bool> saveString(String key, String value) =>
|
||||||
|
preferences.setString(key, value);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ Future<void> main() async {
|
|||||||
logSafe("App initialized successfully.");
|
logSafe("App initialized successfully.");
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ChangeNotifierProvider<AppNotifier>(
|
ChangeNotifierProvider(
|
||||||
create: (_) => AppNotifier(),
|
create: (_) => AppNotifier(),
|
||||||
child: const MainWrapper(),
|
child: const MainWrapper(),
|
||||||
),
|
),
|
||||||
@ -31,24 +31,21 @@ Future<void> main() async {
|
|||||||
error: e,
|
error: e,
|
||||||
stackTrace: stacktrace,
|
stackTrace: stacktrace,
|
||||||
);
|
);
|
||||||
|
runApp(_buildErrorApp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runApp(
|
Widget _buildErrorApp() => const MaterialApp(
|
||||||
const MaterialApp(
|
home: Scaffold(
|
||||||
home: Scaffold(
|
body: Center(
|
||||||
body: Center(
|
child: Text(
|
||||||
child: Text(
|
"Failed to initialize the app.",
|
||||||
"Failed to initialize the app.",
|
style: TextStyle(color: Colors.red),
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This widget listens to connectivity changes and switches between
|
|
||||||
/// `MyApp` and `OfflineScreen` automatically.
|
|
||||||
class MainWrapper extends StatefulWidget {
|
class MainWrapper extends StatefulWidget {
|
||||||
const MainWrapper({super.key});
|
const MainWrapper({super.key});
|
||||||
|
|
||||||
@ -57,7 +54,6 @@ class MainWrapper extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MainWrapperState extends State<MainWrapper> {
|
class _MainWrapperState extends State<MainWrapper> {
|
||||||
// Use a List to store connectivity status as the API now returns a list
|
|
||||||
List<ConnectivityResult> _connectivityStatus = [ConnectivityResult.none];
|
List<ConnectivityResult> _connectivityStatus = [ConnectivityResult.none];
|
||||||
final Connectivity _connectivity = Connectivity();
|
final Connectivity _connectivity = Connectivity();
|
||||||
|
|
||||||
@ -65,38 +61,21 @@ class _MainWrapperState extends State<MainWrapper> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initializeConnectivity();
|
_initializeConnectivity();
|
||||||
// Listen for changes, the callback now provides a List<ConnectivityResult>
|
_connectivity.onConnectivityChanged.listen((results) {
|
||||||
_connectivity.onConnectivityChanged
|
setState(() => _connectivityStatus = results);
|
||||||
.listen((List<ConnectivityResult> results) {
|
|
||||||
setState(() {
|
|
||||||
_connectivityStatus = results;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeConnectivity() async {
|
Future<void> _initializeConnectivity() async {
|
||||||
// checkConnectivity() now returns a List<ConnectivityResult>
|
|
||||||
final result = await _connectivity.checkConnectivity();
|
final result = await _connectivity.checkConnectivity();
|
||||||
setState(() {
|
setState(() => _connectivityStatus = result);
|
||||||
_connectivityStatus = result;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Check if any of the connectivity results indicate no internet
|
final bool isOffline = _connectivityStatus.contains(ConnectivityResult.none);
|
||||||
final bool isOffline =
|
return isOffline
|
||||||
_connectivityStatus.contains(ConnectivityResult.none);
|
? const MaterialApp(debugShowCheckedModeBanner: false, home: OfflineScreen())
|
||||||
|
: const MyApp();
|
||||||
// Show OfflineScreen if no internet
|
|
||||||
if (isOffline) {
|
|
||||||
return const MaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
home: OfflineScreen(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show main app if online
|
|
||||||
return const MyApp();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/view/dashboard/dashboard_chart.dart';
|
import 'package:marco/view/dashboard/dashboard_chart.dart';
|
||||||
import 'package:marco/view/layouts/layout.dart';
|
import 'package:marco/view/layouts/layout.dart';
|
||||||
|
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
||||||
|
|
||||||
class DashboardScreen extends StatefulWidget {
|
class DashboardScreen extends StatefulWidget {
|
||||||
const DashboardScreen({super.key});
|
const DashboardScreen({super.key});
|
||||||
@ -21,7 +22,8 @@ class DashboardScreen extends StatefulWidget {
|
|||||||
static const String attendanceRoute = "/dashboard/attendance";
|
static const String attendanceRoute = "/dashboard/attendance";
|
||||||
static const String tasksRoute = "/dashboard/daily-task";
|
static const String tasksRoute = "/dashboard/daily-task";
|
||||||
static const String dailyTasksRoute = "/dashboard/daily-task-planing";
|
static const String dailyTasksRoute = "/dashboard/daily-task-planing";
|
||||||
static const String dailyTasksProgressRoute = "/dashboard/daily-task-progress";
|
static const String dailyTasksProgressRoute =
|
||||||
|
"/dashboard/daily-task-progress";
|
||||||
static const String directoryMainPageRoute = "/dashboard/directory-main-page";
|
static const String directoryMainPageRoute = "/dashboard/directory-main-page";
|
||||||
static const String expenseMainPageRoute = "/dashboard/expense-main-page";
|
static const String expenseMainPageRoute = "/dashboard/expense-main-page";
|
||||||
|
|
||||||
@ -30,7 +32,8 @@ class DashboardScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||||
final DashboardController dashboardController = Get.put(DashboardController());
|
final DashboardController dashboardController =
|
||||||
|
Get.put(DashboardController());
|
||||||
bool hasMpin = true;
|
bool hasMpin = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -52,6 +55,17 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final fcmService = FirebaseNotificationService();
|
||||||
|
final token = await fcmService.getFcmToken();
|
||||||
|
if (token != null) {
|
||||||
|
await fcmService.sendTestNotification(token);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Send Test Notification"),
|
||||||
|
),
|
||||||
|
MySpacing.height(10),
|
||||||
_buildDashboardStats(context),
|
_buildDashboardStats(context),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_buildAttendanceChartSection(),
|
_buildAttendanceChartSection(),
|
||||||
@ -64,12 +78,18 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
/// Dashboard Statistics Section with ProjectController
|
/// Dashboard Statistics Section with ProjectController
|
||||||
Widget _buildDashboardStats(BuildContext context) {
|
Widget _buildDashboardStats(BuildContext context) {
|
||||||
final stats = [
|
final stats = [
|
||||||
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, DashboardScreen.attendanceRoute),
|
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
|
||||||
_StatItem(LucideIcons.users, "Employees", contentTheme.warning, DashboardScreen.employeesRoute),
|
DashboardScreen.attendanceRoute),
|
||||||
_StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info, DashboardScreen.dailyTasksRoute),
|
_StatItem(LucideIcons.users, "Employees", contentTheme.warning,
|
||||||
_StatItem(LucideIcons.list_todo, "Daily Task Progress", contentTheme.info, DashboardScreen.dailyTasksProgressRoute),
|
DashboardScreen.employeesRoute),
|
||||||
_StatItem(LucideIcons.folder, "Directory", contentTheme.info, DashboardScreen.directoryMainPageRoute),
|
_StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info,
|
||||||
_StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info, DashboardScreen.expenseMainPageRoute),
|
DashboardScreen.dailyTasksRoute),
|
||||||
|
_StatItem(LucideIcons.list_todo, "Daily Task Progress", contentTheme.info,
|
||||||
|
DashboardScreen.dailyTasksProgressRoute),
|
||||||
|
_StatItem(LucideIcons.folder, "Directory", contentTheme.info,
|
||||||
|
DashboardScreen.directoryMainPageRoute),
|
||||||
|
_StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info,
|
||||||
|
DashboardScreen.expenseMainPageRoute),
|
||||||
];
|
];
|
||||||
|
|
||||||
return GetBuilder<ProjectController>(
|
return GetBuilder<ProjectController>(
|
||||||
@ -89,13 +109,15 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final maxWidth = constraints.maxWidth;
|
final maxWidth = constraints.maxWidth;
|
||||||
final crossAxisCount = (maxWidth / 100).floor().clamp(2, 4);
|
final crossAxisCount = (maxWidth / 100).floor().clamp(2, 4);
|
||||||
final cardWidth = (maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
|
final cardWidth =
|
||||||
|
(maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
runSpacing: 10,
|
runSpacing: 10,
|
||||||
children: stats
|
children: stats
|
||||||
.map((stat) => _buildStatCard(stat, cardWidth, isProjectSelected))
|
.map((stat) =>
|
||||||
|
_buildStatCard(stat, cardWidth, isProjectSelected))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -118,7 +140,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
ignoring: !isProjectSelected,
|
ignoring: !isProjectSelected,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: AttendanceDashboardChart(),
|
child: AttendanceDashboardChart(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -158,7 +180,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
runSpacing: 10,
|
runSpacing: 10,
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
4,
|
4,
|
||||||
(index) => _buildStatCardSkeleton(MediaQuery.of(context).size.width / 3),
|
(index) =>
|
||||||
|
_buildStatCardSkeleton(MediaQuery.of(context).size.width / 3),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -229,7 +252,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: "No Project Selected",
|
title: "No Project Selected",
|
||||||
middleText: "You need to select a project before accessing this section.",
|
middleText:
|
||||||
|
"You need to select a project before accessing this section.",
|
||||||
confirm: ElevatedButton(
|
confirm: ElevatedButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
child: const Text("OK"),
|
child: const Text("OK"),
|
||||||
|
@ -79,6 +79,12 @@ dependencies:
|
|||||||
quill_delta: ^3.0.0-nullsafety.2
|
quill_delta: ^3.0.0-nullsafety.2
|
||||||
connectivity_plus: ^6.1.4
|
connectivity_plus: ^6.1.4
|
||||||
geocoding: ^4.0.0
|
geocoding: ^4.0.0
|
||||||
|
firebase_core: ^3.14.0
|
||||||
|
firebase_messaging: ^15.2.7
|
||||||
|
googleapis_auth: ^2.0.0
|
||||||
|
device_info_plus: ^10.1.0
|
||||||
|
flutter_local_notifications: 19.4.0
|
||||||
|
|
||||||
timeline_tile: ^2.0.0
|
timeline_tile: ^2.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -112,6 +118,7 @@ flutter:
|
|||||||
- assets/logo/
|
- assets/logo/
|
||||||
- assets/logo/loading_logo.png
|
- assets/logo/loading_logo.png
|
||||||
- assets/social/
|
- assets/social/
|
||||||
|
- assets/service-account.json
|
||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
Loading…
x
Reference in New Issue
Block a user