Compare commits

...

1 Commits

Author SHA1 Message Date
FaizHashmi 311157b0d7 Bug Fixes 5 days ago

@ -336,6 +336,8 @@
"vehiclePart": "جزء المركبة", "vehiclePart": "جزء المركبة",
"damagePartPictures": "صور الأجزاء المتضررة", "damagePartPictures": "صور الأجزاء المتضررة",
"qrCode": "رمز الاستجابة السريعة", "qrCode": "رمز الاستجابة السريعة",
"downgradeAppliedAfterExpiry": "سيتم تطبيق هذه الباقة تلقائيًا بعد انتهاء صلاحية باقتك الحالية.",
"downgradeDeactivateInstruction": "لتحديد هذا الاشتراك، يجب عليك إلغاء تفعيل الفروع والمستخدمين حيث أن هذه الباقة تدعم فقط {branches} و {users}. يمكنك إعادة تفعيل هذه الفروع لاحقًا بعد ترقية الباقة",
"vehicleDamagePart": "جزء المركبة المتضرر", "vehicleDamagePart": "جزء المركبة المتضرر",
"duration": "المدة", "duration": "المدة",
"specialServices": "الخدمات الخاصة", "specialServices": "الخدمات الخاصة",
@ -419,6 +421,7 @@
"daysVar": "يوم", "daysVar": "يوم",
"adCharges": "رسوم الإعلان", "adCharges": "رسوم الإعلان",
"select": "اختر", "select": "اختر",
"selectOption": "اختر خيار",
"deleteAdConfirmation": "هل تريد حذف الإعلان؟", "deleteAdConfirmation": "هل تريد حذف الإعلان؟",
"deleteAdConfirmationMessage": "سيتم حذف إعلانك نهائيًا ولا يمكنك التراجع عن هذا الإجراء", "deleteAdConfirmationMessage": "سيتم حذف إعلانك نهائيًا ولا يمكنك التراجع عن هذا الإجراء",
"mileage": "المسافة المقطوعة", "mileage": "المسافة المقطوعة",
@ -461,6 +464,8 @@
"bookAndPay": "الحجز والدفع", "bookAndPay": "الحجز والدفع",
"reservationAmounts": "مبالغ الحجز", "reservationAmounts": "مبالغ الحجز",
"tax": "الضريبة", "tax": "الضريبة",
"requester": "مقدم الطلب",
"mowaterAdvertisement": "شركة مواتر",
"adsFilter": "تصفية الإعلانات", "adsFilter": "تصفية الإعلانات",
"searchByCity": "البحث حسب المدينة", "searchByCity": "البحث حسب المدينة",
"selectCity": "اختر المدينة", "selectCity": "اختر المدينة",

@ -339,6 +339,8 @@
"vehiclePart": "Vehicle Part", "vehiclePart": "Vehicle Part",
"damagePartPictures": "Damage Part Pictures", "damagePartPictures": "Damage Part Pictures",
"qrCode": "QR Code", "qrCode": "QR Code",
"downgradeAppliedAfterExpiry": "This package will be applied automatically after your current package expires.",
"downgradeDeactivateInstruction": "To select this subscription you need to deactivate your branches and users as this package only supports {branches} and {users}. You can reactivate these branches later after upgrading the package",
"vehicleDamagePart": "Vehicle Damage Part", "vehicleDamagePart": "Vehicle Damage Part",
"duration": "Duration", "duration": "Duration",
"specialServices": "Special Services", "specialServices": "Special Services",
@ -423,6 +425,7 @@
"daysVar": "Days", "daysVar": "Days",
"adCharges": "Ad Charges", "adCharges": "Ad Charges",
"select": "Select", "select": "Select",
"selectOption": "Select Option",
"deleteAdConfirmation": "Do you want to delete the ad?", "deleteAdConfirmation": "Do you want to delete the ad?",
"deleteAdConfirmationMessage": "Your ad will be permanently deleted and you cannot undo this action", "deleteAdConfirmationMessage": "Your ad will be permanently deleted and you cannot undo this action",
"mileage": "Mileage", "mileage": "Mileage",
@ -465,6 +468,8 @@
"bookAndPay": "Book and Pay", "bookAndPay": "Book and Pay",
"reservationAmounts": "Reservation Amounts", "reservationAmounts": "Reservation Amounts",
"tax": "Tax", "tax": "Tax",
"requester": "Requester",
"mowaterAdvertisement": "Mowater Company",
"adsFilter": "Ads Filter", "adsFilter": "Ads Filter",
"searchByCity": "Search By City", "searchByCity": "Search By City",
"selectCity": "Select City", "selectCity": "Select City",

@ -87,6 +87,36 @@ class ApiClientImp implements ApiClient {
return 'en'; return 'en';
} }
// The "I/flutter (pid):" prefix on every line is added by Android's own logcat viewer
// when displaying a multi-line log message -- confirmed this happens regardless of how
// the app prints (debugPrint, plain print, single call or chunked), so it can't be
// avoided from here. Given that cleanup is unavoidable either way, use the shared
// `logger` (PrettyPrinter) for a readable structure instead of hand-rolled plain text.
// Gated by GlobalConsts.showApiResponseInLogs: off by default (URL + payload only,
// since response bodies are noisy and usually not needed), flip on to also see the
// response.
void _logApiCall({required String method, required String url, String? payload, String? response}) {
if (kReleaseMode) return;
logger.d("----- $method -----");
logger.i(url);
if (payload != null && payload.isNotEmpty) {
logger.d("--- Payload ---");
logger.i(_tryDecode(payload));
}
if (GlobalConsts.showApiResponseInLogs && response != null) {
logger.d("--- Response ---");
logger.i(_tryDecode(response));
}
}
dynamic _tryDecode(String raw) {
try {
return jsonDecode(raw);
} catch (_) {
return raw;
}
}
@override @override
Future<U> postJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, T jsonObject, Future<U> postJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, T jsonObject,
{String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async { {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
@ -154,14 +184,7 @@ class ApiClientImp implements ApiClient {
response = await _post(Uri.parse(url), body: requestBody, headers: headers0).timeout(const Duration(seconds: 100)); response = await _post(Uri.parse(url), body: requestBody, headers: headers0).timeout(const Duration(seconds: 100));
if (!kReleaseMode) { _logApiCall(method: 'POST', url: url, payload: requestBody, response: response.body);
logger.d("------URL------");
logger.i(url);
logger.d("------Payload------");
logger.i(jsonDecode(requestBody));
logger.d("------Response------");
logger.i(jsonDecode(response.body));
}
if (response.statusCode >= 200 && response.statusCode < 500) { if (response.statusCode >= 200 && response.statusCode < 500) {
var jsonData = jsonDecode(response.body); var jsonData = jsonDecode(response.body);
if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") { if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") {
@ -270,17 +293,9 @@ class ApiClientImp implements ApiClient {
if (isFirstCall) url = '$url?$queryString'; if (isFirstCall) url = '$url?$queryString';
} }
if (!kReleaseMode) {
log("---URL---");
log(url);
log("---Params---");
log("$queryParameters");
}
var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 120)); var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 120));
if (!kReleaseMode) { _logApiCall(method: 'GET', url: url, payload: queryParameters != null ? jsonEncode(queryParameters) : null, response: response.body);
logger.i(jsonDecode(response.body));
}
if (response.statusCode >= 200 && response.statusCode < 500) { if (response.statusCode >= 200 && response.statusCode < 500) {
var jsonData = jsonDecode(response.body); var jsonData = jsonDecode(response.body);
if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") { if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") {

@ -212,6 +212,11 @@ class ApiConsts {
} }
class GlobalConsts { class GlobalConsts {
// When false, API logs print only the request URL and payload. When true, the
// response body is also printed. Flip this on only while actively debugging a
// specific API issue -- flip back off afterwards, since response bodies are noisy.
static bool showApiResponseInLogs = false;
static String appNameCustomer = "MOWATER Customer"; static String appNameCustomer = "MOWATER Customer";
static String appNameProvider = "MOWATER Provider"; static String appNameProvider = "MOWATER Provider";

@ -966,7 +966,7 @@ extension RequestOfferStatusEnumExt on int {
} else if (this == 5) { } else if (this == 5) {
return RequestOfferStatusEnum.cancel; return RequestOfferStatusEnum.cancel;
} }
return RequestOfferStatusEnum.cancel; return RequestOfferStatusEnum.defaultStatus;
} }
} }

