diff --git a/lib/core/network/network_manager.dart b/lib/core/network/network_manager.dart index cb70b6a..997c69e 100644 --- a/lib/core/network/network_manager.dart +++ b/lib/core/network/network_manager.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tm_app/data/services/model/error/error_model.dart'; import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; import '../config/app_config.dart'; @@ -305,15 +306,24 @@ class NetworkManager { return 'Unknown error'; } - if (data is Map) { - return data['message'] ?? - data['error'] ?? - data['detail'] ?? - 'Unknown error'; - } + try { + if (data is Map) { + final Object? nested = data['error']; + final Map json = + nested is Map ? nested : data; + final ErrorModel model = ErrorModel.fromJson(json); + return model.message ?? + model.details ?? + model.code ?? + (data['detail'] as String?) ?? + 'Unknown error'; + } - if (data is String) { - return data; + if (data is String) { + return data; + } + } catch (_) { + // fall through to default message } return 'Unknown error'; diff --git a/lib/core/utils/error_utils.dart b/lib/core/utils/error_utils.dart new file mode 100644 index 0000000..17b876e --- /dev/null +++ b/lib/core/utils/error_utils.dart @@ -0,0 +1,19 @@ +import 'package:tm_app/core/network/app_exceptions.dart'; + +/// Utility class for handling error messages +class ErrorUtils { + /// Extracts a clean error message from an Exception + static String getErrorMessage(Exception error) { + if (error is AppException) { + return error.message; + } + + // For plain Exception, remove "Exception: " prefix + final errorString = error.toString(); + if (errorString.startsWith('Exception: ')) { + return errorString.substring(11); + } + + return errorString; + } +} diff --git a/lib/main.dart b/lib/main.dart index ea5eace..c33f307 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,25 +63,88 @@ class _MyAppState extends State { _handleMessage(initialMessage); } - if (kIsWeb) { - FirebaseMessaging.onMessage.listen((event) { - final overlayCtx = rootNavigatorKey.currentContext; - if (overlayCtx != null) { - if (overlayCtx.mounted) { - AppToast.notification( - overlayCtx, - event.notification?.title ?? 'New Notification', - event.notification?.body ?? 'New Notification', - ); - } + // if (kIsWeb) { + FirebaseMessaging.onMessage.listen((event) { + final overlayCtx = rootNavigatorKey.currentContext; + AppLogger().error('Notification received: ${event.notification?.web}'); + AppLogger().error('web link: ${event.notification?.title}'); + AppLogger().error('web link: ${event.notification?.web?.link}'); + print('Notification received1: ${event.notification?.web.toString()}'); + print( + 'Notification received1: ${event.notification?.web?.analyticsLabel}', + ); + + print('web link: ${event.notification?.web?.link}'); + print('Notification received: ${event.notification?.title}'); + AppLogger().error('notif data: ${event.data}'); + AppLogger().error('notif from: ${event.from}'); + AppLogger().error('notif type: ${event.messageType}'); + AppLogger().error('notif keys: ${event.data.keys}'); + AppLogger().info('notif title: ${event.notification?.title}'); + AppLogger().info('notif body: ${event.notification?.body}'); + AppLogger().info('notif data: ${event.data}'); + AppLogger().info('notif from: ${event.from}'); + AppLogger().info('notif type: ${event.messageType}'); + AppLogger().info('notif keys: ${event.data.keys}'); + if (overlayCtx != null) { + if (overlayCtx.mounted) { + AppLogger().error( + 'Notification received: ${event.notification?.web}', + ); + AppLogger().error( + ' link android: ${event.notification?.android?.link}', + ); + AppLogger().error( + 'android link image: ${event.notification?.android?.imageUrl}', + ); + AppLogger().error( + 'android link click action: ${event.notification?.android?.clickAction}', + ); + AppLogger().error( + 'web link image: ${event.notification?.web?.image}', + ); + print('Notification received: ${event.notification?.web}'); + print('web link: ${event.notification?.web?.link}'); + AppToast.notification( + overlayCtx, + event.notification?.title ?? 'New Notification', + event.notification?.body ?? 'New Notification', + ); } - }); - } + } + }); + // } FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); } void _handleMessage(RemoteMessage message) { + SharedPreferences.getInstance().then((value) { + value.setString('notif_data', message.data.toString()); + value.setString('notif_from', message.from.toString()); + value.setString('notif_type', message.messageType.toString()); + value.setString('notif_keys', message.data.keys.toString()); + value.setString('notif_title', message.notification?.title ?? ''); + value.setString('notif_body', message.notification?.body ?? ''); + value.setString('notif_data', message.contentAvailable.toString()); + value.setString('notif_web_link', message.notification?.web?.link ?? ''); + value.setString( + 'notif_android_link', + message.notification?.android?.link ?? '', + ); + value.setString( + 'notif_web_image', + message.notification?.web?.image ?? '', + ); + value.setString( + 'notif_android_image', + message.notification?.android?.imageUrl ?? '', + ); + value.setString( + 'notif_web_click_action', + message.notification?.web?.link ?? '', + ); + }); appRouter.go( const SplashScreenRoute(navigateToNotification: true).location, ); diff --git a/lib/view_models/auth_view_model.dart b/lib/view_models/auth_view_model.dart index 93f53b0..e3bfecd 100644 --- a/lib/view_models/auth_view_model.dart +++ b/lib/view_models/auth_view_model.dart @@ -147,7 +147,11 @@ class AuthViewModel with ChangeNotifier { } Future localLogout() async { - await _authRepository.localLogout(); + try { + await _authRepository.localLogout(); + } catch (e) { + // Handle local logout exceptions gracefully + } _loggedInUser = null; notifyListeners(); } @@ -160,7 +164,7 @@ class AuthViewModel with ChangeNotifier { switch (result) { case Ok(): - localLogout(); + await localLogout(); break; case Error(): _errorMessage = result.error.toString(); diff --git a/lib/view_models/profile_view_model.dart b/lib/view_models/profile_view_model.dart index 5e2f0f1..5f10ed3 100644 --- a/lib/view_models/profile_view_model.dart +++ b/lib/view_models/profile_view_model.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/data/repositories/auth_repository.dart'; import 'package:tm_app/data/services/model/company_profile_data/company_profile_data.dart'; import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart'; @@ -29,12 +30,65 @@ class ProfileViewModel with ChangeNotifier { CompanyProfileData? _companyProfileData; bool _loggedOut = false; + // Notification info saved from FCM message in SharedPreferences + String? _notifData; + String? _notifFrom; + String? _notifType; + String? _notifKeys; + String? _notifTitle; + String? _notifBody; + String? _notifWebLink; + String? _notifAndroidLink; + String? _notifWebImage; + String? _notifAndroidImage; + String? _notifWebClickAction; + bool get isLoading => _isLoading; bool get loggedOut => _loggedOut; String? get errorMessage => _errorMessage; ProfileData? get profileData => _profileData; CompanyProfileData? get companyProfileData => _companyProfileData; + // Expose notification info + String? get notifData => _notifData; + String? get notifFrom => _notifFrom; + String? get notifType => _notifType; + String? get notifKeys => _notifKeys; + String? get notifTitle => _notifTitle; + String? get notifBody => _notifBody; + String? get notifWebLink => _notifWebLink; + String? get notifAndroidLink => _notifAndroidLink; + String? get notifWebImage => _notifWebImage; + String? get notifAndroidImage => _notifAndroidImage; + String? get notifWebClickAction => _notifWebClickAction; + + String get notifSummaryText { + final parts = []; + if (_notifTitle != null && _notifTitle!.isNotEmpty) + parts.add('Title: ${_notifTitle!}'); + if (_notifBody != null && _notifBody!.isNotEmpty) + parts.add('Body: ${_notifBody!}'); + if (_notifType != null && _notifType!.isNotEmpty) + parts.add('Type: ${_notifType!}'); + if (_notifFrom != null && _notifFrom!.isNotEmpty) + parts.add('From: ${_notifFrom!}'); + if (_notifKeys != null && _notifKeys!.isNotEmpty) + parts.add('Keys: ${_notifKeys!}'); + if (_notifWebLink != null && _notifWebLink!.isNotEmpty) + parts.add('Web: ${_notifWebLink!}'); + if (_notifAndroidLink != null && _notifAndroidLink!.isNotEmpty) + parts.add('Android: ${_notifAndroidLink!}'); + if (_notifWebImage != null && _notifWebImage!.isNotEmpty) + parts.add('WebImg: ${_notifWebImage!}'); + if (_notifAndroidImage != null && _notifAndroidImage!.isNotEmpty) + parts.add('AndroidImg: ${_notifAndroidImage!}'); + if (_notifWebClickAction != null && _notifWebClickAction!.isNotEmpty) + parts.add('Click: ${_notifWebClickAction!}'); + if (_notifData != null && _notifData!.isNotEmpty) + parts.add('Data: ${_notifData!}'); + return parts.isEmpty ? '-' : parts.join(' | '); + } + Future getProfile() async { _isLoading = true; _errorMessage = null; @@ -98,4 +152,28 @@ class ProfileViewModel with ChangeNotifier { _loggedOut = false; notifyListeners(); } + + Future loadNotificationInfoFromPrefs() async { + // Do not toggle global loading; this is a lightweight local load + try { + final prefs = await SharedPreferences.getInstance(); + _notifData = prefs.getString('notif_data'); + _notifFrom = prefs.getString('notif_from'); + _notifType = prefs.getString('notif_type'); + _notifKeys = prefs.getString('notif_keys'); + _notifTitle = prefs.getString('notif_title'); + _notifBody = prefs.getString('notif_body'); + _notifWebLink = prefs.getString('notif_web_link'); + _notifAndroidLink = prefs.getString('notif_android_link'); + _notifWebImage = prefs.getString('notif_web_image'); + _notifAndroidImage = prefs.getString('notif_android_image'); + _notifWebClickAction = prefs.getString('notif_web_click_action'); + } catch (e) { + // Non-fatal; surface as errorMessage so UI can toast if listening + _errorMessage = e.toString(); + } + notifyListeners(); + _errorMessage = null; + notifyListeners(); + } } diff --git a/lib/view_models/your_tenders_view_model.dart b/lib/view_models/your_tenders_view_model.dart index b36c801..bcd0f2d 100644 --- a/lib/view_models/your_tenders_view_model.dart +++ b/lib/view_models/your_tenders_view_model.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tm_app/core/theme/colors.dart'; +import 'package:tm_app/core/utils/error_utils.dart'; import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/data/repositories/your_tenders_repository.dart'; import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; @@ -109,7 +110,7 @@ class YourTendersViewModel with ChangeNotifier { errorMessage = null; break; case Error(): - errorMessage = result.error.toString(); + errorMessage = ErrorUtils.getErrorMessage(result.error); break; } @@ -175,7 +176,7 @@ class YourTendersViewModel with ChangeNotifier { errorMessage = null; break; case Error(): - errorMessage = result.error.toString(); + errorMessage = ErrorUtils.getErrorMessage(result.error); break; } diff --git a/lib/views/profile/pages/m_profile_page.dart b/lib/views/profile/pages/m_profile_page.dart index b244b8f..d99325a 100644 --- a/lib/views/profile/pages/m_profile_page.dart +++ b/lib/views/profile/pages/m_profile_page.dart @@ -28,6 +28,10 @@ class _MobileProfilePageState extends State { super.initState(); viewModel = context.read(); viewModel.addListener(_profileListener); + // Load notification info saved in SharedPreferences + WidgetsBinding.instance.addPostFrameCallback((_) { + viewModel.loadNotificationInfoFromPrefs(); + }); } void _profileListener() { @@ -75,6 +79,30 @@ class _MobileProfilePageState extends State { child: Column( children: [ SizedBox(height: 36.0.h()), + Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + 'Last Notification', + style: TextStyle( + fontSize: 18.0.sp(), + fontWeight: FontWeight.w600, + color: AppColors.grey70, + ), + ), + ), + SizedBox(height: 8.0.h()), + Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + viewModel.notifSummaryText, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey60, + ), + ), + ), + SizedBox(height: 24.0.h()), TitleDescription( title: ProfileStrings.companyName, description: viewModel.companyProfileData!.name!,