From c02319fefa66bbea0853ac71a859a71c47257634 Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Sun, 9 Aug 2026 10:59:47 +0300 Subject: [PATCH] Completed the bugs --- assets/langs/ar-SA.json | 3 + assets/langs/en-US.json | 5 +- lib/api/api_client.dart | 25 ++ lib/classes/consts.dart | 2 + lib/config/dependency_injection.dart | 4 +- lib/generated/codegen_loader.g.dart | 6 + lib/generated/locale_keys.g.dart | 8 +- .../reserved_ads_models.dart | 2 +- lib/repositories/ads_repo.dart | 95 ++++++- lib/services/chat_navigation_service.dart | 180 +++++++++++++ lib/services/firebase_service.dart | 239 ++++++++++++++++- lib/utils/enums.dart | 1 + lib/utils/utils.dart | 48 ++++ lib/view_models/ad_view_model.dart | 68 ++++- lib/view_models/chat_view_model.dart | 240 +++++++++++++++++- .../dashboard_view_model_customer.dart | 14 +- lib/view_models/payment_view_model.dart | 104 +++++++- lib/view_models/requests_view_model.dart | 158 ++++++------ .../ads_detail_view/ads_detail_view.dart | 37 +-- .../ads_detail_view/components.dart | 21 +- .../components/ads_list_widget.dart | 13 +- .../advertisement/favourite_ads_view.dart | 31 ++- .../chat/widgets/chat_message_widget.dart | 41 ++- lib/views/common_fragments/ads_fragment.dart | 85 +++++-- .../common_fragments/requests_fragment.dart | 119 ++++++--- lib/views/requests/request_detail_page.dart | 4 +- lib/views/user/change_password_page.dart | 2 + lib/views/user/confirm_new_password_page.dart | 30 ++- lib/widgets/common_widgets/filters_list.dart | 18 +- lib/widgets/txt_field.dart | 104 ++++---- pubspec.lock | 22 +- 31 files changed, 1414 insertions(+), 315 deletions(-) create mode 100644 lib/services/chat_navigation_service.dart diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index efa3119..c3a7664 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -722,6 +722,8 @@ "customerLocation": "موقع العميل", "deliveryAvailable": "التوصيل متاح", "viewed": "تم المشاهدة", + "read": "تمت القراءة", + "sent": "تم الإرسال", "updateUserDetails": "تحديث تفاصيل المستخدم", "enterNewFirstName": "أدخل الاسم الأول", "enterNewLastName": "أدخل الاسم الأخير", @@ -834,6 +836,7 @@ "myDraftAds": "مسودتي للإعلانات", "scheduleDeletedSuccessfully": "تم حذف الجدول بنجاح", "addValidAddress": "رجى إضافة عنوان صالح", + "pleaseAddAtLeastOneImage": "يرجى إضافة صورة واحدة على الأقل لطلبك", "termsPrivacyPolicy": "الشروط وسياسة الخصوصية", "pendingApproval": "قيد الموافقة", "inReview": "قيد المراجعة", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index e288afd..1414964 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -474,7 +474,7 @@ "cancelReservation": "Cancel Reservation", "reasonForCancellation": "Reason for Cancellation", "doWantCancelReservation": "Do you want to cancel the reservation", - "yourAdVisibletoBuy.": "Your ad reservation will be cancelled and this ad will be again visible to everyone to buy", + "yourAdVisibletoBuy": "Your ad reservation will be cancelled and this ad will be again visible to everyone to buy", "completeDeal": "Complete Deal", "uploadBankReceipt": "Upload Bank Receipt", "notesForAdmin": "Notes For Admin", @@ -720,6 +720,8 @@ "customerLocation": "Customer Location", "deliveryAvailable": "Delivery Available", "viewed": "Viewed", + "read": "Read", + "sent": "Sent", "itemNoLongerAvailable": "This item is no longer available.", "reactivateAd": "Reactivate Ad", "dealOutsideApp": "The deal was done outside the app with another customer.", @@ -832,6 +834,7 @@ "myDraftAds": "My Draft Ads", "scheduleDeletedSuccessfully": "The schedule has been deleted successfully", "addValidAddress": "Please add a valid address", + "pleaseAddAtLeastOneImage": "Please add at least one image for your request", "termsPrivacyPolicy": "Terms & Privacy Policy", "pendingApproval": "Pending Approval", "inReview": "In Review", diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart index 4a640b0..5d812b8 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; @@ -70,6 +71,22 @@ abstract class ApiClient { } class ApiClientImp implements ApiClient { + /// Get current language code for Accept-Language header + /// Returns 'ar' for Arabic, 'en' for English (default) + String _getCurrentLanguage() { + try { + BuildContext? context = navigatorKey.currentContext; + if (context != null) { + String languageCode = context.locale.languageCode; + return languageCode == 'ar' ? 'ar' : 'en'; + } + } catch (e) { + log("Error getting current language: $e"); + } + // Default to English if context not available + return 'en'; + } + @override Future postJsonForObject(FactoryConstructor factoryConstructor, String url, T jsonObject, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0}) async { @@ -117,6 +134,10 @@ class ApiClientImp implements ApiClient { }) async { try { var headers0 = {}; + + // Add Accept-Language header based on current locale + headers0['Accept-Language'] = _getCurrentLanguage(); + if (token != null) { headers0['Authorization'] = 'Bearer $token'; } @@ -233,6 +254,10 @@ class ApiClientImp implements ApiClient { Future _getForResponse(String url, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0}) async { try { var headers0 = {}; + + // Add Accept-Language header based on current locale + headers0['Accept-Language'] = _getCurrentLanguage(); + if (token != null) { headers0['Authorization'] = 'Bearer $token'; } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 95d3937..01f0a36 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -139,6 +139,7 @@ class ApiConsts { static String adsPhotoOfficeAppointmentCreate = "${baseUrlServices}api/Advertisement/PhotoOfficeAppointment_Create"; static String adsMCBankAccountAdGet = "${baseUrlServices}api/Advertisement/MCBankAccountAd_Get"; static String adsReserveCreate = "${baseUrlServices}api/Advertisement/AdsReserve_Create"; + static String adsReserveCancelByOwner = "${baseUrlServices}api/Advertisement/AdsReserve_CancelByOwner"; static String reserveAdPaymentOnDealDoneCreate = "${baseUrlServices}api/Advertisement/ReserveAdPaymentOnDealDone_Create"; static String adsExtendDurationCreate = "${baseUrlServices}api/Advertisement/Ads_ExtendDuration_Create"; static String adsSingleStepDraftCreate = "${baseUrlServices}api/Advertisement/AdsSingleStepDraft_Create"; @@ -422,6 +423,7 @@ class SignalrConsts { // Requests static String receiveMessageRequestOffer = "ReceiveMessageRequestOffer"; static String sendMessageRequestOffer = "SendMessageRequestOffer"; + static String requestOfferReadUpdate = "RequestOfferReadUpdate"; // Ads static String sendMessageAds = "SendMessageAds"; diff --git a/lib/config/dependency_injection.dart b/lib/config/dependency_injection.dart index 970a88a..2cfb009 100644 --- a/lib/config/dependency_injection.dart +++ b/lib/config/dependency_injection.dart @@ -20,7 +20,8 @@ import 'package:mc_common_app/services/firebase_service.dart'; import 'package:mc_common_app/services/payments_service.dart'; import 'package:mc_common_app/utils/location/Location.dart'; -import '../repositories/request_repo.dart'; +import 'package:mc_common_app/repositories/request_repo.dart'; +import 'package:mc_common_app/services/chat_navigation_service.dart'; Injector injector = Injector.appInstance; @@ -31,6 +32,7 @@ class AppDependencies { injector.registerSingleton(() => ApiClientImp()); injector.registerSingleton(() => CommonServicesImp()); injector.registerSingleton(() => PaymentServiceImp()); + injector.registerSingleton(() => ChatNavigationServiceImp()); injector.registerSingleton(() => LocationService()); injector.registerSingleton(() => FirebaseMessagingServiceImp( firebaseMessaging: FirebaseMessaging.instance, diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 1a2c357..93492d6 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -738,6 +738,8 @@ class CodegenLoader extends AssetLoader{ "customerLocation": "موقع العميل", "deliveryAvailable": "التوصيل متاح", "viewed": "تم المشاهدة", + "read": "تمت القراءة", + "sent": "تم الإرسال", "updateUserDetails": "تحديث تفاصيل المستخدم", "enterNewFirstName": "أدخل الاسم الأول", "enterNewLastName": "أدخل الاسم الأخير", @@ -850,6 +852,7 @@ class CodegenLoader extends AssetLoader{ "myDraftAds": "مسودتي للإعلانات", "scheduleDeletedSuccessfully": "تم حذف الجدول بنجاح", "addValidAddress": "رجى إضافة عنوان صالح", + "pleaseAddAtLeastOneImage": "يرجى إضافة صورة واحدة على الأقل لطلبك", "termsPrivacyPolicy": "الشروط وسياسة الخصوصية", "pendingApproval": "قيد الموافقة", "inReview": "قيد المراجعة", @@ -1642,6 +1645,8 @@ static const Map _en_US = { "customerLocation": "Customer Location", "deliveryAvailable": "Delivery Available", "viewed": "Viewed", + "read": "Read", + "sent": "Sent", "itemNoLongerAvailable": "This item is no longer available.", "reactivateAd": "Reactivate Ad", "dealOutsideApp": "The deal was done outside the app with another customer.", @@ -1754,6 +1759,7 @@ static const Map _en_US = { "myDraftAds": "My Draft Ads", "scheduleDeletedSuccessfully": "The schedule has been deleted successfully", "addValidAddress": "Please add a valid address", + "pleaseAddAtLeastOneImage": "Please add at least one image for your request", "termsPrivacyPolicy": "Terms & Privacy Policy", "pendingApproval": "Pending Approval", "inReview": "In Review", diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index fa241f8..678714e 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const firstTimeLogIn = 'firstTimeLogIn'; static const signUp = 'signUp'; static const changeMobile = 'changeMobile'; @@ -49,7 +49,7 @@ abstract class LocaleKeys { static const emailChangedSuccessfully = 'emailChangedSuccessfully'; static const passwordIsUpdated = 'passwordIsUpdated'; static const passwordShouldContains = 'passwordShouldContains'; - static const successfullyRegistered = ' successfullyRegistered'; + static const successfullyRegistered = ' successfullyRegistered'; static const pleaseEnterSamePassword = 'pleaseEnterSamePassword'; static const firstNameMandatory = 'firstNameMandatory'; static const surnameNameMandatory = 'surnameNameMandatory'; @@ -703,6 +703,8 @@ abstract class LocaleKeys { static const customerLocation = 'customerLocation'; static const deliveryAvailable = 'deliveryAvailable'; static const viewed = 'viewed'; + static const read = 'read'; + static const sent = 'sent'; static const updateUserDetails = 'updateUserDetails'; static const enterNewFirstName = 'enterNewFirstName'; static const enterNewLastName = 'enterNewLastName'; @@ -815,6 +817,7 @@ abstract class LocaleKeys { static const myDraftAds = 'myDraftAds'; static const scheduleDeletedSuccessfully = 'scheduleDeletedSuccessfully'; static const addValidAddress = 'addValidAddress'; + static const pleaseAddAtLeastOneImage = 'pleaseAddAtLeastOneImage'; static const termsPrivacyPolicy = 'termsPrivacyPolicy'; static const pendingApproval = 'pendingApproval'; static const inReview = 'inReview'; @@ -884,5 +887,4 @@ abstract class LocaleKeys { static const iosApp = 'iosApp'; static const pleaseWait = 'pleaseWait'; static const numberOfShipping = 'numberOfShipping'; - } diff --git a/lib/models/advertisment_models/reserved_ads_models.dart b/lib/models/advertisment_models/reserved_ads_models.dart index c6f7441..4f50c9f 100644 --- a/lib/models/advertisment_models/reserved_ads_models.dart +++ b/lib/models/advertisment_models/reserved_ads_models.dart @@ -26,7 +26,7 @@ class MyReservedAdsRespModel { adsID = json['adsID']; customerID = json['customerID']; paymentStatus = json['paymentStatus']; - paymentStatus = json['adsReserveStatus']; + adsReserveStatus = json['adsReserveStatus']; reservationTimeID = json['reservationTimeID']; reservationBasePrice = json['reservationBasePrice']; refundAmount = json['refundAmount']; diff --git a/lib/repositories/ads_repo.dart b/lib/repositories/ads_repo.dart index 4d0a8ea..50448e5 100644 --- a/lib/repositories/ads_repo.dart +++ b/lib/repositories/ads_repo.dart @@ -7,6 +7,7 @@ import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/config/dependency_injection.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; +import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart'; import 'package:mc_common_app/models/advertisment_models/ads_bank_details_model.dart'; import 'package:mc_common_app/models/advertisment_models/ads_duration_model.dart'; @@ -57,7 +58,11 @@ abstract class AdsRepo { Future> getMyAds(); - Future> getFavouriteAds(); + Future> getFavouriteAds({ + AdPostStatus? adPostStatus, + CreatedByRoleEnum? createdByRoleEnum, + int? vehicleBrandId, + }); Future toggleAdFavorite({required int adsID, required bool isFavorite}); @@ -455,16 +460,17 @@ class AdsRepoImp implements AdsRepo { ); if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) { - Utils.showToast(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr()); - return []; + throw Exception(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr()); } + List reservedAds = List.generate(adsGenericModel.data.length, (index) => MyReservedAdsRespModel.fromJson(adsGenericModel.data[index])); - List selectedIdsString = reservedAds.map((component) => component.adsID.toString()).toList(); - if (selectedIdsString.isEmpty) { - return []; + if (reservedAds.isEmpty) { + return []; // No reserved ads - this is a valid state } + List selectedIdsString = reservedAds.map((component) => component.adsID.toString()).toList(); + return await getAdsPerSpecificIds(ids: selectedIdsString, reservedAds: reservedAds); } @@ -472,7 +478,7 @@ class AdsRepoImp implements AdsRepo { var params = { "AdsIDs": ids, "isActive": "true", - "PageSize": "30", + "PageSize": ids.length.toString(), // Request all reserved ads in one call }; GenericRespModel adsGenericModel = await apiClient.getJsonForObject( token: appState.getUser.data!.accessToken, @@ -481,16 +487,49 @@ class AdsRepoImp implements AdsRepo { ApiConsts.vehicleAdsGet, ); if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) { - Utils.showToast(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr()); - return []; + throw Exception(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr()); } + List vehicleAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], true, adsGenericModel.totalItemsCount ?? 1)); - for (int i = 0; i < vehicleAdsDetails.length; i++) { - vehicleAdsDetails[i].adReserveStatus = (reservedAds[i].adsReserveStatus ?? 0).toAdRserveStatusEnum(); + // Create a map for O(1) lookup: adsID -> reserve status + Map statusMap = {}; + for (var reserved in reservedAds) { + log("adsReserveStatus: ${reserved.adsReserveStatus}"); + + statusMap[reserved.adsID ?? 0] = (reserved.adsReserveStatus ?? 0).toAdRserveStatusEnum(); + } + + log("📋 Status Map created: ${statusMap.length} entries"); + statusMap.forEach((key, value) { + log(" Ad ID: $key -> Status: $value"); + }); + + // Assign reserve status by matching ad ID (not by index!) + log("📦 Processing ${vehicleAdsDetails.length} ads from second API"); + for (var ad in vehicleAdsDetails) { + if (statusMap.containsKey(ad.id)) { + ad.adReserveStatus = statusMap[ad.id]!; + // Backend will set adPostStatus=reserved in the response, so no need to set it manually + log(" ✅ Ad ${ad.id}: Assigned adReserveStatus ${ad.adReserveStatus}"); + } else { + // Ad exists but no reserve status found - shouldn't happen but handle it + ad.adReserveStatus = AdReserveStatus.defaultStatus; + log(" ⚠️ Ad ${ad.id}: No status found, set to defaultStatus"); + } + } + + // Show all ads with ANY reservation history (exclude only defaultStatus/none) + // This includes: reserved (1), cancelledByOwner (2), cancelledByAdmin (3), timeOver (4), dealDone (5), fullPaymentVerified (6) + int beforeFilter = vehicleAdsDetails.length; + vehicleAdsDetails.removeWhere((element) => element.adReserveStatus == AdReserveStatus.defaultStatus); + int afterFilter = vehicleAdsDetails.length; + + log("🎯 Filter Result: ${afterFilter} ads remaining (removed ${beforeFilter - afterFilter})"); + for (var ad in vehicleAdsDetails) { + log(" ✅ Keeping Ad ${ad.id} with status ${ad.adReserveStatus}"); } - vehicleAdsDetails.removeWhere((element) => element.adReserveStatus != AdReserveStatus.reserved); return vehicleAdsDetails; } @@ -515,16 +554,44 @@ class AdsRepoImp implements AdsRepo { } @override - Future> getFavouriteAds() async { + Future> getFavouriteAds({ + AdPostStatus? adPostStatus, + CreatedByRoleEnum? createdByRoleEnum, + int? vehicleBrandId, + }) async { + // Build filter parameters similar to getAllAds + Map params = { + "PageSize": "30", + }; + + // Add status filter if provided + if (adPostStatus != null) { + params["AdsStatuses"] = ["${adPostStatus.getIdFromAdPostStatusEnum()}"]; + } + + // Add created by role filter if provided + if (createdByRoleEnum != null && createdByRoleEnum != CreatedByRoleEnum.allAds) { + params["CreatedByRoles"] = ["${createdByRoleEnum.getIdFromCreatedByRoleEnum()}"]; + } + + // Add vehicle brand filter if provided + if (vehicleBrandId != null && vehicleBrandId != 0) { + params["VehicleBrandIDs"] = ["$vehicleBrandId"]; + } + GenericRespModel adsGenericModel = await apiClient.getJsonForObject( token: appState.getUser.data!.accessToken, (json) => GenericRespModel.fromJson(json), ApiConsts.favouriteAdsGet, + queryParameters: params, ); + if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) { return []; } + List favouriteAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], false, adsGenericModel.totalItemsCount ?? 1)); + return favouriteAdsDetails; } @@ -638,7 +705,7 @@ class AdsRepoImp implements AdsRepo { String token = appState.getUser.data!.accessToken ?? ""; GenericRespModel adsGenericModel = await apiClient.postJsonForObject( (json) => GenericRespModel.fromJson(json), - ApiConsts.deleteAd, + ApiConsts.adsReserveCancelByOwner, postParams, token: token, ); diff --git a/lib/services/chat_navigation_service.dart b/lib/services/chat_navigation_service.dart new file mode 100644 index 0000000..69a80b9 --- /dev/null +++ b/lib/services/chat_navigation_service.dart @@ -0,0 +1,180 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/app_state.dart'; +import 'package:mc_common_app/config/routes.dart'; +import 'package:mc_common_app/models/chat_models/buyers_chat_for_ads_model.dart'; +import 'package:mc_common_app/models/requests_models/request_model.dart'; +import 'package:mc_common_app/utils/enums.dart'; +import 'package:mc_common_app/utils/navigator.dart'; + +/// Service for handling navigation to different chat types in the app +/// Provides centralized navigation logic for Request, Advertisement, and General chats +abstract class ChatNavigationService { + /// Navigate to Request Offer Chat screen + Future navigateToRequestOfferChat({ + required BuildContext context, + required int requestId, + required String receiverId, + required String senderId, + required int requestIndex, + required int providerIndex, + required RequestModel requestModel, + }); + + /// Navigate to Advertisement Chat screen + Future navigateToAdChat({ + required BuildContext context, + required int adsId, + required String receiverUserId, + }); + + /// Navigate to General Chat screen + Future navigateToGeneralChat({ + required BuildContext context, + required String receiverUserId, + Map? additionalData, + }); + + /// Navigate to Chat List screen based on chat type + Future navigateToChatList({ + required BuildContext context, + required ChatTypeEnum chatType, + Map? arguments, + }); +} + +class ChatNavigationServiceImp implements ChatNavigationService { + @override + Future navigateToRequestOfferChat({ + required BuildContext context, + required int requestId, + required String receiverId, + required String senderId, + required int requestIndex, + required int providerIndex, + required RequestModel requestModel, + }) async { + try { + log("Navigating to Request Offer Chat: requestId=$requestId"); + + ChatViewArgumentsForRequest args = ChatViewArgumentsForRequest( + chatTypeEnum: ChatTypeEnum.requestOffer, + requestId: requestId, + receiverId: receiverId, + senderId: senderId, + requestIndex: requestIndex, + providerIndex: providerIndex, + requestModel: requestModel, + ); + + ChatViewArguments chatViewArguments = ChatViewArguments( + chatTypeEnum: ChatTypeEnum.requestOffer, + chatViewArgumentsForRequest: args, + ); + + navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments); + } catch (e) { + log("Error navigating to request offer chat: $e"); + } + } + + @override + Future navigateToAdChat({ + required BuildContext context, + required int adsId, + required String receiverUserId, + }) async { + try { + log("Navigating to Ad Chat: adsId=$adsId, receiverId=$receiverUserId"); + + ChatViewArgumentsForAd args = ChatViewArgumentsForAd( + adsID: adsId, + receiverUserID: receiverUserId, + ); + + ChatViewArguments chatViewArguments = ChatViewArguments( + chatTypeEnum: ChatTypeEnum.ads, + chatViewArgumentsForAd: args, + ); + + navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments); + } catch (e) { + log("Error navigating to ad chat: $e"); + } + } + + @override + Future navigateToGeneralChat({ + required BuildContext context, + required String receiverUserId, + Map? additionalData, + }) async { + try { + log("Navigating to General Chat: receiverId=$receiverUserId"); + + // General chat implementation + // You can extend this based on your general chat requirements + ChatViewArguments chatViewArguments = ChatViewArguments( + chatTypeEnum: ChatTypeEnum.general, + ); + + navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments); + } catch (e) { + log("Error navigating to general chat: $e"); + } + } + + @override + Future navigateToChatList({ + required BuildContext context, + required ChatTypeEnum chatType, + Map? arguments, + }) async { + try { + log("Navigating to Chat List: type=$chatType"); + + switch (chatType) { + case ChatTypeEnum.requestOffer: + // Navigate to Providers Chat List (for customer side) + // or Requests list (for provider side) + if (AppState().currentAppType == AppType.customer) { + navigateWithName(context, AppRoutes.providersChatListPage); + } else { + // For provider, navigate to requests page + navigateWithName(context, AppRoutes.dashboard); + } + break; + + case ChatTypeEnum.ads: + // Navigate to Ads Buyers Chat List + if (arguments != null && arguments['buyers'] != null) { + navigateWithName( + context, + AppRoutes.adsBuyerChatsListView, + arguments: arguments['buyers'] as List, + ); + } else { + // Navigate to ads list if buyers not provided + navigateWithName(context, AppRoutes.dashboard); + } + break; + + case ChatTypeEnum.general: + // Navigate to General Chats List + if (arguments != null && arguments['requestId'] != null) { + navigateWithName( + context, + AppRoutes.generalChatsListForProvider, + arguments: arguments['requestId'] as int, + ); + } else { + navigateWithName(context, AppRoutes.dashboard); + } + break; + } + } catch (e) { + log("Error navigating to chat list: $e"); + } + } +} diff --git a/lib/services/firebase_service.dart b/lib/services/firebase_service.dart index 5e70d57..63fb768 100644 --- a/lib/services/firebase_service.dart +++ b/lib/services/firebase_service.dart @@ -1,13 +1,18 @@ +import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:mc_common_app/classes/app_state.dart'; +import 'package:mc_common_app/config/dependency_injection.dart'; +import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/main.dart'; +import 'package:mc_common_app/services/chat_navigation_service.dart'; +import 'package:mc_common_app/utils/navigator.dart'; import 'package:permission_handler/permission_handler.dart'; abstract class FirebaseMessagingService { @@ -63,17 +68,39 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { defaultPresentSound: true, ); final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsDarwin); - await flutterLocalNotifications.initialize(initializationSettings, onDidReceiveNotificationResponse: (notificationResponse) {}, ); + await flutterLocalNotifications.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (NotificationResponse response) async { + // Handle notification tap when app is in foreground/background + log("========== Notification Tapped =========="); + log("Notification ID: ${response.id}"); + log("Action ID: ${response.actionId}"); + log("Input: ${response.input}"); + log("Payload: ${response.payload}"); + log("========================================="); + + if (response.payload != null && response.payload!.isNotEmpty) { + try { + Map data = jsonDecode(response.payload!); + log("Parsed payload data: $data"); + _handleNotificationNavigation(data); + } catch (e) { + logger.e("Error parsing notification payload: $e"); + } + } + }, + ); RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) _handleNotification(initialMessage); FirebaseMessaging.onMessage.listen((RemoteMessage message) { - if (message.notification != null) _handleNotification(message); + // Foreground notification - just display, don't navigate automatically + if (message.notification != null) _handleForegroundNotification(message); }); - FirebaseMessaging.onMessageOpenedApp.listen(_handleNotification); + FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationOpenedApp); FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); @@ -86,14 +113,208 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { logger.i(body); } + void _handleForegroundNotification(RemoteMessage message) async { + log("========== Foreground Notification Received =========="); + log("Title: ${message.notification!.title}"); + log("Body: ${message.notification!.body}"); + log("Data payload: ${message.data}"); + log("Message ID: ${message.messageId}"); + log("Sent time: ${message.sentTime}"); + log("======================================================"); + + // Just display notification - DON'T navigate automatically + // Navigation happens only when user taps the notification + displayNotification( + title: message.notification!.title ?? '', + body: message.notification!.body ?? '', + payload: jsonEncode(message.data), + ); + } + + void _handleNotificationOpenedApp(RemoteMessage message) async { + log("========== Notification Opened App =========="); + log("Title: ${message.notification!.title}"); + log("Body: ${message.notification!.body}"); + log("Data payload: ${message.data}"); + log("Message ID: ${message.messageId}"); + log("Sent time: ${message.sentTime}"); + log("============================================="); + + // User tapped notification - navigate to chat + _handleNotificationNavigation(message.data); + } + void _handleNotification(RemoteMessage message) async { - logger.i(message.notification!.title); - logger.i(message.notification!.body); + // This is called when app is terminated and opened by notification + log("========== App Terminated Notification =========="); + log("Title: ${message.notification!.title}"); + log("Body: ${message.notification!.body}"); + log("Data payload: ${message.data}"); + log("Message ID: ${message.messageId}"); + log("Sent time: ${message.sentTime}"); + log("================================================="); + + // User opened app from notification - navigate to chat + _handleNotificationNavigation(message.data); + } + + void _handleNotificationNavigation(Map data) async { + log("========== Handling Notification Navigation =========="); + log("Full data received: $data"); + + if (data.isEmpty) { + log("Data is empty, skipping navigation"); + log("======================================================"); + return; + } + + // Backend sends NotiType as string: "1" = Request, "2" = Ads, "3" = General + String? notiTypeStr = data['NotiType']; + int? notiType = int.tryParse(notiTypeStr ?? ''); + + log("NotiType from backend: $notiTypeStr (parsed: $notiType)"); + + if (notiType == null || notiType < 1 || notiType > 3) { + log("Invalid or missing NotiType, skipping navigation"); + log("======================================================"); + return; + } + + // Map NotiType to chat type + String chatType; + switch (notiType) { + case 1: + chatType = 'request_offer'; + break; + case 2: + chatType = 'ad_chat'; + break; + case 3: + chatType = 'general_chat'; + break; + default: + log("Unknown NotiType: $notiType"); + log("======================================================"); + return; + } + + log("Mapped chat type: $chatType"); + + // Wait a bit for context to be available + await Future.delayed(Duration(milliseconds: 300)); + + BuildContext? context = navigatorKey.currentContext; + if (context == null) { + logger.e("Context not available for navigation"); + log("======================================================"); + return; + } + + // Log all available data fields to understand backend format + log("Available data fields:"); + data.forEach((key, value) { + log(" $key: $value"); + }); + + switch (chatType) { + case 'request_offer': + log("Navigating to Request Offer Chat..."); + await _navigateToRequestOfferChat(context, data); + break; + case 'ad_chat': + log("Navigating to Advertisement Chat..."); + await _navigateToAdChat(context, data); + break; + case 'general': + log("Navigating to General Chat..."); + await _navigateToGeneralChat(context, data); + break; + default: + logger.e("Unknown chat type: $chatType"); + } + + log("======================================================"); + } + + Future _navigateToRequestOfferChat( + BuildContext context, + Map data, + ) async { + try { + // Try both backend format and our format + int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0; + + if (requestId == 0) { + log("❌ Invalid or missing RequestID in notification data"); + return; + } + + log("✅ Navigating to providers chat list for request: $requestId"); + navigateWithName(context, AppRoutes.providersChatListPage); + + // TODO: If backend sends all required data (ProviderId, CustomerId), + // we can navigate directly to the chat screen + } catch (e) { + logger.e("Error navigating to request chat: $e"); + } + } + + Future _navigateToAdChat( + BuildContext context, + Map data, + ) async { + try { + // Try both backend format and our format + int adId = int.tryParse(data['AdID']?.toString() ?? data['ad_id']?.toString() ?? '0') ?? 0; + String? receiverUserId = data['ReceiverUserID'] ?? data['receiver_id']; - displayNotification(title: message.notification!.title ?? '', body: message.notification!.body ?? ''); + if (adId == 0 || receiverUserId == null) { + log("❌ Invalid AdID or ReceiverUserID in notification data"); + log(" AdID: ${data['AdID']}, ReceiverUserID: ${data['ReceiverUserID']}"); + return; + } + + log("✅ Navigating to ad chat: adId=$adId"); + final chatNavService = injector.get(); + await chatNavService.navigateToAdChat( + context: context, + adsId: adId, + receiverUserId: receiverUserId, + ); + } catch (e) { + logger.e("Error navigating to ad chat: $e"); + } + } + + Future _navigateToGeneralChat( + BuildContext context, + Map data, + ) async { + try { + // Try both backend format and our format + int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0; + + if (requestId == 0) { + log("❌ Invalid or missing RequestID in notification data"); + return; + } + + log("✅ Navigating to general chat list for request: $requestId"); + navigateWithName( + context, + AppRoutes.generalChatsListForProvider, + arguments: requestId, + ); + } catch (e) { + logger.e("Error navigating to general chat: $e"); + } } - Future displayNotification({required String title, required String body}) async { + Future displayNotification({ + required String title, + required String body, + String? payload, + }) async { const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( "0", 'mowater', @@ -105,7 +326,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(threadIdentifier: 'thread_id'); const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iOSPlatformChannelSpecifics); - await flutterLocalNotifications.show(0, title, body, notificationDetails, payload: 'item x'); + await flutterLocalNotifications.show(0, title, body, notificationDetails, payload: payload ?? 'item x'); } } diff --git a/lib/utils/enums.dart b/lib/utils/enums.dart index 28d93c7..f386e12 100644 --- a/lib/utils/enums.dart +++ b/lib/utils/enums.dart @@ -26,6 +26,7 @@ class AppEnums { enum DashboardRouteEnum { fromAdsPayment, fromAdsSubmit, + fromReserveAdsPayment, none, } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index ba15a61..0ea0b16 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -263,6 +263,54 @@ class Utils { } } + static Color getChipColorByReserveStatus(AdReserveStatus reserveStatus) { + switch (reserveStatus) { + case AdReserveStatus.reserved: + return MyColors.primaryColor; // Active reservation - blue/primary + + case AdReserveStatus.timeOver: + return MyColors.adCancelledStatusColor; // Expired - red/orange + + case AdReserveStatus.cancelledByOwner: + case AdReserveStatus.cancelledByAdmin: + return MyColors.adCancelledStatusColor; // Cancelled - red + + case AdReserveStatus.dealDone: + case AdReserveStatus.fullPaymentVerified: + return MyColors.greenColor; // Completed successfully - green + + case AdReserveStatus.defaultStatus: + default: + return MyColors.textColor; // Default/unknown - gray + } + } + + static String getReserveStatusLabel(AdReserveStatus reserveStatus) { + switch (reserveStatus) { + case AdReserveStatus.reserved: + return LocaleKeys.reserved.tr(); + + case AdReserveStatus.timeOver: + return LocaleKeys.reservationTimeOver.tr(); + + case AdReserveStatus.cancelledByOwner: + return LocaleKeys.cancelledByOwner.tr(); + + case AdReserveStatus.cancelledByAdmin: + return LocaleKeys.cancelledByAdmin.tr(); + + case AdReserveStatus.dealDone: + return LocaleKeys.dealCompleted.tr(); + + case AdReserveStatus.fullPaymentVerified: + return LocaleKeys.paymentVerified.tr(); + + case AdReserveStatus.defaultStatus: + default: + return ""; + } + } + static Color getChipColorByRequestStatus(RequestStatusEnum requestStatus) { switch (requestStatus) { case RequestStatusEnum.pending: diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index aa1c82f..77e148f 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -295,6 +295,16 @@ class AdVM extends BaseVM { return await adsRepo.getAllAds(isMyAds: isMyAds, adPostStatus: adPostStatus, createdByRoleEnum: createdByRoleEnum, vehicleBrandId: vehicleBrandId); } + /// Check if the "Reserved" filter is currently selected in My Ads + bool get isReservedFilterSelected { + if (myAdsFilterOptions.isEmpty) return false; + var selectedFilter = myAdsFilterOptions.firstWhere( + (element) => element.isSelected, + orElse: () => FilterListModel(id: 0, title: "All Ads", isSelected: true), + ); + return selectedFilter.id.toAdPostEnum() == AdPostStatus.reserved; + } + applyFilterOnMyAds({required AdPostStatus adPostStatusEnum}) async { pageIndexForMyAds = 1; hasMoreDataForMyAds = true; @@ -310,9 +320,15 @@ class AdVM extends BaseVM { getMyAds(); return; } + // this means if the filter is reserved ads if (adPostStatusEnum == AdPostStatus.reserved) { - log("reserved: ${myReservedAds.length}"); + // Fetch reserved ads when filter is tapped (lazy loading for consistency) + setState(ViewState.busy); + await getMyReservedAds(); + setState(ViewState.idle); + + log("📊 Reserved ads count: ${myReservedAds.length}"); myAdsFilteredList = myReservedAds; for (var ad in myAdsFilteredList) { ad.isReservedByMe = true; @@ -320,6 +336,7 @@ class AdVM extends BaseVM { notifyListeners(); return; } + setState(ViewState.busy); myAdsFilteredList = await getAdsByFilter(adPostStatus: adPostStatusEnum, isMyAds: true); setState(ViewState.idle); @@ -385,10 +402,18 @@ class AdVM extends BaseVM { } } - Future getFavouriteAds() async { + Future getFavouriteAds({ + AdPostStatus? adPostStatus, + CreatedByRoleEnum? createdByRoleEnum, + int? vehicleBrandId, + }) async { try { setState(ViewState.busy); - favouriteAds = await adsRepo.getFavouriteAds(); + favouriteAds = await adsRepo.getFavouriteAds( + adPostStatus: adPostStatus, + createdByRoleEnum: createdByRoleEnum, + vehicleBrandId: vehicleBrandId, + ); notifyListeners(); setState(ViewState.idle); } catch (e) { @@ -433,7 +458,8 @@ class AdVM extends BaseVM { myAdsFilteredList = myAds; final myActiveAds = myAds.where((element) => element.adPostStatus == AdPostStatus.active).toList(); myActiveAdsForHome = myActiveAds.length >= 3 ? myActiveAds.take(3).toList() : myActiveAds; - await getMyReservedAds(); + // Removed: await getMyReservedAds(); + // Now lazy loads when "Reserved" filter is tapped (see applyFilterOnMyAds) notifyListeners(); setState(ViewState.idle); } @@ -442,12 +468,19 @@ class AdVM extends BaseVM { try { setState(ViewState.busy); myReservedAds = await adsRepo.getMyReservedAds(); - isFetchingLists = false; setState(ViewState.idle); + notifyListeners(); // Notify UI of data changes return myReservedAds; } catch (e) { - logger.e(e.toString()); + logger.e("Error fetching reserved ads: ${e.toString()}"); setState(ViewState.idle); + myReservedAds = []; // Explicitly set to empty list on error + notifyListeners(); // Notify UI of error state + + // Show user-friendly error message + String errorMessage = e.toString().replaceAll('Exception: ', ''); + Utils.showToast(errorMessage); + return []; } } @@ -538,6 +571,11 @@ class AdVM extends BaseVM { Utils.showToast(respModel.message ?? LocaleKeys.somethingWrong.tr()); return; } + + // Reset the cancel reason text field + reservationCancelReason = ""; + reservationCancelError = ""; + Utils.hideLoading(context); Utils.showToast(LocaleKeys.yourReservationCancelled.tr()); updateIsExploreAds(false); @@ -2733,15 +2771,23 @@ class AdVM extends BaseVM { bool isArabic = AppState().isArabic(context); updateSelectionVehicleModelId(SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? "")); updateSelectionVehicleModelYearId(SelectionModel(selectedId: previousAdDetails!.vehicle!.modelyear!.id!, selectedOption: previousAdDetails!.vehicle!.modelyear!.label ?? "")); - updateSelectionVehicleColorId(SelectionModel(selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? "")); - updateSelectionVehicleConditionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.condition!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() :previousAdDetails!.vehicle!.condition!.label ?? "")); - updateSelectionVehicleCategoryId(SelectionModel(selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() :previousAdDetails!.vehicle!.category!.label ?? "")); + updateSelectionVehicleColorId(SelectionModel( + selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? "")); + updateSelectionVehicleConditionId(SelectionModel( + selectedId: previousAdDetails!.vehicle!.condition!.id!, + selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() : previousAdDetails!.vehicle!.condition!.label ?? "")); + updateSelectionVehicleCategoryId(SelectionModel( + selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() : previousAdDetails!.vehicle!.category!.label ?? "")); updateSelectionVehicleMileageId(SelectionModel( selectedId: previousAdDetails!.vehicle!.mileage!.id!, selectedOption: "${previousAdDetails!.vehicle!.mileage!.mileageStart} - ${previousAdDetails!.vehicle!.mileage!.mileageEnd}", )); - updateSelectionVehicleTransmissionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.transmission!.id!, selectedOption:isArabic ? previousAdDetails!.vehicle!.transmission!.labelN.toString() : previousAdDetails!.vehicle!.transmission!.label ?? "")); - updateSelectionVehicleSellerTypeId(SelectionModel(selectedId: previousAdDetails!.vehicle!.sellertype!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() :previousAdDetails!.vehicle!.sellertype!.label ?? "")); + updateSelectionVehicleTransmissionId(SelectionModel( + selectedId: previousAdDetails!.vehicle!.transmission!.id!, + selectedOption: isArabic ? previousAdDetails!.vehicle!.transmission!.labelN.toString() : previousAdDetails!.vehicle!.transmission!.label ?? "")); + updateSelectionVehicleSellerTypeId(SelectionModel( + selectedId: previousAdDetails!.vehicle!.sellertype!.id!, + selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() : previousAdDetails!.vehicle!.sellertype!.label ?? "")); int indexCountry = vehicleCountries.indexWhere((element) => element.id == previousAdDetails!.vehicle!.countryID); if (indexCountry != -1) { updateSelectionVehicleCountryId(SelectionModel(selectedId: vehicleCountries[indexCountry].id!, selectedOption: vehicleCountries[indexCountry].countryName ?? "")); diff --git a/lib/view_models/chat_view_model.dart b/lib/view_models/chat_view_model.dart index 24b2206..a672365 100644 --- a/lib/view_models/chat_view_model.dart +++ b/lib/view_models/chat_view_model.dart @@ -225,6 +225,28 @@ class ChatVM extends BaseVM { int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == currentMessage.senderUserID); if (providerIndex != -1) { + // Check if message already exists to prevent duplicates + bool messageExists = false; + + if (currentMessage.id != null) { + // For messages with ID, use ID for duplicate detection + messageExists = serviceProviderOffersList[providerIndex].chatMessages!.any((msg) => msg.id == currentMessage.id); + } else { + // For messages without ID (locally created), use content-based duplicate detection + // Check if same sender, same text, same reqOfferID within short time window + messageExists = serviceProviderOffersList[providerIndex].chatMessages!.any((msg) { + return msg.senderUserID == currentMessage.senderUserID && + msg.chatText == currentMessage.chatText && + msg.reqOfferID == currentMessage.reqOfferID && + msg.messageType == currentMessage.messageType; + }); + } + + if (messageExists) { + log("Message already exists (id: ${currentMessage.id}), skipping duplicate"); + continue; + } + if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) { for (var chatMessage in serviceProviderOffersList[providerIndex].chatMessages!) { if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) { @@ -248,6 +270,28 @@ class ChatVM extends BaseVM { } log("requestIndex: $requestIndex"); if (requestIndex != -1) { + // Check if message already exists to prevent duplicates + bool messageExists = false; + + if (currentMessage.id != null) { + // For messages with ID, use ID for duplicate detection + messageExists = requestsVM.myFilteredRequests[requestIndex].chatMessages.any((msg) => msg.id == currentMessage.id); + } else { + // For messages without ID (locally created), use content-based duplicate detection + // Check if same sender, same text, same reqOfferID within short time window + messageExists = requestsVM.myFilteredRequests[requestIndex].chatMessages.any((msg) { + return msg.senderUserID == currentMessage.senderUserID && + msg.chatText == currentMessage.chatText && + msg.reqOfferID == currentMessage.reqOfferID && + msg.messageType == currentMessage.messageType; + }); + } + + if (messageExists) { + log("Message already exists (id: ${currentMessage.id}), skipping duplicate"); + continue; + } + if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) { for (var chatMessage in requestsVM.myFilteredRequests[requestIndex].chatMessages) { if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) { @@ -256,6 +300,13 @@ class ChatVM extends BaseVM { chatMessage.reqOffer!.requestOfferStatusEnum = currentMessage.reqOffer!.requestOfferStatusEnum; } } + + // If this offer was accepted, update the request status to inProgress + // This ensures all other offers show "offerNoLongerAvailable" on provider side + if (currentMessage.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.accepted) { + log("Offer accepted, updating request status to inProgress"); + requestsVM.myFilteredRequests[requestIndex].requestStatus = RequestStatusEnum.inProgress; + } } requestsVM.addChatMessagesInRequestsModel(msg: currentMessage, index: requestIndex); scrollChatDown(); @@ -271,6 +322,13 @@ class ChatVM extends BaseVM { return; } + // Clear existing listener first to prevent duplicates + try { + hubConnection!.off(SignalrConsts.receiveMessageRequestOffer); + } catch (e) { + // Ignore if no listener exists + } + hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List? arguments) { logger.i(arguments); @@ -328,6 +386,7 @@ class ChatVM extends BaseVM { hubConnection!.off(SignalrConsts.receiveMessageRequestOffer); hubConnection!.off(SignalrConsts.receiveMessageAds); hubConnection!.off(SignalrConsts.receiveMessageGeneral); + hubConnection!.off(SignalrConsts.requestOfferReadUpdate); _isSubscribed = false; logger.i("Cleared all SignalR listeners"); } catch (e) { @@ -371,8 +430,10 @@ class ChatVM extends BaseVM { _subscribeToReceiveRequestOfferMessages(); _subscribeToReceiveAdMessages(); _subscribeToReceiveGeneralMessages(); + _subscribeToRequestOfferReadUpdate(); _isSubscribed = true; - logger.i("All listeners subscribed successfully"); + log("✅ All SignalR listeners subscribed | Hub: ${hubConnection!.connectionId}"); + log("📍 Events: ReceiveMessageRequestOffer, ReceiveMessageAds, ReceiveMessageGeneral, RequestOfferReadUpdate"); } else { logger.w("ViewModels not injected yet. Listeners will be subscribed after injection."); } @@ -392,13 +453,14 @@ class ChatVM extends BaseVM { hubConnection!.onreconnected((connectionId) async { logger.i("onReconnected: ${connectionId.toString()}"); _retryCount = 0; // Reset on successful reconnect - + // ✅ CRITICAL FIX: Re-subscribe listeners after automatic reconnect if (_requestsVM != null && _adVM != null) { _clearAllListeners(); _subscribeToReceiveRequestOfferMessages(); _subscribeToReceiveAdMessages(); _subscribeToReceiveGeneralMessages(); + _subscribeToRequestOfferReadUpdate(); _isSubscribed = true; logger.i("✅ Listeners re-subscribed after automatic reconnection"); } else { @@ -643,7 +705,7 @@ class ChatVM extends BaseVM { } } } - + isSendingMessage = false; notifyListeners(); return true; @@ -655,7 +717,7 @@ class ChatVM extends BaseVM { return false; } } - + isSendingMessage = false; notifyListeners(); return false; @@ -873,6 +935,13 @@ class ChatVM extends BaseVM { return; } + // Clear existing listener first to prevent duplicates + try { + hubConnection!.off(SignalrConsts.receiveMessageAds); + } catch (e) { + // Ignore if no listener exists + } + hubConnection!.on(SignalrConsts.receiveMessageAds, (List? arguments) { if (arguments == null || arguments.isEmpty) return; List chat = []; @@ -889,6 +958,23 @@ class ChatVM extends BaseVM { Future onNewMessageReceivedForAds({required List messages}) async { for (var msg in messages) { + // Check if message already exists to prevent duplicates + bool messageExists = false; + + if (msg.id != null) { + // For messages with ID, use ID for duplicate detection + messageExists = currentMessagesForAds.any((existingMsg) => existingMsg.id == msg.id); + } else { + // For messages without ID (locally created), use content-based duplicate detection + messageExists = currentMessagesForAds.any((existingMsg) { + return existingMsg.senderUserID == msg.senderUserID && existingMsg.chatText == msg.chatText && existingMsg.messageType == msg.messageType; + }); + } + + if (messageExists) { + log("Message already exists in ads chat (id: ${msg.id}), skipping duplicate"); + continue; + } currentMessagesForAds.add(msg); } scrollChatDown(); @@ -965,7 +1051,7 @@ class ChatVM extends BaseVM { if (AppState().currentAppType == AppType.customer) { } else {} - + isSendingMessage = false; notifyListeners(); return true; @@ -977,7 +1063,7 @@ class ChatVM extends BaseVM { return false; } } - + isSendingMessage = false; notifyListeners(); return false; @@ -1021,6 +1107,23 @@ class ChatVM extends BaseVM { Future onNewMessageReceivedForGeneral({required List messages}) async { for (var msg in messages) { + // Check if message already exists to prevent duplicates + bool messageExists = false; + + if (msg.id != null) { + // For messages with ID, use ID for duplicate detection + messageExists = currentMessagesForGeneralChat.any((existingMsg) => existingMsg.id == msg.id); + } else { + // For messages without ID (locally created), use content-based duplicate detection + messageExists = currentMessagesForGeneralChat.any((existingMsg) { + return existingMsg.senderUserID == msg.senderUserID && existingMsg.chatText == msg.chatText && existingMsg.messageType == msg.messageType; + }); + } + + if (messageExists) { + log("Message already exists in general chat (id: ${msg.id}), skipping duplicate"); + continue; + } currentMessagesForGeneralChat.add(msg); // ✅ FIXED: Use correct list } scrollChatDown(); @@ -1028,6 +1131,13 @@ class ChatVM extends BaseVM { } void _subscribeToReceiveGeneralMessages() { + // Clear existing listener first to prevent duplicates + try { + hubConnection!.off(SignalrConsts.receiveMessageGeneral); + } catch (e) { + // Ignore if no listener exists + } + // General messages don't need specific VM, just storing messages hubConnection!.on(SignalrConsts.receiveMessageGeneral, (List? arguments) { if (arguments == null || arguments.isEmpty) return; @@ -1043,6 +1153,120 @@ class ChatVM extends BaseVM { logger.i("Subscribed to General messages"); } + void _subscribeToRequestOfferReadUpdate() { + if (_requestsVM == null) { + logger.e("RequestsVM not set! Cannot subscribe to request offer read updates."); + return; + } + + // Clear existing listener first to prevent duplicates + try { + hubConnection!.off(SignalrConsts.requestOfferReadUpdate); + } catch (e) { + // Ignore if no listener exists + } + + hubConnection!.on(SignalrConsts.requestOfferReadUpdate, (List? arguments) { + log("🔔 RequestOfferReadUpdate EVENT TRIGGERED! Args: $arguments"); + + if (arguments == null || arguments.isEmpty) { + log("⚠️ RequestOfferReadUpdate: Arguments are null or empty"); + return; + } + + try { + final data = arguments[0] as Map; + log("✅ Parsed RequestOfferReadUpdate data: reqOfferChatID=${data['reqOfferChatID']}, isRead=${data['isRead']}"); + _handleRequestOfferReadUpdate(data); + } catch (e) { + log("❌ Error parsing RequestOfferReadUpdate: ${e.toString()}"); + } + }); + + log("✅ Subscribed to RequestOfferReadUpdate"); + } + + void _handleRequestOfferReadUpdate(Map data) async { + try { + final int? messageId = data['reqOfferChatID']; + final int? reqOfferID = data['reqOfferID']; + final bool? isRead = data['isRead']; + final String? readOn = data['readOn']; + + if (messageId == null) { + log("❌ RequestOfferReadUpdate: reqOfferChatID is null"); + return; + } + + log("📨 Processing read update: msgID=$messageId, offerID=$reqOfferID, isRead=$isRead"); + + // For Provider App: Reload messages from API to get updated read status and IDs + if (AppState().currentAppType == AppType.provider && _requestsVM != null) { + // Check if viewing the chat currently (currentSelectedRequest is set) + if (_requestsVM!.currentSelectedRequest != null) { + log("🔄 Reloading messages for currentSelectedRequest (ID: ${_requestsVM!.currentSelectedRequest!.id})"); + + await _reloadRequestMessages( + requestId: _requestsVM!.currentSelectedRequest!.id, + requestOfferId: 0, // Provider side always uses 0 (sees all offers for request) + customerId: _requestsVM!.currentSelectedRequest!.customerId ?? 0, + ); + + log("✅ Reloaded messages for currentSelectedRequest - read status updated"); + } else { + // No current chat open, optionally reload all requests + // (In most cases, we only care about updating the currently visible chat) + log("ℹ️ Read update received but no active chat open, skipping reload"); + } + } + + // For Customer App: Not needed (customer is the one marking as read) + if (AppState().currentAppType == AppType.customer) { + log("RequestOfferReadUpdate on customer side (ignored)"); + } + } catch (e) { + log("❌ Error in _handleRequestOfferReadUpdate: ${e.toString()}"); + } + } + + /// Helper method to reload request messages from API without showing loading dialog + Future _reloadRequestMessages({ + required int requestId, + required int requestOfferId, + required int customerId, + }) async { + try { + int providerId = AppState().getUser.data!.userInfo!.providerId!; + + log("🔄 Fetching messages from API: requestId=$requestId, requestOfferId=$requestOfferId"); + + List chatMessages = await chatRepo.getUsersChatMessagesForRequests( + providerId: providerId, + customerId: customerId, + requestOfferId: requestOfferId, + requestId: requestId, + ); + + // Find the request in myFilteredRequests and update its messages + int requestIndex = _requestsVM!.myFilteredRequests.indexWhere((req) => req.id == requestId); + if (requestIndex != -1) { + _requestsVM!.myFilteredRequests[requestIndex].chatMessages = chatMessages; + + // Also update currentSelectedRequest if it's the same request + if (_requestsVM!.currentSelectedRequest != null && _requestsVM!.currentSelectedRequest!.id == requestId) { + _requestsVM!.currentSelectedRequest!.chatMessages = chatMessages; + } + + log("✅ Updated ${chatMessages.length} messages for request $requestId"); + notifyListeners(); + } else { + log("⚠️ Request $requestId not found in myFilteredRequests"); + } + } catch (e) { + log("❌ Error reloading messages: ${e.toString()}"); + } + } + Future onTextMessageSendForGeneralChat({ required String receiverId, required ChatMessageTypeEnum chatMessageType, @@ -1110,7 +1334,7 @@ class ChatVM extends BaseVM { if (AppState().currentAppType == AppType.customer) { } else {} - + isSendingMessage = false; notifyListeners(); return true; @@ -1122,7 +1346,7 @@ class ChatVM extends BaseVM { return false; } } - + isSendingMessage = false; notifyListeners(); return false; diff --git a/lib/view_models/dashboard_view_model_customer.dart b/lib/view_models/dashboard_view_model_customer.dart index a184209..f465923 100644 --- a/lib/view_models/dashboard_view_model_customer.dart +++ b/lib/view_models/dashboard_view_model_customer.dart @@ -42,26 +42,26 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - logger.i("App lifecycle state changed to: $state"); - + log("App lifecycle state changed to: $state"); + if (_context == null) return; - + final chatVM = Provider.of(_context!, listen: false); - + switch (state) { case AppLifecycleState.resumed: - // App came back to foreground - ensure SignalR is connected + // App came back to foreground - ensure SignalR is connected logger.i("App resumed - checking SignalR connection"); chatVM.buildHubConnection(context: _context); break; case AppLifecycleState.paused: - // App went to background - keep connection alive (as per user requirement) + // App went to background - keep connection alive (as per user requirement) logger.i("App paused - SignalR staying connected"); break; case AppLifecycleState.inactive: case AppLifecycleState.detached: case AppLifecycleState.hidden: - // Other states - no action needed + // Other states - no action needed break; } } diff --git a/lib/view_models/payment_view_model.dart b/lib/view_models/payment_view_model.dart index 373f964..44c8d54 100644 --- a/lib/view_models/payment_view_model.dart +++ b/lib/view_models/payment_view_model.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/scheduler.dart'; import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; @@ -85,8 +86,8 @@ class PaymentVM extends ChangeNotifier { ); // Utils.hideLoading(context); // Commented out: matching pair with showLoading - log("isPaid: ${isPaid}"); - // TODO: VERIFY THIS WITH ZAHOOR + log("isPaid: $isPaid"); + if (!isPaid) { // Utils.showToast(LocaleKeys.paymentFailed.tr()); // pop(context); @@ -94,9 +95,11 @@ class PaymentVM extends ChangeNotifier { } else { Utils.showToast(LocaleKeys.paymentSuccessful.tr()); } + + // Call onVerified callback - success methods handle their own context checks onVerified(); } catch (e) { - log("verifyPayments: ${e.toString()}"); + log("verifyPayments error: ${e.toString()}"); Utils.showToast(e.toString()); return; } @@ -147,7 +150,7 @@ class PaymentVM extends ChangeNotifier { context: context, paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(), id: orderProviderSubscriptionId, - onVerified: () => onSubscriptionPaymentSuccess(context), + onVerified: () => onSubscriptionPaymentSuccess(), ); break; @@ -173,6 +176,14 @@ class PaymentVM extends ChangeNotifier { break; case PaymentTypes.adReserve: + await verifyPayments( + context: context, + paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(), + id: currentAdId, + onVerified: () => onAdReservePaymentSuccess(context), + ); + break; + case PaymentTypes.ads: case PaymentTypes.extendAds: await verifyPayments( @@ -187,6 +198,18 @@ class PaymentVM extends ChangeNotifier { } void onRequestPaymentSuccess(BuildContext context) { + log("onRequestPaymentSuccess"); + // Check if context is still mounted before using it + if (!context.mounted) { + log("Context not mounted, using navigatorKey instead"); + // Use navigatorKey to navigate without context dependency + navigatorKey.currentState?.pushNamedAndRemoveUntil( + AppRoutes.dashboard, + (route) => false, + ); + return; + } + if (AppState().currentAppType == AppType.provider) { context.read().onNavbarTapped(4); } else { @@ -196,6 +219,19 @@ class PaymentVM extends ChangeNotifier { } void onAdsPaymentSuccess(BuildContext context) { + log("onAdsPaymentSuccess"); + // Check if context is still mounted before using it + if (!context.mounted) { + log("Context not mounted, using navigatorKey instead"); + // Use navigatorKey to navigate without context dependency + navigatorKey.currentState?.pushNamedAndRemoveUntil( + AppRoutes.dashboard, + (route) => false, + arguments: DashboardRouteEnum.fromAdsPayment, + ); + return; + } + context.read().updateIsExploreAds(false); if (AppState().currentAppType == AppType.provider) { context.read().onNavbarTapped(3); @@ -207,14 +243,68 @@ class PaymentVM extends ChangeNotifier { navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsPayment); } - void onSubscriptionPaymentSuccess(BuildContext context) { + void onAdReservePaymentSuccess(BuildContext context) { + log("onAdReservePaymentSuccess - Reserve ads payment completed"); + + // Always use navigatorKey for reliable navigation after payment + // Payment flow often unmounts context, so navigatorKey is more reliable + log("Navigating to dashboard with reserved ads filter"); + + // Navigate to dashboard first + navigatorKey.currentState?.pushNamedAndRemoveUntil( + AppRoutes.dashboard, + (route) => false, + arguments: DashboardRouteEnum.fromReserveAdsPayment, + ); + + // Use a post-frame callback to ensure navigation completes before updating state + WidgetsBinding.instance.addPostFrameCallback((_) { + final context = navigatorKey.currentContext; + if (context != null && context.mounted) { + try { + // Navigate to My Ads tab and apply Reserved filter + context.read().updateIsExploreAds(false); + if (AppState().currentAppType == AppType.provider) { + context.read().onNavbarTapped(3); // My Ads tab + context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); + } else { + context.read().onNavbarTapped(3); // My Ads tab + context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); + } + log("✅ Successfully navigated to Reserved Ads"); + } catch (e) { + log("⚠️ Error updating AdVM after navigation: $e"); + } + } else { + log("⚠️ Context not available for post-navigation updates"); + } + }); + } + + void onSubscriptionPaymentSuccess() { log("onSubscriptionPaymentSuccess"); - pop(context); + // Use navigatorKey to navigate without context dependency + // This works even after payment browser is closed and context is unmounted + navigatorKey.currentState?.pushNamedAndRemoveUntil( + AppRoutes.dashboard, + (route) => false, + ); // context.read().getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true); - navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); } void onAppointmentSuccess(BuildContext context) { + log("onAppointmentSuccess"); + // Check if context is still mounted before using it + if (!context.mounted) { + log("Context not mounted, using navigatorKey instead"); + // Use navigatorKey to navigate without context dependency + navigatorKey.currentState?.pushNamedAndRemoveUntil( + AppRoutes.dashboard, + (route) => false, + ); + return; + } + if (AppState().currentAppType == AppType.provider) { context.read().onNavbarTapped(1); } else { diff --git a/lib/view_models/requests_view_model.dart b/lib/view_models/requests_view_model.dart index 5a77eb5..7b8e234 100644 --- a/lib/view_models/requests_view_model.dart +++ b/lib/view_models/requests_view_model.dart @@ -115,7 +115,6 @@ class RequestsVM extends BaseVM { Future getRequests({bool isNeedToRebuild = false}) async { if (isNeedToRebuild) setState(ViewState.busy); applyFilterOnRequestsVM(requestsTypeEnum: RequestsTypeEnum.specialCarRequest); - isOfferSent = false; setState(ViewState.idle); } @@ -302,7 +301,6 @@ class RequestsVM extends BaseVM { } await getRequestsBasedOnFilters(); - isOfferSent = false; notifyListeners(); } @@ -930,6 +928,9 @@ class RequestsVM extends BaseVM { } else if (address.isEmpty && requestTypeId.selectedId == 2) { Utils.showToast(LocaleKeys.addValidAddress.tr()); isValid = false; + } else if (pickedVehicleImages.isEmpty) { + Utils.showToast(LocaleKeys.pleaseAddAtLeastOneImage.tr()); + isValid = false; } return isValid; } @@ -1220,12 +1221,6 @@ class RequestsVM extends BaseVM { return requestImages; } - bool isOfferSent = false; - - updateIsOfferSent(var value) { - isOfferSent = value; - notifyListeners(); - } Future updateOfferFromProvider({ required String message, @@ -1306,81 +1301,94 @@ class RequestsVM extends BaseVM { required int? offerId, }) async { if (isSendOfferValidated()) { - final chatVM = context.read(); - List images = getMessagesImageList(offerId: context.read().latestOfferId); - List offerImages = []; - images.forEach((element) { - offerImages.add(element.toJson()); - }); - - bool status = await chatVM.onOfferSendForRequest( - receiverId: receiverId, - chatMessageType: ChatMessageTypeEnum.offer, - message: message, - requestId: requestId, - offerPrice: offerPrice, - isDeliveryAvailable: isDeliveryAvailable, - offerDeliveryOption: offerDeliveryOption, - manufacturedByName: manufacturedByName, - manufacturedOn: manufacturedOn, - serviceItemName: serviceItemName, - offerImages: offerImages, - context: context, - ); - log("status from onOfferSendForRequest: $status"); - - if (!status) { - Utils.showToast(LocaleKeys.somethingWrong.tr()); - return; - } + try { + Utils.showLoading(context); + + final chatVM = context.read(); + List images = getMessagesImageList(offerId: context.read().latestOfferId); + List offerImages = []; + images.forEach((element) { + offerImages.add(element.toJson()); + }); - updateIsOfferSent(true); - - final senderName = AppState().getUser.data!.userInfo!.firstName; - final senderId = AppState().getUser.data!.userInfo!.userId; - resetSendOfferBottomSheet(); - Navigator.pop(context); - ChatMessageModel chatMessageModel = ChatMessageModel( - isMyMessage: true, - chatText: message, - messageType: ChatMessageTypeEnum.offer.getIdFromChatMessageTypeEnum(), - senderName: senderName, - senderUserID: senderId, - receiverUserID: receiverId, - chatMessageTypeEnum: ChatMessageTypeEnum.offer, - requestID: requestModel.id, - offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(), - reqOffer: ReqOffer( - offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(), - requestID: requestModel.id, - price: double.parse(offerPrice), + bool status = await chatVM.onOfferSendForRequest( + receiverId: receiverId, + chatMessageType: ChatMessageTypeEnum.offer, + message: message, + requestId: requestId, + offerPrice: offerPrice, isDeliveryAvailable: isDeliveryAvailable, - requestDeliveryOption: offerDeliveryOption, + offerDeliveryOption: offerDeliveryOption, manufacturedByName: manufacturedByName, manufacturedOn: manufacturedOn, serviceItemName: serviceItemName, - requestOfferStatusEnum: RequestOfferStatusEnum.offer, - comment: message, - offerStatusText: "", - reqOfferImages: images, - ), - ); + offerImages: offerImages, + context: context, + ); + log("status from onOfferSendForRequest: $status"); - context.read().onNewMessageReceivedForRequestOffer(messages: [chatMessageModel], requestsVM: this, isMyOwnOffer: true); - - if (!isFromChatScreen) { - ChatViewArgumentsForRequest chatViewArgumentsForRequest = ChatViewArgumentsForRequest( - chatTypeEnum: ChatTypeEnum.requestOffer, - requestId: requestModel.id, - receiverId: requestModel.customerID, - senderId: senderId ?? "", - requestIndex: requestIndex, - providerIndex: -1, - requestModel: requestModel, + if (!status) { + Utils.hideLoading(context); + Utils.showToast(LocaleKeys.somethingWrong.tr()); + return; + } + + // Update the specific request's isChatted status locally for immediate UI feedback + myFilteredRequests[requestIndex].isChatted = true; + notifyListeners(); + + final senderName = AppState().getUser.data!.userInfo!.firstName; + final senderId = AppState().getUser.data!.userInfo!.userId; + resetSendOfferBottomSheet(); + + Utils.hideLoading(context); + Navigator.pop(context); + ChatMessageModel chatMessageModel = ChatMessageModel( + isMyMessage: true, + chatText: message, + messageType: ChatMessageTypeEnum.offer.getIdFromChatMessageTypeEnum(), + senderName: senderName, + senderUserID: senderId, + receiverUserID: receiverId, + chatMessageTypeEnum: ChatMessageTypeEnum.offer, + requestID: requestModel.id, + offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(), + reqOffer: ReqOffer( + offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(), + requestID: requestModel.id, + price: double.parse(offerPrice), + isDeliveryAvailable: isDeliveryAvailable, + requestDeliveryOption: offerDeliveryOption, + manufacturedByName: manufacturedByName, + manufacturedOn: manufacturedOn, + serviceItemName: serviceItemName, + requestOfferStatusEnum: RequestOfferStatusEnum.offer, + comment: message, + offerStatusText: "", + reqOfferImages: images, + ), ); - ChatViewArguments chatViewArguments = ChatViewArguments(chatTypeEnum: ChatTypeEnum.requestOffer, chatViewArgumentsForRequest: chatViewArgumentsForRequest); - navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments); + context.read().onNewMessageReceivedForRequestOffer(messages: [chatMessageModel], requestsVM: this, isMyOwnOffer: true); + + if (!isFromChatScreen) { + ChatViewArgumentsForRequest chatViewArgumentsForRequest = ChatViewArgumentsForRequest( + chatTypeEnum: ChatTypeEnum.requestOffer, + requestId: requestModel.id, + receiverId: requestModel.customerID, + senderId: senderId ?? "", + requestIndex: requestIndex, + providerIndex: -1, + requestModel: requestModel, + ); + + ChatViewArguments chatViewArguments = ChatViewArguments(chatTypeEnum: ChatTypeEnum.requestOffer, chatViewArgumentsForRequest: chatViewArgumentsForRequest); + navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments); + } + } catch (e) { + Utils.hideLoading(context); + log(e.toString()); + Utils.showToast(e.toString()); } } } diff --git a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart index 11a34a0..f8b4506 100644 --- a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart @@ -51,7 +51,8 @@ class _AdsDetailViewState extends State { Future onAdDetailsLoaded() async { context.read().updateCurrentAdId(id: widget.adDetails.id!); - if ((widget.adDetails.isMyAd ?? false) && (widget.adDetails.adPostStatus == AdPostStatus.reserved)) { + // Load banking info when viewing MY created ad that someone else reserved + if ((widget.adDetails.isMyAd ?? false) && !(widget.adDetails.isReservedByMe ?? false) && (widget.adDetails.adPostStatus == AdPostStatus.reserved)) { await adVM.getAdBankingAccountInfo(adId: widget.adDetails.id!); } } @@ -114,8 +115,9 @@ class _AdsDetailViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( - child: "${widget.adDetails.vehicle!.vehicleTitle} | ${isArabic ? widget.adDetails.vehicle!.color!.labelN : widget.adDetails.vehicle!.color!.label}|" - " ${widget.adDetails.id}".toText(fontSize: 16, maxLines: 2), + child: "${widget.adDetails.vehicle!.vehicleTitle} | ${isArabic ? widget.adDetails.vehicle!.color!.labelN : widget.adDetails.vehicle!.color!.label}|" + " ${widget.adDetails.id}" + .toText(fontSize: 16, maxLines: 2), ), (widget.adDetails.vehicle!.cityName ?? "").toText(fontSize: 10, color: MyColors.lightTextColor), ], @@ -126,7 +128,7 @@ class _AdsDetailViewState extends State { Row( children: [ ("${LocaleKeys.vehicleModel.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor), - "${isArabic? widget.adDetails.vehicle!.model!.labelN :widget.adDetails.vehicle!.model!.label}".toText(fontSize: 12), + "${isArabic ? widget.adDetails.vehicle!.model!.labelN : widget.adDetails.vehicle!.model!.label}".toText(fontSize: 12), ], ), (widget.adDetails.vehicle!.countryName ?? '').toText(fontSize: 10, color: MyColors.lightTextColor), @@ -138,7 +140,6 @@ class _AdsDetailViewState extends State { "${widget.adDetails.vehicle!.modelyear!.label}".toText(fontSize: 12), ], ), - Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -154,7 +155,7 @@ class _AdsDetailViewState extends State { Row( children: [ ("${LocaleKeys.transmission.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor), - "${isArabic? widget.adDetails.vehicle!.transmission!.labelN :widget.adDetails.vehicle!.transmission!.label}".toText(fontSize: 12), + "${isArabic ? widget.adDetails.vehicle!.transmission!.labelN : widget.adDetails.vehicle!.transmission!.label}".toText(fontSize: 12), ], ), Row( @@ -445,7 +446,9 @@ class _AdsDetailViewState extends State { @override Widget build(BuildContext context) { Widget actionWidget = const SizedBox(); + // Show delete button ONLY for ads I created/own (not ads I reserved from others) if ((widget.adDetails.isMyAd ?? false) && + !(widget.adDetails.isReservedByMe ?? false) && (widget.adDetails.adPostStatus != AdPostStatus.reserved) && (widget.adDetails.adPostStatus != AdPostStatus.active) && (widget.adDetails.adPostStatus != AdPostStatus.pendingForPost)) { @@ -506,7 +509,8 @@ class _AdsDetailViewState extends State { 12.height, buildPersonalInformationCard(context: context), ], - if (widget.adDetails.isMyAd ?? false) ...[ + // Show special services and dates ONLY for ads I created/own (not ads I reserved from others) + if ((widget.adDetails.isMyAd ?? false) && !(widget.adDetails.isReservedByMe ?? false)) ...[ if (widget.adDetails.specialservice != null && widget.adDetails.specialservice!.isNotEmpty) ...[ 12.height, buildSpecialServicesContainer(), @@ -537,7 +541,8 @@ class _AdsDetailViewState extends State { ), 14.height, ], - widget.adDetails.isMyAd ?? false ? BuildAdDetailsActionButtonForMyAds(adDetailsModel: widget.adDetails) : BuildAdDetailsActionButtonForExploreAds(adDetailsModel: widget.adDetails), + // Show action buttons (mark as sold, etc.) ONLY for ads I created/own (not ads I reserved) + (widget.adDetails.isMyAd ?? false) ? BuildAdDetailsActionButtonForMyAds(adDetailsModel: widget.adDetails) : BuildAdDetailsActionButtonForExploreAds(adDetailsModel: widget.adDetails), ], ), ) @@ -567,12 +572,12 @@ class _FavoriteAdButtonState extends State with SingleTickerPr void initState() { super.initState(); isFavorite = widget.adDetails.isAdFavorite ?? false; - + _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); - + _scaleAnimation = Tween(begin: 1.0, end: 1.3).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); @@ -660,13 +665,13 @@ class ShareAdButton extends StatelessWidget { void _shareAd() { final vehicle = adDetails.vehicle!; - + // Build share message String shareMessage = ''; - + // Add header shareMessage += '${LocaleKeys.checkOutThisAd.tr()}\n\n'; - + // Vehicle details shareMessage += '${vehicle.vehicleTitle ?? ""}\n'; shareMessage += '${LocaleKeys.model.tr()}: ${vehicle.modelyear?.label ?? ""}\n'; @@ -676,10 +681,10 @@ class ShareAdButton extends StatelessWidget { shareMessage += '${LocaleKeys.location.tr()}: ${vehicle.cityName}\n'; } shareMessage += '\n'; - + // App download links shareMessage += '${LocaleKeys.downloadApp.tr()}\n'; - + if (Platform.isAndroid) { shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n'; } else if (Platform.isIOS) { @@ -689,7 +694,7 @@ class ShareAdButton extends StatelessWidget { shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n'; shareMessage += '${LocaleKeys.iosApp.tr()}: https://apps.apple.com/app/mowater/id123456789\n'; } - + // Share using share_plus Share.share(shareMessage); } diff --git a/lib/views/advertisement/ads_detail_view/components.dart b/lib/views/advertisement/ads_detail_view/components.dart index 6824f93..1d8cdf0 100644 --- a/lib/views/advertisement/ads_detail_view/components.dart +++ b/lib/views/advertisement/ads_detail_view/components.dart @@ -276,7 +276,6 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { void onBookPhotographyServiceClicked(BuildContext context, {required AdDetailsModel adDetailsModel}) async { AdVM adVM = context.read(); - log("adDetailsModel.vehicle!.cityID: ${adDetailsModel.vehicle!.cityID}"); if (adVM.photoOfficeSelectedId.selectedId == -1) { adVM.getPhotographyServiceScheduleListByOffices( cityId: adDetailsModel.vehicle!.cityID ?? AppState().getUser.data!.userInfo!.cityId ?? 0, @@ -759,6 +758,9 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { } Future buildCancelReservationBottomSheet(BuildContext context, {required AdDetailsModel adDetails}) { + // Capture parent context that will remain valid + final BuildContext parentContext = context; + return showModalBottomSheet( context: context, isScrollControlled: true, @@ -794,18 +796,21 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { bool status = adVM.validateReservationCancelReason(); if (status) { Navigator.pop(context); + // Save reason before showing confirmation dialog + final String cancelReason = adVM.reservationCancelReason; return actionConfirmationBottomSheet( - context: context, + context: parentContext, // Use parent context title: LocaleKeys.doWantCancelReservation.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), - subtitle: LocaleKeys.yourAdVisibletoBuy.tr(), + subtitle: "yourAdVisibletoBuy.".tr(), actionButtonYes: Expanded( child: ShowFillButton( maxHeight: 55, title: LocaleKeys.yes.tr(), fontSize: 15, - onPressed: () { - Navigator.pop(context); - adVM.cancelMyAdReservation(context, adId: adDetails.id!, reason: ""); + onPressed: () async { + Navigator.pop(parentContext); // Pop confirmation + // Call with parent context that's still valid + await adVM.cancelMyAdReservation(parentContext, adId: adDetails.id!, reason: cancelReason); }, ), ), @@ -1040,12 +1045,16 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { @override Widget build(BuildContext context) { + log("adDetailsModel.adPostStatus: ${adDetailsModel.adPostStatus}"); + switch (adDetailsModel.adPostStatus!) { case AdPostStatus.pendingForPayment: return pendingForPaymentAction(context, ad: adDetailsModel); case AdPostStatus.active: return markAsSoldAction(context); case AdPostStatus.reserved: + // adPostStatus=reserved is set in getMyReservedAds() + // adReserveStatus tells us the detailed status (reserved, timeOver, dealDone, etc.) AdVM adVM = context.watch(); if (adVM.state == ViewState.busy) { return const CircularProgressIndicator(); diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index ac33d3b..475f205 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -275,7 +275,18 @@ class _AdCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ + // When viewing reserved ads filter, show ONLY reserve status badge + if (widget.adDetails.isReservedByMe == true && + widget.adDetails.adReserveStatus != null && + widget.adDetails.adReserveStatus != AdReserveStatus.defaultStatus && + context.read().isReservedFilterSelected) ...[ + Utils.statusContainerChip( + text: Utils.getReserveStatusLabel(widget.adDetails.adReserveStatus!), + chipColor: Utils.getChipColorByReserveStatus(widget.adDetails.adReserveStatus!) + ), + ] + // Otherwise, show regular ad status for MyAds + else if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), ] else if (isAdsFragment && !context.read().isExploreAdsTapped && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), diff --git a/lib/views/advertisement/favourite_ads_view.dart b/lib/views/advertisement/favourite_ads_view.dart index fa8eee0..54a3968 100644 --- a/lib/views/advertisement/favourite_ads_view.dart +++ b/lib/views/advertisement/favourite_ads_view.dart @@ -10,6 +10,7 @@ import 'package:mc_common_app/views/advertisement/components/ads_list_widget.dar import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; import 'package:mc_common_app/widgets/common_widgets/filters_list.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart'; import 'package:provider/provider.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -36,12 +37,13 @@ class _FavouriteAdsViewState extends State { super.initState(); } - List getFilteredAds(AdVM adVM) { - if (selectedBrandId == 0) { - return adVM.favouriteAds; - } - return adVM.favouriteAds.where((ad) => ad.vehicle?.model?.vehicleBrandID == selectedBrandId).toList(); - } + // Removed client-side filtering - now using API filter + // List getFilteredAds(AdVM adVM) { + // if (selectedBrandId == 0) { + // return adVM.favouriteAds; + // } + // return adVM.favouriteAds.where((ad) => ad.vehicle?.model?.vehicleBrandID == selectedBrandId).toList(); + // } @override Widget build(BuildContext context) { @@ -58,7 +60,7 @@ class _FavouriteAdsViewState extends State { FiltersList( showImages: true, filterList: adVM.exploreAdsFilterOptions, - onFilterTapped: (index, selectedFilterId) { + onFilterTapped: (index, selectedFilterId) async { setState(() { selectedBrandId = selectedFilterId; }); @@ -67,6 +69,11 @@ class _FavouriteAdsViewState extends State { filter.isSelected = false; } adVM.exploreAdsFilterOptions[index].isSelected = true; + + // Call API with filter (0 = all brands) + await adVM.getFavouriteAds( + vehicleBrandId: selectedFilterId == 0 ? null : selectedFilterId, + ); }, needLeftPadding: false, ).paddingOnly(left: 21), @@ -75,11 +82,13 @@ class _FavouriteAdsViewState extends State { Expanded( child: RefreshIndicator( onRefresh: () async { - await adVM.getFavouriteAds(); + await adVM.getFavouriteAds( + vehicleBrandId: selectedBrandId == 0 ? null : selectedBrandId, + ); }, child: adVM.state == ViewState.busy - ? const Center(child: CircularProgressIndicator()) - : getFilteredAds(adVM).isEmpty + ? const AdsListShimmer(itemCount: 5) + : adVM.favouriteAds.isEmpty ? Center( child: Text( LocaleKeys.noFavouriteAds.tr(), @@ -90,7 +99,7 @@ class _FavouriteAdsViewState extends State { isAdsFragment: false, isDraftAds: false, shouldShowAdStatus: false, - adsList: getFilteredAds(adVM), + adsList: adVM.favouriteAds, // Use API-filtered list directly hasMoreData: false, ), ), diff --git a/lib/views/chat/widgets/chat_message_widget.dart b/lib/views/chat/widgets/chat_message_widget.dart index 3d5495c..c2c7ca4 100644 --- a/lib/views/chat/widgets/chat_message_widget.dart +++ b/lib/views/chat/widgets/chat_message_widget.dart @@ -780,19 +780,36 @@ class _ChatMessageCustomWidgetState extends State { Widget buildFreeTextDetailsInMessage({required ChatMessageTypeEnum chatMessageTypeEnum}) { return Row( children: [ - if (chatMessageTypeEnum == ChatMessageTypeEnum.offer && (widget.chatMessageModel.isMyMessage == true) && (widget.chatMessageModel.isRead ?? false)) ...[ - Row( - children: [ - const Icon(Icons.remove_red_eye_outlined, size: 12, color: MyColors.lightTextColor), - 4.width, - LocaleKeys.viewed.tr().toText(fontSize: 10, color: MyColors.lightTextColor, fontWeight: MyFonts.Medium), - ], - ), - ] else if (AppState().currentAppType == AppType.provider && + // For Provider: Show status and edit button + if (AppState().currentAppType == AppType.provider && chatMessageTypeEnum == ChatMessageTypeEnum.offer && - widget.chatMessageModel.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.offer && - (widget.chatMessageModel.isMyMessage == true && widget.chatMessageModel.reqOffer != null && widget.chatMessageModel.reqOffer!.id != null)) ...[ - MyAssets.icEdit.buildSvg(color: MyColors.white, height: 15).onPress(() => onOfferEditIconPressed()), + widget.chatMessageModel.isMyMessage == true && + widget.chatMessageModel.reqOffer != null && + widget.chatMessageModel.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.offer) ...[ + // Show read status if message is read + if (widget.chatMessageModel.isRead ?? false) ...[ + Row( + children: [ + const Icon(Icons.done_all, size: 14, color: Colors.blue), + 4.width, + LocaleKeys.read.tr().toText(fontSize: 10, color: Colors.blue, fontWeight: MyFonts.Medium), + 8.width, + ], + ), + ] + // Show edit button if message is NOT read + else ...[ + Row( + children: [ + const Icon(Icons.check, size: 14, color: MyColors.lightTextColor), + 4.width, + LocaleKeys.sent.tr().toText(fontSize: 10, color: MyColors.lightTextColor, fontWeight: MyFonts.Medium), + 8.width, + MyAssets.icEdit.buildSvg(color: MyColors.white, height: 15).onPress(() => onOfferEditIconPressed()), + 8.width, + ], + ), + ], ], Expanded( child: Directionality( diff --git a/lib/views/common_fragments/ads_fragment.dart b/lib/views/common_fragments/ads_fragment.dart index e65a834..b2a00c3 100644 --- a/lib/views/common_fragments/ads_fragment.dart +++ b/lib/views/common_fragments/ads_fragment.dart @@ -46,21 +46,23 @@ class AdsFragment extends StatelessWidget { isRemoveBackButton: true, actions: [ Padding( - padding: EdgeInsets.only(top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: 21), + padding: EdgeInsets.only( + top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: 21), child: Badge( isLabelVisible: adVM.adsFiltersCounter > 0, largeSize: 15, smallSize: 20, backgroundColor: MyColors.darkPrimaryColor, label: Text('${adVM.adsFiltersCounter}'), - child: MyAssets.searchIcon.buildSvg(), + child: MyAssets.searchIcon.buildSvg(height: 22, width: 22), ), ).onPress(() { navigateWithName(context, AppRoutes.adsFilterView); }) ], ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, + arguments: false); }), body: Stack( children: [ @@ -71,7 +73,8 @@ class AdsFragment extends StatelessWidget { children: [ 16.height, Consumer( - builder: (BuildContext context, AdVM adVM, Widget? child) { + builder: + (BuildContext context, AdVM adVM, Widget? child) { return Column( children: [ Row( @@ -82,7 +85,9 @@ class AdsFragment extends StatelessWidget { maxHeight: 55, fontSize: 15, title: LocaleKeys.exploreAds.tr(), - txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, + txtColor: adVM.isExploreAdsTapped + ? MyColors.white + : MyColors.darkTextColor, onPressed: () { log("accessToken: ${AppState().getUser.data!.accessToken}"); log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}"); @@ -98,7 +103,9 @@ class AdsFragment extends StatelessWidget { Expanded( child: ShowFillButton( isFilled: !adVM.isExploreAdsTapped, - txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, + txtColor: !adVM.isExploreAdsTapped + ? MyColors.white + : MyColors.darkTextColor, maxHeight: 55, fontSize: 15, title: LocaleKeys.myAds.tr(), @@ -106,7 +113,9 @@ class AdsFragment extends StatelessWidget { adVM.updateIsExploreAds(false); }, ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + navigateWithName( + context, AppRoutes.loginWithPassword, + arguments: false); }), ), ], @@ -118,7 +127,8 @@ class AdsFragment extends StatelessWidget { showImages: true, filterList: adVM.exploreAdsFilterOptions, onFilterTapped: (index, selectedFilterId) { - adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId); + adVM.applyFilterOnExploreAds( + vehicleBrandId: selectedFilterId); }, needLeftPadding: false, ).paddingOnly(left: 21), @@ -128,7 +138,9 @@ class AdsFragment extends StatelessWidget { FiltersList( filterList: adVM.myAdsFilterOptions, onFilterTapped: (index, selectedFilterId) { - adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum()); + adVM.applyFilterOnMyAds( + adPostStatusEnum: + selectedFilterId.toAdPostEnum()); }, needLeftPadding: false, ).paddingOnly(left: 21), @@ -142,11 +154,19 @@ class AdsFragment extends StatelessWidget { child: RefreshIndicator( onRefresh: () async { if (adVM.isExploreAdsTapped) { - int vehicleBrandId = adVM.exploreAdsFilterOptions.firstWhere((element) => element.isSelected).id; - adVM.applyFilterOnExploreAds(vehicleBrandId: vehicleBrandId); + int vehicleBrandId = adVM.exploreAdsFilterOptions + .firstWhere((element) => element.isSelected) + .id; + adVM.applyFilterOnExploreAds( + vehicleBrandId: vehicleBrandId); } else { - AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); - adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum); + AdPostStatus adPostStatusEnum = adVM + .myAdsFilterOptions + .firstWhere((element) => element.isSelected) + .id + .toAdPostEnum(); + adVM.applyFilterOnMyAds( + adPostStatusEnum: adPostStatusEnum); } }, child: adVM.state == ViewState.busy @@ -156,14 +176,22 @@ class AdsFragment extends StatelessWidget { isDraftAds: false, shouldShowAdStatus: !adVM.isExploreAdsTapped, adsList: getAdsList(adVM), - hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, + hasMoreData: adVM.isExploreAdsTapped + ? adVM.hasMoreDataForExploreAds + : adVM.hasMoreDataForMyAds, onFetchMoreAds: () async { - AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); + AdPostStatus adPostStatusEnum = adVM + .myAdsFilterOptions + .firstWhere( + (element) => element.isSelected) + .id + .toAdPostEnum(); if (adVM.isExploreAdsTapped) { await adVM.fetchMoreExploreAds(); } else { - await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum); + await adVM.fetchMoreMyAds( + adsStatus: adPostStatusEnum); } }, ), @@ -187,7 +215,8 @@ class AdsFragment extends StatelessWidget { color: Colors.black.withOpacity(0.3), child: const Center( child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(MyColors.darkPrimaryColor), + valueColor: AlwaysStoppedAnimation( + MyColors.darkPrimaryColor), ), ), ), @@ -197,30 +226,38 @@ class AdsFragment extends StatelessWidget { onPressed: () async { // ✅ Await API calls with loading state await adVM.getVehicleAdsDuration(); - + // ✅ Ensure vehicle types are loaded if (adVM.vehicleTypes.isEmpty) { await adVM.getVehicleTypes(); } - + if (AppState().userType != UserType.providerDealer) { - if (adVM.vehicleAdDurationId.selectedId == -1 && adVM.vehicleAdsDurations.isNotEmpty) { + if (adVM.vehicleAdDurationId.selectedId == -1 && + adVM.vehicleAdsDurations.isNotEmpty) { SelectionModel selection = SelectionModel( selectedId: adVM.vehicleAdsDurations.first.id ?? 0, - selectedOption: ("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"), + selectedOption: + ("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"), itemPrice: adVM.vehicleAdsDurations.first.price!.toString(), - currency: adVM.vehicleAdsDurations.first.currency!.toString(), + currency: + adVM.vehicleAdsDurations.first.currency!.toString(), ); adVM.updateVehicleAdDurationId(selection); } } - navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [injector.get().currentAppType == AppType.provider, false, -1]); + navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [ + injector.get().currentAppType == AppType.provider, + false, + -1 + ]); }, backgroundColor: MyColors.darkPrimaryColor, child: const Icon(Icons.add, color: MyColors.white), ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, + arguments: false); }), ); }, diff --git a/lib/views/common_fragments/requests_fragment.dart b/lib/views/common_fragments/requests_fragment.dart index 823bb2d..8000122 100644 --- a/lib/views/common_fragments/requests_fragment.dart +++ b/lib/views/common_fragments/requests_fragment.dart @@ -29,7 +29,9 @@ class MyRequestsFragment extends StatelessWidget { void cancelRequestBottomSheet(BuildContext context, int requestId) { return actionConfirmationBottomSheet( context: context, - title: LocaleKeys.cancelRequestPrompt.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), + title: LocaleKeys.cancelRequestPrompt + .tr() + .toText(fontSize: 28, isBold: true, letterSpacing: -1.44), subtitle: LocaleKeys.requestPermanentlyCancelled.tr(), actionButtonYes: Expanded( child: ShowFillButton( @@ -41,7 +43,10 @@ class MyRequestsFragment extends StatelessWidget { final requestVM = context.read(); - bool status = await requestVM.onActionRequestTapped(context: context, requestStatusEnum: RequestStatusEnum.cancelled, requestId: requestId); + bool status = await requestVM.onActionRequestTapped( + context: context, + requestStatusEnum: RequestStatusEnum.cancelled, + requestId: requestId); if (status) { await requestVM.getRequests(isNeedToRebuild: true); } @@ -69,7 +74,8 @@ class MyRequestsFragment extends StatelessWidget { return false; } - if (requestStatusEnum == RequestStatusEnum.pending || requestStatusEnum == RequestStatusEnum.submitted) { + if (requestStatusEnum == RequestStatusEnum.pending || + requestStatusEnum == RequestStatusEnum.submitted) { return true; } return false; @@ -77,7 +83,8 @@ class MyRequestsFragment extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer(builder: (BuildContext context, RequestsVM requestsVM, Widget? child) { + return Consumer( + builder: (BuildContext context, RequestsVM requestsVM, Widget? child) { return Scaffold( appBar: CustomAppBar( title: LocaleKeys.manageRequests.tr(), @@ -86,7 +93,9 @@ class MyRequestsFragment extends StatelessWidget { if (AppState().currentAppType == AppType.provider) ...[ Padding( padding: const EdgeInsets.only(top: 8, bottom: 8, right: 15), - child: const Icon(Icons.messenger_outline_rounded, color: Colors.black, size: 18).toContainer( + child: const Icon(Icons.messenger_outline_rounded, + color: Colors.black, size: 18) + .toContainer( borderRadius: 80, borderColor: MyColors.lightGreyEFColor, isEnabledBorder: true, @@ -96,21 +105,24 @@ class MyRequestsFragment extends StatelessWidget { }) ], Padding( - padding: EdgeInsets.only(top: requestsVM.requestsFiltersCounter > 0 ? 20 : 0, right: 21), + padding: EdgeInsets.only( + top: requestsVM.requestsFiltersCounter > 0 ? 20 : 0, + right: 21), child: Badge( isLabelVisible: requestsVM.requestsFiltersCounter > 0, largeSize: 15, smallSize: 20, backgroundColor: MyColors.darkPrimaryColor, label: Text('${requestsVM.requestsFiltersCounter}'), - child: MyAssets.searchIcon.buildSvg(), + child: MyAssets.searchIcon.buildSvg(height: 22, width: 22), ), ).onPress(() { navigateWithName(context, AppRoutes.requestsFilterView); }), ], ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, + arguments: false); }), body: Container( color: MyColors.backgroundColor, @@ -122,7 +134,8 @@ class MyRequestsFragment extends StatelessWidget { FiltersList( filterList: requestsVM.requestsTypeFilterOptions, onFilterTapped: (index, selectedFilterId) { - requestsVM.applyFilterOnRequestsVM(requestsTypeEnum: selectedFilterId.toRequestTypeEnum()); + requestsVM.applyFilterOnRequestsVM( + requestsTypeEnum: selectedFilterId.toRequestTypeEnum()); }, ), // Show condition filters for all request types @@ -131,27 +144,31 @@ class MyRequestsFragment extends StatelessWidget { FiltersList( filterList: requestsVM.sparePartsConditionFilterOptions, onFilterTapped: (index, selectedFilterId) { - requestsVM.applySparePartsConditionFilter(conditionId: selectedFilterId); + requestsVM.applySparePartsConditionFilter( + conditionId: selectedFilterId); }, ), ], 8.height, Expanded( child: RefreshIndicator( - onRefresh: () async => await requestsVM.getRequestsBasedOnFilters(), + onRefresh: () async => + await requestsVM.getRequestsBasedOnFilters(), child: requestsVM.state == ViewState.busy ? const RequestsListShimmer(itemCount: 5) : requestsVM.myFilteredRequests.isEmpty ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - LocaleKeys.noRequeststoShow.tr().toText(fontSize: 16, color: MyColors.lightTextColor), + LocaleKeys.noRequeststoShow.tr().toText( + fontSize: 16, color: MyColors.lightTextColor), if (requestsVM.requestsFiltersCounter > 0) ...[ 8.height, InkWell( onTap: () async { requestsVM.clearRequestsFilters(); - await requestsVM.getRequestsBasedOnFilters(); + await requestsVM + .getRequestsBasedOnFilters(); }, child: LocaleKeys.clearFilters.tr().toText( fontSize: 14, @@ -170,43 +187,76 @@ class MyRequestsFragment extends StatelessWidget { secondaryBackground: Container( color: const Color(0xFFDD0505), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ - LocaleKeys.cancel.tr().toText(fontSize: 15, fontWeight: MyFonts.Medium, height: 23 / 25, color: MyColors.white).margin(right: 23), + LocaleKeys.cancel + .tr() + .toText( + fontSize: 15, + fontWeight: MyFonts.Medium, + height: 23 / 25, + color: MyColors.white) + .margin(right: 23), ], ), ), dragStartBehavior: DragStartBehavior.start, - direction: shouldDismiss(requestsVM.myFilteredRequests[index].requestStatus) ? DismissDirection.endToStart : DismissDirection.none, - confirmDismiss: (DismissDirection direction) async { - cancelRequestBottomSheet(context, requestsVM.myFilteredRequests[index].id); + direction: shouldDismiss(requestsVM + .myFilteredRequests[index] + .requestStatus) + ? DismissDirection.endToStart + : DismissDirection.none, + confirmDismiss: + (DismissDirection direction) async { + cancelRequestBottomSheet(context, + requestsVM.myFilteredRequests[index].id); return; }, child: RequestItem( - request: requestsVM.myFilteredRequests[index], + request: + requestsVM.myFilteredRequests[index], shouldShowStatuses: true, onTap: () async { - RequestModel request = requestsVM.myFilteredRequests[index]; - requestsVM.updateCurrentSelectedRequest(request); + RequestModel request = + requestsVM.myFilteredRequests[index]; + requestsVM.updateCurrentSelectedRequest( + request); log("request.requestStatus: ${request.requestStatus}"); - if (request.requestStatus == RequestStatusEnum.pending || - request.requestStatus == RequestStatusEnum.cancelled || - request.requestStatus == RequestStatusEnum.expired) { + if (request.requestStatus == + RequestStatusEnum.pending || + request.requestStatus == + RequestStatusEnum.cancelled || + request.requestStatus == + RequestStatusEnum.expired) { return; } - if (AppState().currentAppType == AppType.provider) { - RequestDetailPageArguments requestDetailPageArguments = RequestDetailPageArguments(requestIndex: index, requestModel: request); - navigateWithName(context, AppRoutes.requestsDetailPage, arguments: requestDetailPageArguments); + if (AppState().currentAppType == + AppType.provider) { + RequestDetailPageArguments + requestDetailPageArguments = + RequestDetailPageArguments( + requestIndex: index, + requestModel: request); + navigateWithName(context, + AppRoutes.requestsDetailPage, + arguments: + requestDetailPageArguments); } else { - requestsVM.myFilteredRequests[index].offerCount = 0; + requestsVM.myFilteredRequests[index] + .offerCount = 0; requestsVM.notifyListeners(); - navigateWithName(context, AppRoutes.offersListPage, arguments: request.id); + navigateWithName( + context, AppRoutes.offersListPage, + arguments: request.id); } }).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + navigateWithName( + context, AppRoutes.loginWithPassword, + arguments: false); }), ); }, @@ -214,7 +264,8 @@ class MyRequestsFragment extends StatelessWidget { return 16.height; }, itemCount: requestsVM.myFilteredRequests.length, - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16, top: 8), + padding: const EdgeInsets.only( + left: 16, right: 16, bottom: 16, top: 8), ), )) ], @@ -222,14 +273,16 @@ class MyRequestsFragment extends StatelessWidget { ), floatingActionButton: AppState().currentAppType == AppType.customer ? FloatingActionButton( - onPressed: () => navigateWithName(context, AppRoutes.createRequestPage), + onPressed: () => + navigateWithName(context, AppRoutes.createRequestPage), backgroundColor: MyColors.darkPrimaryColor, child: const Icon( Icons.add, color: MyColors.white, ), ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, + arguments: false); }) : null, ); diff --git a/lib/views/requests/request_detail_page.dart b/lib/views/requests/request_detail_page.dart index 5efe370..1de723c 100644 --- a/lib/views/requests/request_detail_page.dart +++ b/lib/views/requests/request_detail_page.dart @@ -303,7 +303,7 @@ class RequestDetailPage extends StatelessWidget { appBar: CustomAppBar( title: LocaleKeys.requestDetail.tr(), actions: [ - (context.read().isOfferSent || requestDetailPageArguments.requestModel.isChatted) + requestDetailPageArguments.requestModel.isChatted ? Padding( padding: const EdgeInsets.only(top: 8, bottom: 8, right: 21), child: const Icon(Icons.messenger_outline_rounded, color: Colors.black, size: 18).toContainer( @@ -332,7 +332,7 @@ class RequestDetailPage extends StatelessWidget { allPading: 12, margin: const EdgeInsets.only(top: 21, right: 21, left: 21), ), - if (!context.read().isOfferSent && !requestDetailPageArguments.requestModel.isChatted) ...[ + if (!requestDetailPageArguments.requestModel.isChatted) ...[ ShowFillButton( maxWidth: double.infinity, margin: const EdgeInsets.all(15), diff --git a/lib/views/user/change_password_page.dart b/lib/views/user/change_password_page.dart index 7704885..f59debb 100644 --- a/lib/views/user/change_password_page.dart +++ b/lib/views/user/change_password_page.dart @@ -47,11 +47,13 @@ class _ChangePasswordPageState extends State { // 12.height, TxtField( hint: LocaleKeys.enterOldPassword.tr(), + isPasswordEnabled: true, onChanged: (v) => currentPassword = v, ), 12.height, TxtField( hint: LocaleKeys.enterNewPassword.tr(), + isPasswordEnabled: true, onChanged: (v) => newPassword = v, ), 20.height, diff --git a/lib/views/user/confirm_new_password_page.dart b/lib/views/user/confirm_new_password_page.dart index 94032a6..0ddf424 100644 --- a/lib/views/user/confirm_new_password_page.dart +++ b/lib/views/user/confirm_new_password_page.dart @@ -21,6 +21,8 @@ class ConfirmNewPasswordPage extends StatefulWidget { class _ConfirmNewPasswordPageState extends State { String newPassword = ""; String confirmPassword = ''; + bool _obscureNewPassword = true; + bool _obscureConfirmPassword = true; @override Widget build(BuildContext context) { @@ -42,7 +44,6 @@ class _ConfirmNewPasswordPageState extends State { 12.height, TextFormField( decoration: InputDecoration( - // hintText: "Enter New Password", hintText: LocaleKeys.enterNewPassword.tr(), hintStyle: const TextStyle(color: Colors.grey), border: const OutlineInputBorder( @@ -50,14 +51,24 @@ class _ConfirmNewPasswordPageState extends State { Radius.circular(5.0), ), ), + suffixIcon: IconButton( + icon: Icon( + _obscureNewPassword ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscureNewPassword = !_obscureNewPassword; + }); + }, + ), ), - obscureText: true, + obscureText: _obscureNewPassword, onChanged: (v) => newPassword = v, ), 12.height, TextFormField( decoration: InputDecoration( - // hintText: "Confirm Password", hintText: LocaleKeys.confirmPassword.tr(), hintStyle: const TextStyle(color: Colors.grey), border: const OutlineInputBorder( @@ -65,8 +76,19 @@ class _ConfirmNewPasswordPageState extends State { Radius.circular(5.0), ), ), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), ), - obscureText: true, + obscureText: _obscureConfirmPassword, onChanged: (v) => confirmPassword = v, ), 40.height, diff --git a/lib/widgets/common_widgets/filters_list.dart b/lib/widgets/common_widgets/filters_list.dart index e9bd1e0..19231cf 100644 --- a/lib/widgets/common_widgets/filters_list.dart +++ b/lib/widgets/common_widgets/filters_list.dart @@ -36,8 +36,6 @@ class FiltersList extends StatelessWidget { itemCount: filterList.length, scrollDirection: Axis.horizontal, itemBuilder: (BuildContext context, int index) { - print("=================================================="); - print(filterList[index].title); return InkWell( onTap: () { onFilterTapped(index, filterList[index].id); @@ -68,16 +66,12 @@ class FiltersList extends StatelessWidget { ], ], // Text displayed in the chip - (isArabic - ? (filterList[index].titleN ?? filterList[index].title) - : filterList[index].title) - .tr() - .toText( - // filterList[index].title.tr().toText( - fontSize: 13, - letterSpacing: -0.14, - color: filterList[index].isSelected ? MyColors.white : null, - ), + (isArabic ? (filterList[index].titleN ?? filterList[index].title) : filterList[index].title).toText( + // filterList[index].title.tr().toText( + fontSize: 13, + letterSpacing: -0.14, + color: filterList[index].isSelected ? MyColors.white : null, + ), ], ), ), diff --git a/lib/widgets/txt_field.dart b/lib/widgets/txt_field.dart index eddbc98..253a4e0 100644 --- a/lib/widgets/txt_field.dart +++ b/lib/widgets/txt_field.dart @@ -8,7 +8,7 @@ import 'package:mc_common_app/utils/utils.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:sizer/sizer.dart'; -class TxtField extends StatelessWidget { +class TxtField extends StatefulWidget { String? value; String? hint; String? lable; @@ -70,20 +70,26 @@ class TxtField extends StatelessWidget { this.isNeedLabelOnTop = true, }); + @override + State createState() => _TxtFieldState(); +} + +class _TxtFieldState extends State { TextEditingController controller = TextEditingController(); + bool _obscurePassword = true; @override Widget build(BuildContext context) { - controller.text = value ?? ""; + controller.text = widget.value ?? ""; controller.selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length)); return Column( children: [ - if (isNeedLabelOnTop && hint != null) ...[ + if (widget.isNeedLabelOnTop && widget.hint != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - (hint ?? "").toText( + (widget.hint ?? "").toText( color: borderColor, fontSize: 13, fontWeight: MyFonts.Medium, @@ -93,10 +99,10 @@ class TxtField extends StatelessWidget { 4.height, ], InkWell( - onTap: isNeedClickAll == false + onTap: widget.isNeedClickAll == false ? null : () { - onTap!(); + widget.onTap!(); }, customBorder: Utils.inkWellCorner(), child: Row( @@ -104,70 +110,80 @@ class TxtField extends StatelessWidget { Expanded( child: Container( decoration: BoxDecoration( - color: isBackgroundEnabled ? MyColors.textFieldColor : Colors.white, + color: widget.isBackgroundEnabled ? MyColors.textFieldColor : Colors.white, borderRadius: const BorderRadius.all(Radius.circular(0)), ), child: TextField( style: const TextStyle(color: MyColors.darkTextColor, fontSize: 15, fontWeight: MyFonts.Medium), - textInputAction: isSearchBar ? TextInputAction.search : null, - keyboardType: keyboardType, + textInputAction: widget.isSearchBar ? TextInputAction.search : null, + keyboardType: widget.keyboardType, autofocus: false, controller: controller, - enabled: isNeedClickAll == true ? false : true, - maxLines: maxLines, + enabled: widget.isNeedClickAll == true ? false : true, + maxLines: widget.maxLines, onTap: () {}, - obscureText: isPasswordEnabled ?? false, - onChanged: onChanged, - onSubmitted: onSubmitted, - inputFormatters: allowOnlyOneDigit && numbersOnly + obscureText: widget.isPasswordEnabled == true ? _obscurePassword : false, + onChanged: widget.onChanged, + onSubmitted: widget.onSubmitted, + inputFormatters: widget.allowOnlyOneDigit && widget.numbersOnly ? [FilteringTextInputFormatter.allow(RegExp('[0-9]')), LengthLimitingTextInputFormatter(1)] - : numbersOnly + : widget.numbersOnly ? [FilteringTextInputFormatter.allow(RegExp('[0-9]'))] : [], decoration: InputDecoration( - labelText: lable, + labelText: widget.lable, alignLabelWithHint: true, fillColor: Colors.white, focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: isNeedBorder ? 2.0 : 0), + borderSide: BorderSide(color: widget.isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: widget.isNeedBorder ? 2.0 : 0), borderRadius: BorderRadius.circular(0.0), ), enabledBorder: OutlineInputBorder( - // borderSide: BorderSide(color: MyColors.textFieldColor, width: isNeedBorder ? 1.0 : 0), - borderSide: BorderSide(color: isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: isNeedBorder ? 2.0 : 0), + borderSide: BorderSide(color: widget.isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: widget.isNeedBorder ? 2.0 : 0), borderRadius: BorderRadius.circular(0.0), ), disabledBorder: OutlineInputBorder( - // borderSide: BorderSide(color: MyColors.textFieldColor, width: isNeedBorder ? 1.0 : 0), - borderSide: BorderSide(color: isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: isNeedBorder ? 2.0 : 0), + borderSide: BorderSide(color: widget.isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: widget.isNeedBorder ? 2.0 : 0), borderRadius: BorderRadius.circular(0.0), ), - suffixIcon: postfixData != null - ? InkWell( - onTap: onPostFixPressed, - child: Icon(postfixData, color: postFixDataColor), + suffixIcon: widget.isPasswordEnabled == true + ? IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, ) - : postfixWidget, - prefixIcon: prefixData != null ? Icon(prefixData, color: borderColor) : preFixWidget, + : widget.postfixData != null + ? InkWell( + onTap: widget.onPostFixPressed, + child: Icon(widget.postfixData, color: widget.postFixDataColor), + ) + : widget.postfixWidget, + prefixIcon: widget.prefixData != null ? Icon(widget.prefixData, color: borderColor) : widget.preFixWidget, labelStyle: const TextStyle(color: borderColor, fontSize: 13, fontWeight: MyFonts.Medium), hintStyle: const TextStyle(color: borderColor, fontSize: 13, fontWeight: MyFonts.Medium), - hintText: isNeedLabelOnTop ? null : hint ?? "", - contentPadding: prefixData == null + hintText: widget.isNeedLabelOnTop ? null : widget.hint ?? "", + contentPadding: widget.prefixData == null ? EdgeInsets.only( left: 12, right: 12, - top: maxLines != null ? 12 : 0, - bottom: maxLines != null ? 12 : 0, + top: widget.maxLines != null ? 12 : 0, + bottom: widget.maxLines != null ? 12 : 0, ) : EdgeInsets.zero, ), ), ), ), - if (isNeedFilterButton) 8.width, - if (isNeedFilterButton) + if (widget.isNeedFilterButton) 8.width, + if (widget.isNeedFilterButton) InkWell( - onTap: isNeedClickAll + onTap: widget.isNeedClickAll ? null : () { controller.clear(); @@ -177,30 +193,26 @@ class TxtField extends StatelessWidget { height: 55, child: Card( color: accentColor, - // margin: EdgeInsets.all(4), - // shape: cardRadius(0), child: Icon( - postfixData ?? Icons.filter_alt, - color: postFixDataColor, + widget.postfixData ?? Icons.filter_alt, + color: widget.postFixDataColor, ), ), ), ), - if (isButtonEnable) + if (widget.isButtonEnable) Material( child: InkWell( - onTap: onTap, + onTap: widget.onTap, customBorder: Utils.inkWellCorner(), child: SizedBox( height: 55, child: Card( color: accentColor, - // margin: EdgeInsets.all(4), - // shape: cardRadius(0), child: Center( child: Padding( padding: const EdgeInsets.only(left: 12, right: 12), - child: (buttonTitle ?? "Search").toText( + child: (widget.buttonTitle ?? "Search").toText( color: Colors.white, fontSize: 16, letterSpacing: -0.64, @@ -214,11 +226,11 @@ class TxtField extends StatelessWidget { ], ), ), - if (errorValue != "") + if (widget.errorValue != "") Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - errorValue.toText(fontSize: 12, color: Colors.red), + widget.errorValue.toText(fontSize: 12, color: Colors.red), ], ).paddingOnly(right: 10), ], diff --git a/pubspec.lock b/pubspec.lock index 3d5446b..0f627d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -873,26 +873,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1342,10 +1342,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.11" timezone: dependency: transitive description: @@ -1539,5 +1539,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.35.0"