@ -352,6 +352,8 @@ class CodegenLoader extends AssetLoader{
"vehiclePart": "جزء المركبة", "vehiclePart": "جزء المركبة",
"damagePartPictures": "صور الأجزاء المتضررة", "damagePartPictures": "صور الأجزاء المتضررة",
"qrCode": "رمز الاستجابة السريعة", "qrCode": "رمز الاستجابة السريعة",
"downgradeAppliedAfterExpiry": "سيتم تطبيق هذه الباقة تلقائيًا بعد انتهاء صلاحية باقتك الحالية.",
"downgradeDeactivateInstruction": "لتحديد هذا الاشتراك، يجب عليك إلغاء تفعيل الفروع والمستخدمين حيث أن هذه الباقة تدعم فقط {branches} و {users}. يمكنك إعادة تفعيل هذه الفروع لاحقًا بعد ترقية الباقة",
"vehicleDamagePart": "جزء المركبة المتضرر", "vehicleDamagePart": "جزء المركبة المتضرر",
"duration": "المدة", "duration": "المدة",
"specialServices": "الخدمات الخاصة", "specialServices": "الخدمات الخاصة",
@ -435,6 +437,7 @@ class CodegenLoader extends AssetLoader{
"daysVar": "يوم", "daysVar": "يوم",
"adCharges": "رسوم الإعلان", "adCharges": "رسوم الإعلان",
"select": "اختر", "select": "اختر",
"selectOption": "اختر خيار",
"deleteAdConfirmation": "هل تريد حذف الإعلان؟", "deleteAdConfirmation": "هل تريد حذف الإعلان؟",
"deleteAdConfirmationMessage": "سيتم حذف إعلانك نهائيًا ولا يمكنك التراجع عن هذا الإجراء", "deleteAdConfirmationMessage": "سيتم حذف إعلانك نهائيًا ولا يمكنك التراجع عن هذا الإجراء",
"mileage": "المسافة المقطوعة", "mileage": "المسافة المقطوعة",
@ -477,6 +480,8 @@ class CodegenLoader extends AssetLoader{
"bookAndPay": "الحجز والدفع", "bookAndPay": "الحجز والدفع",
"reservationAmounts": "مبالغ الحجز", "reservationAmounts": "مبالغ الحجز",
"tax": "الضريبة", "tax": "الضريبة",
"requester": "مقدم الطلب",
"mowaterAdvertisement": "شركة مواتر",
"adsFilter": "تصفية الإعلانات", "adsFilter": "تصفية الإعلانات",
"searchByCity": "البحث حسب المدينة", "searchByCity": "البحث حسب المدينة",
"selectCity": "اختر المدينة", "selectCity": "اختر المدينة",
@ -1285,6 +1290,8 @@ static const Map<String,dynamic> _en_US = {
"vehiclePart": "Vehicle Part", "vehiclePart": "Vehicle Part",
"damagePartPictures": "Damage Part Pictures", "damagePartPictures": "Damage Part Pictures",
"qrCode": "QR Code", "qrCode": "QR Code",
"downgradeAppliedAfterExpiry": "This package will be applied automatically after your current package expires.",
"downgradeDeactivateInstruction": "To select this subscription you need to deactivate your branches and users as this package only supports {branches} and {users}. You can reactivate these branches later after upgrading the package",
"vehicleDamagePart": "Vehicle Damage Part", "vehicleDamagePart": "Vehicle Damage Part",
"duration": "Duration", "duration": "Duration",
"specialServices": "Special Services", "specialServices": "Special Services",
@ -1369,6 +1376,7 @@ static const Map<String,dynamic> _en_US = {
"daysVar": "Days", "daysVar": "Days",
"adCharges": "Ad Charges", "adCharges": "Ad Charges",
"select": "Select", "select": "Select",
"selectOption": "Select Option",
"deleteAdConfirmation": "Do you want to delete the ad?", "deleteAdConfirmation": "Do you want to delete the ad?",
"deleteAdConfirmationMessage": "Your ad will be permanently deleted and you cannot undo this action", "deleteAdConfirmationMessage": "Your ad will be permanently deleted and you cannot undo this action",
"mileage": "Mileage", "mileage": "Mileage",
@ -1411,6 +1419,8 @@ static const Map<String,dynamic> _en_US = {
"bookAndPay": "Book and Pay", "bookAndPay": "Book and Pay",
"reservationAmounts": "Reservation Amounts", "reservationAmounts": "Reservation Amounts",
"tax": "Tax", "tax": "Tax",
"requester": "Requester",
"mowaterAdvertisement": "Mowater Company",
"adsFilter": "Ads Filter", "adsFilter": "Ads Filter",
"searchByCity": "Search By City", "searchByCity": "Search By City",
"selectCity": "Select City", "selectCity": "Select City",

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names
abstract class LocaleKeys { abstract class LocaleKeys {
static const firstTimeLogIn = 'firstTimeLogIn'; static const firstTimeLogIn = 'firstTimeLogIn';
static const signUp = 'signUp'; static const signUp = 'signUp';
static const changeMobile = 'changeMobile'; static const changeMobile = 'changeMobile';
@ -50,7 +50,7 @@ abstract class LocaleKeys {
static const emailChangedSuccessfully = 'emailChangedSuccessfully'; static const emailChangedSuccessfully = 'emailChangedSuccessfully';
static const passwordIsUpdated = 'passwordIsUpdated'; static const passwordIsUpdated = 'passwordIsUpdated';
static const passwordShouldContains = 'passwordShouldContains'; static const passwordShouldContains = 'passwordShouldContains';
static const successfullyRegistered = ' successfullyRegistered'; static const successfullyRegistered = ' successfullyRegistered';
static const pleaseEnterSamePassword = 'pleaseEnterSamePassword'; static const pleaseEnterSamePassword = 'pleaseEnterSamePassword';
static const firstNameMandatory = 'firstNameMandatory'; static const firstNameMandatory = 'firstNameMandatory';
static const surnameNameMandatory = 'surnameNameMandatory'; static const surnameNameMandatory = 'surnameNameMandatory';
@ -318,6 +318,8 @@ abstract class LocaleKeys {
static const vehiclePart = 'vehiclePart'; static const vehiclePart = 'vehiclePart';
static const damagePartPictures = 'damagePartPictures'; static const damagePartPictures = 'damagePartPictures';
static const qrCode = 'qrCode'; static const qrCode = 'qrCode';
static const downgradeAppliedAfterExpiry = 'downgradeAppliedAfterExpiry';
static const downgradeDeactivateInstruction = 'downgradeDeactivateInstruction';
static const vehicleDamagePart = 'vehicleDamagePart'; static const vehicleDamagePart = 'vehicleDamagePart';
static const duration = 'duration'; static const duration = 'duration';
static const specialServices = 'specialServices'; static const specialServices = 'specialServices';
@ -400,6 +402,7 @@ abstract class LocaleKeys {
static const daysVar = 'daysVar'; static const daysVar = 'daysVar';
static const adCharges = 'adCharges'; static const adCharges = 'adCharges';
static const select = 'select'; static const select = 'select';
static const selectOption = 'selectOption';
static const deleteAdConfirmation = 'deleteAdConfirmation'; static const deleteAdConfirmation = 'deleteAdConfirmation';
static const deleteAdConfirmationMessage = 'deleteAdConfirmationMessage'; static const deleteAdConfirmationMessage = 'deleteAdConfirmationMessage';
static const mileage = 'mileage'; static const mileage = 'mileage';
@ -442,6 +445,8 @@ abstract class LocaleKeys {
static const bookAndPay = 'bookAndPay'; static const bookAndPay = 'bookAndPay';
static const reservationAmounts = 'reservationAmounts'; static const reservationAmounts = 'reservationAmounts';
static const tax = 'tax'; static const tax = 'tax';
static const requester = 'requester';
static const mowaterAdvertisement = 'mowaterAdvertisement';
static const adsFilter = 'adsFilter'; static const adsFilter = 'adsFilter';
static const searchByCity = 'searchByCity'; static const searchByCity = 'searchByCity';
static const selectCity = 'selectCity'; static const selectCity = 'selectCity';

@ -118,7 +118,7 @@ class Subscription {
subscriptionSubUsers: json["subscriptionSubUsers"], subscriptionSubUsers: json["subscriptionSubUsers"],
subscriptionAds: json["subscriptionAds"], subscriptionAds: json["subscriptionAds"],
totalBranches: json["totalBranches"], totalBranches: json["totalBranches"],
totalSubUsers: json["totalBranches"], totalSubUsers: json["totalSubUsers"],
totalAds: json["totalAds"], totalAds: json["totalAds"],
branchesRemaining: json["branchesRemaining"], branchesRemaining: json["branchesRemaining"],
subUsersRemaining: json["subUsersRemaining"], subUsersRemaining: json["subUsersRemaining"],

@ -483,7 +483,7 @@ class AdsRepoImp implements AdsRepo {
return await getAdsPerSpecificIds(ids: selectedIdsString, reservedAds: reservedAds); return await getAdsPerSpecificIds(ids: selectedIdsString, reservedAds: reservedAds);
} }
Future<List<AdDetailsModel>> getAdsPerSpecificIds({required List<String> ids, required List<MyReservedAdsRespModel> reservedAds}) async { Future<List<AdDetailsModel>> getAdsPerSpecificIds({required List<String> ids, required List<MyReservedAdsRespModel> reservedAds, bool applyReservationFilter = true}) async {
var params = { var params = {
"AdsIDs": ids, "AdsIDs": ids,
"isActive": "true", "isActive": "true",
@ -509,7 +509,7 @@ class AdsRepoImp implements AdsRepo {
statusMap[reserved.adsID ?? 0] = (reserved.adsReserveStatus ?? 0).toAdRserveStatusEnum(); statusMap[reserved.adsID ?? 0] = (reserved.adsReserveStatus ?? 0).toAdRserveStatusEnum();
reserveIdMap[reserved.adsID ?? 0] = reserved.id ?? 0; // Store reserve ID reserveIdMap[reserved.adsID ?? 0] = reserved.id ?? 0; // Store reserve ID
} }
log("📋 Status Map created: ${statusMap.length} entries"); log("📋 Status Map created: ${statusMap.length} entries");
statusMap.forEach((key, value) { statusMap.forEach((key, value) {
@ -531,15 +531,20 @@ class AdsRepoImp implements AdsRepo {
} }
} }
// Show all ads with ANY reservation history (exclude only defaultStatus/none) if (applyReservationFilter) {
// This includes: reserved (1), cancelledByOwner (2), cancelledByAdmin (3), timeOver (4), dealDone (5), fullPaymentVerified (6) // Show all ads with ANY reservation history (exclude only defaultStatus/none)
int beforeFilter = vehicleAdsDetails.length; // This includes: reserved (1), cancelledByOwner (2), cancelledByAdmin (3), timeOver (4), dealDone (5), fullPaymentVerified (6)
vehicleAdsDetails.removeWhere((element) => element.adReserveStatus == AdReserveStatus.defaultStatus); // Only meaningful for the "My Reserved Ads" list — callers fetching a specific
int afterFilter = vehicleAdsDetails.length; // ad by id (e.g. deep links) should NOT filter, or every ad with no reservation
// history (the normal case) gets incorrectly dropped.
log("🎯 Filter Result: ${afterFilter} ads remaining (removed ${beforeFilter - afterFilter})"); int beforeFilter = vehicleAdsDetails.length;
for (var ad in vehicleAdsDetails) { vehicleAdsDetails.removeWhere((element) => element.adReserveStatus == AdReserveStatus.defaultStatus);
log(" ✅ Keeping Ad ${ad.id} with status ${ad.adReserveStatus}"); 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}");
}
} }
return vehicleAdsDetails; return vehicleAdsDetails;
@ -630,9 +635,15 @@ class AdsRepoImp implements AdsRepo {
@override @override
Future<AdDetailsModel?> getAdDetailsById({required int adId}) async { Future<AdDetailsModel?> getAdDetailsById({required int adId}) async {
try { try {
List<AdDetailsModel> ads = await getAdsPerSpecificIds(ids: [adId.toString()], reservedAds: []); List<AdDetailsModel> ads = await getAdsPerSpecificIds(ids: [adId.toString()], reservedAds: [], applyReservationFilter: false);
if (ads.isEmpty) return null; if (ads.isEmpty) return null;
return ads.first; AdDetailsModel ad = ads.first;
// getAdsPerSpecificIds hardcodes isMyAd=true, which is only correct for its
// original "My Reserved Ads" use case. This method can fetch ANY ad (e.g. from a
// shared deep link), so recompute isMyAd from actual ownership instead.
final String? currentUserId = appState.getUser.data?.userInfo?.userId;
ad.isMyAd = currentUserId != null && ad.userID == currentUserId;
return ad;
} catch (e) { } catch (e) {
logger.e(e.toString()); logger.e(e.toString());
return null; return null;

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart';
import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/utils/navigator.dart'; import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart';
import 'package:mc_common_app/utils/utils.dart'; import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/ad_view_model.dart'; import 'package:mc_common_app/view_models/ad_view_model.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -18,27 +18,54 @@ import 'package:provider/provider.dart';
class AdDeepLinkService { class AdDeepLinkService {
static final AppLinks _appLinks = AppLinks(); static final AppLinks _appLinks = AppLinks();
static StreamSubscription<Uri>? _subscription; static StreamSubscription<Uri>? _subscription;
static Uri? _lastHandledUri;
// Route name given to the inert, self-removing placeholder pushed from main.dart's
// onUnknownRoute for Android's native deep-link route forwarding (see that file for
// why it exists). It's pushed and removed again within a frame, so the route-settle
// polling below must ignore it rather than treat its brief presence/absence as a real
// navigation change -- otherwise the route name never reads as "stable" and _openAd
// gives up without ever opening the ad.
static const String ignoredPlaceholderRouteName = '__ad_deep_link_ignore__';
static String? _currentTopRouteName;
/// Register via MaterialApp(navigatorObservers: [AdDeepLinkService.routeObserver]).
/// This is the only reliable way to know the current top route name from here --
/// confirmed on a real device: ModalRoute.of(navigatorKey.currentContext) does NOT
/// work for this. navigatorKey.currentContext is the Navigator widget's OWN element,
/// which sits ABOVE all its routes in the tree, so it has no ModalRoute ancestor and
/// that call returns null essentially always. That silently made every route-settle
/// poll below read null forever, so _openAd always timed out and gave up without ever
/// opening the ad, regardless of login state or what screen was actually showing.
static final NavigatorObserver routeObserver = _RouteNameTracker();
/// Call once, after the app's first frame (so [navigatorKey] is attached to a Navigator). /// Call once, after the app's first frame (so [navigatorKey] is attached to a Navigator).
static Future<void> init() async { static Future<void> init() async {
debugPrint("AdDeepLinkService: init() called");
try { try {
final Uri? initialUri = await _appLinks.getInitialLink(); final Uri? initialUri = await _appLinks.getInitialLink();
debugPrint("AdDeepLinkService: getInitialLink() -> $initialUri");
if (initialUri != null) { if (initialUri != null) {
_handleUri(initialUri); _handleUri(initialUri);
} }
} catch (e) { } catch (e) {
log("AdDeepLinkService: failed to read initial link - $e"); debugPrint("AdDeepLinkService: failed to read initial link - $e");
} }
_subscription?.cancel(); _subscription?.cancel();
_subscription = _appLinks.uriLinkStream.listen( _subscription = _appLinks.uriLinkStream.listen(
_handleUri, (uri) {
onError: (Object e) => log("AdDeepLinkService: stream error - $e"), debugPrint("AdDeepLinkService: uriLinkStream emitted $uri");
_handleUri(uri);
},
onError: (Object e) => debugPrint("AdDeepLinkService: stream error - $e"),
); );
debugPrint("AdDeepLinkService: uriLinkStream subscription active");
} }
static void _handleUri(Uri uri) { static void _handleUri(Uri uri) {
log("AdDeepLinkService: received $uri"); debugPrint("AdDeepLinkService: received $uri");
// https://ms.hmg.com/ad/1541 -> host="ms.hmg.com", pathSegments=["ad", "1541"] // https://ms.hmg.com/ad/1541 -> host="ms.hmg.com", pathSegments=["ad", "1541"]
// mowatercustomer://ad/1541 -> host="ad" (Dart treats it as the authority), pathSegments=["1541"] // mowatercustomer://ad/1541 -> host="ad" (Dart treats it as the authority), pathSegments=["1541"]
@ -53,22 +80,127 @@ class AdDeepLinkService {
} }
if (adId == null) return; if (adId == null) return;
// app_links can emit the same initial uri twice (once via getInitialLink, once as
// the stream's first event) -- skip the duplicate rather than run _openAd twice.
if (uri == _lastHandledUri) {
debugPrint("AdDeepLinkService: ignoring duplicate $uri");
return;
}
_lastHandledUri = uri;
_openAd(adId); _openAd(adId);
} }
// Deliberately no loading dialog anywhere in here: on a cold start (app was killed),
// this races the app's own bootstrap navigation -- confirmed on a real device, the
// bootstrap chain is SplashPage's hardcoded 5s timer -> pushReplacementNamed to
// registerSelection -> (if a saved login exists) a silent auto-login with its own
// showLoading/hideLoading dialog -> pushReplacementNamed again towards the dashboard.
// Every one of those calls uses a *captured* BuildContext and mutates the same
// Navigator via Navigator.of(context), regardless of what else has since been pushed
// on top. If we push the ad route while any of that is still in flight, our route gets
// clobbered by the next replacement -- in debug this throws "_elements.contains
// (element)"; in release (asserts stripped) it silently wedges the Navigator and the
// app hangs on a black screen. So: wait for the current route name to stay the same
// across two consecutive polls (i.e. bootstrap has actually settled) before touching
// the Navigator or fetching anything.
static Future<void> _openAd(int adId) async { static Future<void> _openAd(int adId) async {
final context = navigatorKey.currentContext; debugPrint("AdDeepLinkService: _openAd($adId) called");
if (context == null) return; if (!await _waitUntilRouteSettles()) {
debugPrint("AdDeepLinkService: app never settled past bootstrap, aborting");
return;
}
debugPrint("AdDeepLinkService: route settled at '$_currentTopRouteName'");
Utils.showLoading(context); // Checking for a saved token here is NOT a reliable "is the user ready" signal --
final adDetails = await context.read<AdVM>().getAdDetailsById(adId); // confirmed on a real device: a stale token from before the app was killed survives
Utils.hideLoading(context); // on disk through the whole re-login flow (splash -> registerSelection -> silent
// relogin -> loginMethodSelection OTP screen), so it reads as "already logged in"
// while still sitting on the OTP screen, fetches too early with no real session
// ready yet, and silently fails. The dashboard route is the one signal that's only
// true once a real session is actually in place, however the user got there.
if (_currentTopRouteName != AppRoutes.dashboard) {
debugPrint("AdDeepLinkService: not on dashboard yet ('$_currentTopRouteName'), waiting for login to finish");
if (!await _waitForRoute(AppRoutes.dashboard)) {
debugPrint("AdDeepLinkService: gave up waiting for dashboard, aborting");
return;
}
debugPrint("AdDeepLinkService: reached dashboard");
} else {
debugPrint("AdDeepLinkService: already on dashboard, proceeding directly");
}
debugPrint("AdDeepLinkService: fetching ad $adId");
AdDetailsModel? adDetails;
try {
adDetails = await navigatorKey.currentContext?.read<AdVM>().getAdDetailsById(adId);
debugPrint("AdDeepLinkService: fetch result adDetails=${adDetails != null ? 'FOUND id=${adDetails.id}' : 'NULL'}");
} catch (e) {
debugPrint("AdDeepLinkService: failed to fetch ad $adId - $e");
}
if (adDetails == null) { if (adDetails == null) {
Utils.showToast(LocaleKeys.somethingWrong.tr()); Utils.showToast(LocaleKeys.somethingWrong.tr());
return; return;
} }
navigateWithName(context, AppRoutes.adsDetailView, arguments: adDetails); // Navigate via the NavigatorState directly (not a captured BuildContext) so this
// still works even if the route/context active when we started has since changed.
debugPrint("AdDeepLinkService: pushing ${AppRoutes.adsDetailView}");
navigatorKey.currentState?.pushNamed(AppRoutes.adsDetailView, arguments: adDetails);
}
static String? _currentRouteNameIgnoringPlaceholder() {
final name = _currentTopRouteName;
// The placeholder pops itself a frame after being pushed -- if this poll happens to
// land exactly then, pretend nothing was read rather than counting it as a route.
return name == ignoredPlaceholderRouteName ? null : name;
}
static Future<bool> _waitUntilRouteSettles({int maxAttempts = 60, Duration retryDelay = const Duration(milliseconds: 300)}) async {
String? lastRouteName;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
final currentRouteName = _currentRouteNameIgnoringPlaceholder();
if (currentRouteName != null && currentRouteName != AppRoutes.splash && currentRouteName == lastRouteName) {
return true;
}
if (currentRouteName != null) lastRouteName = currentRouteName;
if (attempt < maxAttempts) await Future.delayed(retryDelay);
}
return false;
}
// Used both for the short "did bootstrap settle" check and for waiting on the
// dashboard specifically, which can be user-paced (entering an OTP, picking a login
// method) -- default timeout is generous (5 minutes) to cover that case.
static Future<bool> _waitForRoute(String targetRoute, {int maxAttempts = 600, Duration retryDelay = const Duration(milliseconds: 500)}) async {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
if (_currentRouteNameIgnoringPlaceholder() == targetRoute) return true;
if (attempt < maxAttempts) await Future.delayed(retryDelay);
}
return false;
}
}
class _RouteNameTracker extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
AdDeepLinkService._currentTopRouteName = route.settings.name;
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
AdDeepLinkService._currentTopRouteName = previousRoute?.settings.name;
}
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
AdDeepLinkService._currentTopRouteName = previousRoute?.settings.name;
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
AdDeepLinkService._currentTopRouteName = newRoute?.settings.name;
} }
} }

@ -12,6 +12,7 @@ import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/main.dart';
import 'package:mc_common_app/services/chat_navigation_service.dart'; import 'package:mc_common_app/services/chat_navigation_service.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/navigator.dart'; import 'package:mc_common_app/utils/navigator.dart';
import 'package:mc_common_app/view_models/chat_view_model.dart'; import 'package:mc_common_app/view_models/chat_view_model.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -270,8 +271,17 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
return; return;
} }
log("✅ Navigating to providers chat list for request: $requestId"); if (AppState().currentAppType == AppType.customer) {
navigateWithName(context, AppRoutes.providersChatListPage); // Customer notification tap: land on this request's offers list, matching
// the normal (non-notification) customer flow in requests_fragment.dart.
// ProvidersChatListPage below is provider-only (it reads the current
// user's own providerId), so routing a customer there fails.
log("✅ Navigating to offers list for request: $requestId");
navigateWithName(context, AppRoutes.offersListPage, arguments: requestId);
} else {
log("✅ Navigating to providers chat list for request: $requestId");
navigateWithName(context, AppRoutes.providersChatListPage);
}
// TODO: If backend sends all required data (ProviderId, CustomerId), // TODO: If backend sends all required data (ProviderId, CustomerId),
// we can navigate directly to the chat screen // we can navigate directly to the chat screen
@ -287,11 +297,33 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
try { try {
// Try both backend format and our format // Try both backend format and our format
int adId = int.tryParse(data['AdID']?.toString() ?? data['ad_id']?.toString() ?? '0') ?? 0; int adId = int.tryParse(data['AdID']?.toString() ?? data['ad_id']?.toString() ?? '0') ?? 0;
String? receiverUserId = data['ReceiverUserID'] ?? data['receiver_id'];
// The "other participant" for the chat we're about to open/keep messaging in is
// whoever SENT the message that triggered this notification — NOT whatever the
// payload calls the "receiver". Backend has repeatedly mixed up sender/receiver
// fields in this payload (see firebase_notification_payload_format.md); if we
// blindly trust a "receiver" field that turns out to be OUR OWN id, every message
// we send afterwards gets addressed back to ourselves, and we start getting
// pushed notifications for our own messages. Prefer sender_id; fall back to the
// old receiver fields only if sender fields are missing entirely.
String? receiverUserId = data['SenderUserID'] ?? data['sender_id'] ?? data['ReceiverUserID'] ?? data['receiver_id'];
log("📦 Ad chat notification payload fields — "
"AdID: ${data['AdID']}, ad_id: ${data['ad_id']}, "
"SenderUserID: ${data['SenderUserID']}, sender_id: ${data['sender_id']}, "
"ReceiverUserID: ${data['ReceiverUserID']}, receiver_id: ${data['receiver_id']} "
"-> resolved receiverUserId: $receiverUserId");
final String? myUserId = AppState().getUser.data?.userInfo?.userId?.toString();
if (myUserId != null && receiverUserId == myUserId) {
log("⚠️ Resolved receiverUserId ($receiverUserId) matches the current user's own id — "
"the notification payload is likely still misidentifying sender/receiver. "
"Proceeding anyway, but messages sent from this chat may misroute.");
}
if (adId == 0 || receiverUserId == null) { if (adId == 0 || receiverUserId == null) {
log("❌ Invalid AdID or ReceiverUserID in notification data"); log("❌ Invalid AdID or sender/receiver id in notification data");
log(" AdID: ${data['AdID']}, ReceiverUserID: ${data['ReceiverUserID']}"); log(" AdID: ${data['AdID']}, resolved receiverUserId: $receiverUserId");
return; return;
} }

@ -211,6 +211,7 @@ enum RequestOfferStatusEnum {
accepted, accepted,
rejected, rejected,
cancel, cancel,
defaultStatus,
} }
enum ChatTypeEnum { enum ChatTypeEnum {

@ -16,9 +16,11 @@ import 'package:mc_common_app/widgets/common_widgets/info_bottom_sheet.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/exceptions/api_exception.dart'; import 'package:mc_common_app/exceptions/api_exception.dart';
import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart';
import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/enums.dart'; import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/widgets/loading_dialog.dart'; import 'package:mc_common_app/widgets/loading_dialog.dart';
@ -32,6 +34,16 @@ class Utils {
static bool get isLoading => _isLoadingVisible; static bool get isLoading => _isLoadingVisible;
/// Dynamic ad link - opens directly in the app if installed, otherwise falls back
/// to the store via the hosted web page for this path. The "app" query param tells
/// that page whether to target the customer or provider app, since both apps support
/// ad sharing. Used by both the Share button and the QR code sheet, so they always
/// point to the exact same destination.
static String buildAdShareLink(AdDetailsModel adDetails) {
final String appParam = AppState().currentAppType == AppType.customer ? 'customer' : 'provider';
return 'https://ms.hmg.com/ad/${adDetails.id}?app=$appParam';
}
static void showToast(String message) { static void showToast(String message) {
toastification.show( toastification.show(
type: ToastificationType.info, type: ToastificationType.info,
@ -346,6 +358,9 @@ class Utils {
static Color getChipColorByRequestOfferStatusEnum(RequestOfferStatusEnum requestOfferStatusEnum) { static Color getChipColorByRequestOfferStatusEnum(RequestOfferStatusEnum requestOfferStatusEnum) {
switch (requestOfferStatusEnum) { switch (requestOfferStatusEnum) {
case RequestOfferStatusEnum.defaultStatus:
return MyColors.lightTextColor;
case RequestOfferStatusEnum.offer: case RequestOfferStatusEnum.offer:
return MyColors.pendingColor; return MyColors.pendingColor;
@ -364,6 +379,8 @@ class Utils {
static String getNameByRequestOfferStatusEnum(RequestOfferStatusEnum requestOfferStatusEnum) { static String getNameByRequestOfferStatusEnum(RequestOfferStatusEnum requestOfferStatusEnum) {
switch (requestOfferStatusEnum) { switch (requestOfferStatusEnum) {
case RequestOfferStatusEnum.defaultStatus:
return "";
case RequestOfferStatusEnum.offer: case RequestOfferStatusEnum.offer:
return LocaleKeys.offer.tr(); return LocaleKeys.offer.tr();
case RequestOfferStatusEnum.negotiate: case RequestOfferStatusEnum.negotiate:
@ -528,6 +545,9 @@ class Utils {
Color chipColor = MyColors.greenColor, Color chipColor = MyColors.greenColor,
Color textColor = MyColors.white, Color textColor = MyColors.white,
}) { }) {
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: chipColor, color: chipColor,

@ -996,14 +996,14 @@ class ChatVM extends BaseVM {
Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async { Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async {
final myUserId = AppState().getUser.data?.userInfo?.userId?.toString(); final myUserId = AppState().getUser.data?.userInfo?.userId?.toString();
for (var msg in messages) { for (var msg in messages) {
// Skip if this is my own message echoed back from server // Skip if this is my own message echoed back from server
if (myUserId != null && msg.senderUserID == myUserId) { if (myUserId != null && msg.senderUserID == myUserId) {
log("Skipping own message echoed from server (sender: ${msg.senderUserID})"); log("Skipping own message echoed from server (sender: ${msg.senderUserID})");
continue; continue;
} }
// Check if message already exists to prevent duplicates // Check if message already exists to prevent duplicates
bool messageExists = false; bool messageExists = false;
@ -1061,13 +1061,13 @@ class ChatVM extends BaseVM {
notifyListeners(); notifyListeners();
return false; return false;
} }
// Extra verification: ensure connection ID is available // Extra verification: ensure connection ID is available
if (hubConnection!.connectionId == null) { if (hubConnection!.connectionId == null) {
logger.w("Connection ID not yet available, waiting a bit more..."); logger.w("Connection ID not yet available, waiting a bit more...");
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
} }
logger.i("Connection ready! ConnectionId: ${hubConnection!.connectionId}"); logger.i("Connection ready! ConnectionId: ${hubConnection!.connectionId}");
} }
@ -1076,7 +1076,7 @@ class ChatVM extends BaseVM {
final name = AppState().getUser.data!.userInfo!.firstName.toString(); final name = AppState().getUser.data!.userInfo!.firstName.toString();
final obj = <String, dynamic>{ final obj = <String, dynamic>{
"ReceiverUserID": receiverId, "ReceiverUserID": receiverId,
"SenderUserID": userId, // ✅ Send SenderUserID so backend knows who sent it "SenderUserID": userId, // ✅ Send SenderUserID so backend knows who sent it
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message, "ChatText": message,
"AdsID": adId, "AdsID": adId,

@ -93,7 +93,6 @@ class _AdsDetailViewState extends State<AdsDetailView> {
Widget buildVehicleDetailsWidget() { Widget buildVehicleDetailsWidget() {
bool isArabic = AppState().isArabic(context); bool isArabic = AppState().isArabic(context);
print("====================== ${widget.adDetails.vehicle!.currency }");
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -205,11 +204,12 @@ class _AdsDetailViewState extends State<AdsDetailView> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
widget.adDetails.vehicle!.demandAmount!.toInt().toString().toText(fontSize: 30, height: 1.2, isBold: true), widget.adDetails.vehicle!.demandAmount!.toInt().toString().toText(fontSize: 30, height: 1.2, isBold: true),
// (widget.adDetails.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(fontSize: 15, color: MyColors.lightTextColor).paddingOnly(bottom: 5), // (widget.adDetails.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(fontSize: 15, color: MyColors.lightTextColor).paddingOnly(bottom: 5),
( (isArabic ? widget.adDetails.vehicle!.currencyN : widget.adDetails.vehicle!.currency ) ?? '').toText( ((isArabic ? widget.adDetails.vehicle!.currencyN : widget.adDetails.vehicle!.currency) ?? '')
fontSize: 15, .toText(
color: MyColors.lightTextColor, fontSize: 15,
) color: MyColors.lightTextColor,
)
.paddingOnly(bottom: 5), .paddingOnly(bottom: 5),
], ],
), ),
@ -745,12 +745,7 @@ class ShareAdButton extends StatelessWidget {
} }
shareMessage += '\n'; shareMessage += '\n';
// Dynamic ad link - opens directly in the app if installed, otherwise falls shareMessage += '${Utils.buildAdShareLink(adDetails)}\n';
// back to the store via the hosted web page for this path. The "app" query
// param tells that page whether to target the customer or provider app,
// since both apps support ad sharing.
final String appParam = AppState().currentAppType == AppType.customer ? 'customer' : 'provider';
shareMessage += 'https://ms.hmg.com/ad/${adDetails.id}?app=$appParam\n';
// Share using share_plus // Share using share_plus
Share.share(shareMessage); Share.share(shareMessage);

@ -41,11 +41,26 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget {
isScrollControlled: true, isScrollControlled: true,
enableDrag: true, enableDrag: true,
builder: (BuildContext context) { builder: (BuildContext context) {
double carPrice = adDetailsModel.vehicle!.demandAmount ?? 0.0;
double specialServicesTotal = (adDetailsModel.specialservice ?? []).fold<double>(0.0, (sum, service) => sum + (service.price ?? 0.0));
double taxPrice = adDetailsModel.taxPrice ?? 0.0;
double reservePrice = adDetailsModel.reservePrice ?? 0.0;
double remainingAmount = carPrice + specialServicesTotal + taxPrice;
double totalAmount = reservePrice + remainingAmount;
return InfoBottomSheet( return InfoBottomSheet(
title: LocaleKeys.reserveAd.tr().toText(fontSize: 24, isBold: true), title: LocaleKeys.reserveAd.tr().toText(fontSize: 24, isBold: true),
description: Column( description: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Every ad reaching this sheet is admin-created (Reserve Ad is only
// offered for admin/Mowater-owned ads), so this is always accurate.
Row(
children: [
"${LocaleKeys.requester.tr()}: ".toText(fontSize: 14, color: MyColors.lightTextColor),
LocaleKeys.mowaterAdvertisement.tr().toText(fontSize: 14, isBold: true),
],
),
12.height,
// Show instructions if available // Show instructions if available
if (adDetailsModel.reserveCustomerMessage != null && adDetailsModel.reserveCustomerMessage!.isNotEmpty) ...[ if (adDetailsModel.reserveCustomerMessage != null && adDetailsModel.reserveCustomerMessage!.isNotEmpty) ...[
Container( Container(
@ -77,7 +92,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
"${adDetailsModel.reservePrice}".toText(fontSize: 19, isBold: true), "$reservePrice".toText(fontSize: 19, isBold: true),
2.width, 2.width,
(adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 3), (adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 3),
], ],
@ -85,57 +100,61 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget {
], ],
), ),
const Divider(), const Divider(),
LocaleKeys.belowAmountPayLater.tr().toText(fontSize: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
LocaleKeys.carPrice.tr().toText(fontSize: 16, isBold: true), LocaleKeys.belowAmountPayLater.tr().toText(fontSize: 14, isBold: true),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
"${adDetailsModel.vehicle!.demandAmount ?? 0.0}".toText(fontSize: 19, isBold: true), "$remainingAmount".toText(fontSize: 19, isBold: true),
2.width, 2.width,
(adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 3), (adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 3),
], ],
) )
], ],
), ),
Row( 8.height,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
LocaleKeys.vatExcluded.tr().toText(fontSize: 10, isBold: true),
],
),
const Divider(),
LocaleKeys.specialService.tr().toText(fontSize: 16, isBold: true),
5.height,
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
LocaleKeys.carInsuranceService.tr().toText(fontSize: 14, color: MyColors.lightTextColor, fontWeight: FontWeight.w500), LocaleKeys.carPrice.tr().toText(fontSize: 16, isBold: true),
LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
"$carPrice".toText(fontSize: 19, isBold: true),
2.width,
(adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 3),
],
)
], ],
), ),
5.height, if (adDetailsModel.specialservice != null && adDetailsModel.specialservice!.isNotEmpty) ...[
Row( const Divider(),
mainAxisAlignment: MainAxisAlignment.spaceBetween, LocaleKeys.specialService.tr().toText(fontSize: 16, isBold: true),
children: [ 5.height,
LocaleKeys.registrationCarPlates.tr().toText(fontSize: 14, color: MyColors.lightTextColor), for (var service in adDetailsModel.specialservice!) ...[
LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: (service.name ?? "").toText(fontSize: 14, color: MyColors.lightTextColor, fontWeight: FontWeight.w500)),
"${service.price ?? 0.0} ${adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()}".toText(fontSize: 12, isBold: true),
],
),
5.height,
], ],
), ],
5.height, 12.height,
LocaleKeys.specialServicechargesInsuranceDeliveryLocation.tr().toText(fontSize: 12, maxLines: 2),
12.height,
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
LocaleKeys.homeDeliveryService.tr().toText(fontSize: 14, color: MyColors.lightTextColor), LocaleKeys.tax.tr().toText(fontSize: 14, color: MyColors.lightTextColor, fontWeight: FontWeight.w500),
LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), "$taxPrice ${adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()}".toText(fontSize: 12, isBold: true),
], ],
), ),
12.height,
LocaleKeys.specialServicechargesInsuranceDeliveryLocation.tr().toText(fontSize: 12, maxLines: 2),
30.height, 30.height,
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -144,7 +163,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
"${(adDetailsModel.vehicle!.demandAmount ?? 0.0)}".toText(fontSize: 19, isBold: true), "$totalAmount".toText(fontSize: 19, isBold: true),
2.width, 2.width,
(adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 3), (adDetailsModel.vehicle!.currency ?? LocaleKeys.sar.tr()).toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 3),
], ],

@ -1,13 +1,12 @@
import 'dart:developer';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.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/generated/locale_keys.g.dart';
import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart'; import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart';
import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:qr_flutter/qr_flutter.dart';
class AdQrCodeSheet extends StatelessWidget { class AdQrCodeSheet extends StatelessWidget {
final AdDetailsModel adDetails; final AdDetailsModel adDetails;
@ -16,7 +15,9 @@ class AdQrCodeSheet extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
log("dDetails.qrCodePath: ${adDetails.qrCodePath}"); // Encode the exact same link the Share button sends, so scanning this QR from
// another phone lands on the same dynamic link and opens the app directly.
final String shareLink = Utils.buildAdShareLink(adDetails);
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: Column( child: Column(
@ -28,15 +29,10 @@ class AdQrCodeSheet extends StatelessWidget {
], ],
), ),
20.height, 20.height,
SizedBox( QrImageView(
height: 220, data: shareLink,
width: 220, size: 220,
child: CachedNetworkImage( padding: EdgeInsets.zero,
imageUrl: "${adDetails.qrCodePath}",
placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) => const Icon(Icons.error),
fit: BoxFit.contain,
),
), ),
20.height, 20.height,
], ],

@ -15,6 +15,7 @@ import 'package:mc_common_app/view_models/dashboard_view_model_provider.dart';
import 'package:mc_common_app/view_models/payment_view_model.dart'; import 'package:mc_common_app/view_models/payment_view_model.dart';
import 'package:mc_common_app/view_models/requests_view_model.dart'; import 'package:mc_common_app/view_models/requests_view_model.dart';
import 'package:mc_common_app/views/advertisement/components/picked_images_container_widget.dart'; import 'package:mc_common_app/views/advertisement/components/picked_images_container_widget.dart';
import 'package:mc_common_app/widgets/bottom_sheet.dart';
import 'package:mc_common_app/views/chat/widgets/chat_bottom_sheets.dart'; import 'package:mc_common_app/views/chat/widgets/chat_bottom_sheets.dart';
import 'package:mc_common_app/views/chat/widgets/chat_message_widget.dart'; import 'package:mc_common_app/views/chat/widgets/chat_message_widget.dart';
import 'package:mc_common_app/views/requests/request_bottomsheets.dart'; import 'package:mc_common_app/views/requests/request_bottomsheets.dart';
@ -123,6 +124,102 @@ class _ChatViewState extends State<ChatView> {
return status; return status;
} }
void _openAttachmentOptions(BuildContext context, ChatVM chatVM, RequestsVM requestVM) {
final bool canProvideOffer =
AppState().currentAppType == AppType.provider && chatTypeEnum == ChatTypeEnum.requestOffer && requestVM.currentSelectedRequest!.requestStatus == RequestStatusEnum.submitted;
showMyBottomSheet(
context,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21, vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.selectOption.tr().toText(fontSize: 18, isBold: true),
12.height,
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
if (canProvideOffer)
_buildAttachmentOptionTile(
icon: Icons.local_offer_rounded,
label: LocaleKeys.provideOffer.tr(),
backgroundColor: MyColors.darkPrimaryColor,
iconColor: MyColors.white,
onTap: () {
Navigator.pop(context);
requestVM.resetSendOfferBottomSheet();
RequestDetailPageArguments requestDetailArguments = RequestDetailPageArguments(
requestIndex: chatViewArgumentsForRequest!.requestIndex,
requestModel: chatViewArgumentsForRequest!.requestModel!,
);
context.read<DashboardVMProvider>().checkUserSubscription(SubscriptionActionTypeEnum.subscription, context, callback: () {
buildSendOfferBottomSheet(
context: context,
requestDetailPageArguments: requestDetailArguments,
isFromChatScreen: true,
offerId: null, // null means creating new offer
);
});
},
),
_buildAttachmentOptionTile(
icon: Icons.photo_library_rounded,
label: LocaleKeys.attachImage.tr(),
backgroundColor: MyColors.darkPrimaryColor,
iconColor: MyColors.white,
onTap: () {
Navigator.pop(context);
chatVM.pickMultipleImages();
},
),
],
),
],
),
),
);
}
Widget _buildAttachmentOptionTile({
required IconData icon,
required String label,
required VoidCallback onTap,
Color backgroundColor = MyColors.greyButtonColor,
Color iconColor = MyColors.darkPrimaryColor,
}) {
return InkWell(
onTap: onTap,
child: Container(
margin: const EdgeInsets.all(4),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
decoration: BoxDecoration(
border: Border.all(color: MyColors.darkPrimaryColor, width: 1),
borderRadius: BorderRadius.zero,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 50,
width: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: backgroundColor,
),
child: Icon(icon, color: iconColor, size: 24),
),
6.height,
label.toText(fontSize: 12, textAlign: TextAlign.center, maxLines: 2),
],
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget? appBarHeadlines; Widget? appBarHeadlines;
@ -205,22 +302,28 @@ class _ChatViewState extends State<ChatView> {
], ],
Expanded( Expanded(
child: chatMessages.isEmpty child: chatMessages.isEmpty
? Center(child: LocaleKeys.noChatMessage.tr().toText(fontSize: 16, color: MyColors.lightTextColor, textAlign: TextAlign.center)).paddingAll(22) ? Center(
child: LocaleKeys.noChatMessage.tr().toText(
fontSize: 16,
color: MyColors.lightTextColor,
textAlign: TextAlign.center,
fontWeight: FontWeight.w500,
)).paddingAll(22)
: ListView.separated( : ListView.separated(
controller: chatVM.scrollController, controller: chatVM.scrollController,
itemCount: chatMessages.length, itemCount: chatMessages.length,
separatorBuilder: (BuildContext context, int index) => 20.height, separatorBuilder: (BuildContext context, int index) => 20.height,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
ChatMessageModel chatMessageModel = chatMessages[index]; ChatMessageModel chatMessageModel = chatMessages[index];
return ChatMessageCustomWidget( return ChatMessageCustomWidget(
chatMessageModel: chatMessageModel, chatMessageModel: chatMessageModel,
requestModel: chatTypeEnum == ChatTypeEnum.requestOffer ? chatViewArgumentsForRequest!.requestModel! : null, requestModel: chatTypeEnum == ChatTypeEnum.requestOffer ? chatViewArgumentsForRequest!.requestModel! : null,
requestStatusEnum: requestVM.currentSelectedRequest?.requestStatus, requestStatusEnum: requestVM.currentSelectedRequest?.requestStatus,
chatTypeEnum: chatTypeEnum, chatTypeEnum: chatTypeEnum,
requestsTypeEnum: chatTypeEnum == ChatTypeEnum.requestOffer ? chatViewArgumentsForRequest!.requestModel!.requestType.toRequestTypeEnum() : RequestsTypeEnum.specialCarRequest, requestsTypeEnum: chatTypeEnum == ChatTypeEnum.requestOffer ? chatViewArgumentsForRequest!.requestModel!.requestType.toRequestTypeEnum() : RequestsTypeEnum.specialCarRequest,
); );
}, },
).horPaddingMain(), ).horPaddingMain(),
), ),
10.height, 10.height,
Row( Row(
@ -242,10 +345,10 @@ class _ChatViewState extends State<ChatView> {
onPressed: requestVM.isRefreshingAfterAccept onPressed: requestVM.isRefreshingAfterAccept
? null ? null
: () { : () {
context.read<PaymentVM>().updateRequestId(id: requestVM.currentSelectedRequest!.id); context.read<PaymentVM>().updateRequestId(id: requestVM.currentSelectedRequest!.id);
requestVM.selectedDeliveryOptionEnum = null; requestVM.selectedDeliveryOptionEnum = null;
navigateWithName(context, AppRoutes.reviewRequestOffer, arguments: PaymentTypes.request); navigateWithName(context, AppRoutes.reviewRequestOffer, arguments: PaymentTypes.request);
}, },
), ),
), ),
] ]
@ -269,100 +372,73 @@ class _ChatViewState extends State<ChatView> {
// ), // ),
// ] // ]
// //
else ...[ else
if (AppState().currentAppType == AppType.provider && ...[
chatTypeEnum == ChatTypeEnum.requestOffer && if (chatVM.pickedImagesForMessage.isNotEmpty) ...[
requestVM.currentSelectedRequest!.requestStatus == RequestStatusEnum.submitted && Expanded(
chatVM.pickedImagesForMessage.isEmpty) ...[ flex: 10,
Expanded( child: PickedFilesContainer(
flex: 1, pickedFiles: chatVM.pickedImagesForMessage,
child: const Icon( onCrossPressedPrimary: chatVM.removeImageFromList,
Icons.local_offer_rounded, onAddFilePressed: () => chatVM.pickMultipleImages(),
color: MyColors.darkPrimaryColor, ),
size: 30,
).onPress(
() {
requestVM.resetSendOfferBottomSheet();
RequestDetailPageArguments requestDetailArguments = RequestDetailPageArguments(
requestIndex: chatViewArgumentsForRequest!.requestIndex,
requestModel: chatViewArgumentsForRequest!.requestModel!,
);
context.read<DashboardVMProvider>().checkUserSubscription(SubscriptionActionTypeEnum.subscription, context, callback: () {
buildSendOfferBottomSheet(
context: context,
requestDetailPageArguments: requestDetailArguments,
isFromChatScreen: true,
offerId: null, // null means creating new offer
);
});
},
), ),
), ] else
], if (chatTypeEnum == ChatTypeEnum.requestOffer) ...[
if (chatVM.pickedImagesForMessage.isNotEmpty) ...[ Expanded(
Expanded( flex: 1,
flex: 8, child: const Icon(
child: PickedFilesContainer( Icons.add_circle_outline,
pickedFiles: chatVM.pickedImagesForMessage, color: MyColors.darkPrimaryColor,
onCrossPressedPrimary: chatVM.removeImageFromList, size: 30,
onAddFilePressed: () => chatVM.pickMultipleImages(), ).onPress(() => _openAttachmentOptions(context, chatVM, requestVM)),
),
],
if (chatVM.pickedImagesForMessage.isEmpty) ...[
Expanded(
flex: 10,
child: TxtField(
isNeedLabelOnTop: false,
value: chatVM.chatMessageText,
hint: LocaleKeys.typeMessageHere.tr(),
keyboardType: TextInputType.text,
isNeedBorder: false,
onChanged: (v) => chatVM.updateChatMessageText(v),
),
), ),
), ],
] else if (chatTypeEnum == ChatTypeEnum.requestOffer) ...[
Expanded( Expanded(
flex: 1, flex: 1,
child: const Icon( child: chatVM.isSendingMessage
Icons.photo_library_rounded, ? const SizedBox(
color: MyColors.darkPrimaryColor, width: 30,
size: 30, height: 30,
).onPress(() => chatVM.pickMultipleImages()), child: CircularProgressIndicator(
), strokeWidth: 2,
], valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
if (chatVM.pickedImagesForMessage.isEmpty) ...[ ),
Expanded( )
flex: 8, : const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress(
child: TxtField(
isNeedLabelOnTop: false,
value: chatVM.chatMessageText,
hint: LocaleKeys.typeMessageHere.tr(),
keyboardType: TextInputType.text,
isNeedBorder: false,
onChanged: (v) => chatVM.updateChatMessageText(v),
),
),
],
Expanded(
flex: 1,
child: chatVM.isSendingMessage
? const SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
),
)
: const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress(
() async { () async {
ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText; ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText;
if (chatVM.pickedImagesForMessage.isNotEmpty) { if (chatVM.pickedImagesForMessage.isNotEmpty) {
chatMessageTypeEnum = ChatMessageTypeEnum.image; chatMessageTypeEnum = ChatMessageTypeEnum.image;
} }
final status = await onMessageSend(chatMessageType: chatMessageTypeEnum); final status = await onMessageSend(chatMessageType: chatMessageTypeEnum);
if (status) { if (status) {
chatVM.scrollChatDown(); chatVM.scrollChatDown();
if (chatMessageTypeEnum == ChatMessageTypeEnum.freeText) { if (chatMessageTypeEnum == ChatMessageTypeEnum.freeText) {
chatVM.clearChatMessageText(); chatVM.clearChatMessageText();
} else if (chatMessageTypeEnum == ChatMessageTypeEnum.image) { } else if (chatMessageTypeEnum == ChatMessageTypeEnum.image) {
chatVM.clearPickedImagesForMessage(); chatVM.clearPickedImagesForMessage();
} }
} }
}, },
), ),
), ),
], ],
], ],
).toContainer(isShadowEnabled: true) ).toContainer(isShadowEnabled: true)
], ],

@ -252,18 +252,18 @@ class _ChatMessageCustomWidgetState extends State<ChatMessageCustomWidget> {
chatVM.serviceProviderOffersList[index].requestOfferStatusEnum = chatMessageModel.reqOffer!.requestOfferStatusEnum; chatVM.serviceProviderOffersList[index].requestOfferStatusEnum = chatMessageModel.reqOffer!.requestOfferStatusEnum;
} }
setState(() {}); setState(() {});
// Set loading state before refreshing // Set loading state before refreshing
requestVM.updateIsRefreshingAfterAccept(true); requestVM.updateIsRefreshingAfterAccept(true);
// Refresh requests list to get updated data // Refresh requests list to get updated data
await requestVM.getRequestsBasedOnFilters(); await requestVM.getRequestsBasedOnFilters();
// Clear loading state after refresh completes // Clear loading state after refresh completes
requestVM.updateIsRefreshingAfterAccept(false); requestVM.updateIsRefreshingAfterAccept(false);
Utils.showToast(LocaleKeys.offerAccepted.tr()); Utils.showToast(LocaleKeys.offerAccepted.tr());
return true; return true;
} else { } else {
return false; return false;
@ -474,16 +474,16 @@ class _ChatMessageCustomWidgetState extends State<ChatMessageCustomWidget> {
} }
setState(() {}); setState(() {});
// Navigator.pop(context); // Navigator.pop(context);
// Set loading state before refreshing // Set loading state before refreshing
requestVM.updateIsRefreshingAfterAccept(true); requestVM.updateIsRefreshingAfterAccept(true);
// Refresh requests list to get updated data // Refresh requests list to get updated data
await requestVM.getRequestsBasedOnFilters(); await requestVM.getRequestsBasedOnFilters();
// Clear loading state after refresh completes // Clear loading state after refresh completes
requestVM.updateIsRefreshingAfterAccept(false); requestVM.updateIsRefreshingAfterAccept(false);
Utils.showToast(LocaleKeys.offerAccepted.tr()); Utils.showToast(LocaleKeys.offerAccepted.tr());
// navigateReplaceWithName(context, AppRoutes.dashboard); // navigateReplaceWithName(context, AppRoutes.dashboard);
} }
@ -559,6 +559,8 @@ class _ChatMessageCustomWidgetState extends State<ChatMessageCustomWidget> {
} }
switch (requestOfferStatusEnum) { switch (requestOfferStatusEnum) {
case RequestOfferStatusEnum.defaultStatus:
return const SizedBox.shrink();
case RequestOfferStatusEnum.offer: case RequestOfferStatusEnum.offer:
return Column( return Column(
children: [ children: [

@ -38,10 +38,32 @@ class _OfferListPageState extends State<OfferListPage> {
_onRefresh() async { _onRefresh() async {
scheduleMicrotask(() async { scheduleMicrotask(() async {
ChatVM chatVM = context.read<ChatVM>(); ChatVM chatVM = context.read<ChatVM>();
RequestsVM requestsVM = context.read<RequestsVM>();
await _ensureCurrentSelectedRequest(requestsVM);
await chatVM.getOffersFromProvidersByRequest(requestId: widget.requestId); await chatVM.getOffersFromProvidersByRequest(requestId: widget.requestId);
}); });
} }
// Normally requests_fragment.dart sets currentSelectedRequest right before navigating
// here. When this page is opened directly (e.g. from a notification), that never
// happens, leaving it null/stale and crashing ChatView. Look it up ourselves instead,
// switching request-type filter and retrying if not found in the current one.
Future<void> _ensureCurrentSelectedRequest(RequestsVM requestsVM) async {
if (requestsVM.currentSelectedRequest?.id == widget.requestId) return;
int index = requestsVM.myFilteredRequests.indexWhere((request) => request.id == widget.requestId);
if (index == -1 && requestsVM.requestsTypeFilterOptions.isNotEmpty) {
RequestsTypeEnum currentType = requestsVM.requestsTypeFilterOptions.firstWhere((element) => element.isSelected).id.toRequestTypeEnum();
RequestsTypeEnum otherType = currentType == RequestsTypeEnum.specialCarRequest ? RequestsTypeEnum.serviceRequest : RequestsTypeEnum.specialCarRequest;
await requestsVM.applyFilterOnRequestsVM(requestsTypeEnum: otherType);
index = requestsVM.myFilteredRequests.indexWhere((request) => request.id == widget.requestId);
}
if (index != -1) {
requestsVM.updateCurrentSelectedRequest(requestsVM.myFilteredRequests[index]);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<ChatVM>(builder: (context, ChatVM chatVM, Widget? child) { return Consumer<ChatVM>(builder: (context, ChatVM chatVM, Widget? child) {
@ -143,7 +165,7 @@ class _OfferListPageState extends State<OfferListPage> {
chatVM.serviceProviderOffersList[index].offerCount = 0; chatVM.serviceProviderOffersList[index].offerCount = 0;
navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments); navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments);
}); });
}).toContainer(isShadowEnabled: true); }).toContainer(isShadowEnabled: true, paddingAll: 18);
}, },
separatorBuilder: (context, index) => 16.height, separatorBuilder: (context, index) => 16.height,
), ),

@ -340,19 +340,36 @@ class RequestDetailPage extends StatelessWidget {
margin: const EdgeInsets.only(top: 21, right: 21, left: 21), margin: const EdgeInsets.only(top: 21, right: 21, left: 21),
), ),
if (!requestDetailPageArguments.requestModel.isChatted) ...[ if (!requestDetailPageArguments.requestModel.isChatted) ...[
ShowFillButton( Row(
maxWidth: double.infinity, children: [
margin: const EdgeInsets.all(15), Expanded(
maxHeight: 55, child: ShowFillButton(
title: LocaleKeys.provideOffer.tr(), maxHeight: 55,
isBold: false, title: LocaleKeys.chat.tr(),
fontSize: 18, isBold: false,
onPressed: () { fontSize: 18,
context.read<DashboardVMProvider>().checkUserSubscription(SubscriptionActionTypeEnum.subscription, context, callback: () { isFilled: false,
buildSendOfferBottomSheet(context: context, requestDetailPageArguments: requestDetailPageArguments, isFromChatScreen: false, offerId: null); borderColor: MyColors.darkPrimaryColor,
}); txtColor: MyColors.darkPrimaryColor,
}, onPressed: () => onViewChatTapped(context),
), ),
),
12.width,
Expanded(
child: ShowFillButton(
maxHeight: 55,
title: LocaleKeys.provideOffer.tr(),
isBold: false,
fontSize: 18,
onPressed: () {
context.read<DashboardVMProvider>().checkUserSubscription(SubscriptionActionTypeEnum.subscription, context, callback: () {
buildSendOfferBottomSheet(context: context, requestDetailPageArguments: requestDetailPageArguments, isFromChatScreen: false, offerId: null);
});
},
),
),
],
).paddingAll(15),
] else ...[ ] else ...[
buildRequestDetailActionFooter( buildRequestDetailActionFooter(
requestId: requestDetailPageArguments.requestModel.id, requestId: requestDetailPageArguments.requestModel.id,

@ -1125,6 +1125,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.5+1" version: "6.1.5+1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:

@ -65,6 +65,7 @@ dependencies:
# huawei_fido: ^6.3.0+305 # huawei_fido: ^6.3.0+305
device_info_plus: ^10.1.0 device_info_plus: ^10.1.0
app_links: ^7.2.1 app_links: ^7.2.1
qr_flutter: ^4.1.0
dev_dependencies: dev_dependencies:

Loading…
Cancel
Save