diff --git a/android/build.gradle b/android/build.gradle
index 8a97f51..46c8995 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -66,6 +66,20 @@ allprojects {
jvmTarget = "21"
}
}
+
+ if (project.name == 'nfc_manager') {
+ project.tasks.matching { it.name.startsWith("compile") && it.name.endsWith("Kotlin") }.configureEach {
+ doFirst {
+ def translatorFile = project.file("src/main/kotlin/io/flutter/plugins/nfcmanager/Translator.kt")
+ if (translatorFile.exists()) {
+ def content = translatorFile.text
+ if (content.contains("toLowerCase(Locale.ROOT)")) {
+ translatorFile.text = content.replace("toLowerCase(Locale.ROOT)", "lowercase(Locale.ROOT)")
+ }
+ }
+ }
+ }
+ }
}
}
}
diff --git a/assets/images/all_categories.png b/assets/images/all_categories.png
deleted file mode 100644
index aa6f895..0000000
Binary files a/assets/images/all_categories.png and /dev/null differ
diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json
index 4861521..973a5b0 100644
--- a/assets/langs/ar-SA.json
+++ b/assets/langs/ar-SA.json
@@ -782,6 +782,7 @@
"showAllLocations": "عرض جميع الفروع ({})",
"searchBranches": "البحث عن الفروع أو المدن...",
"allCities": "الكل",
+ "allCitiesOption": "جميع المدن",
"noBranchesFound": "لم يتم العثور على فروع مطابقة",
"browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن",
"branchesCount": "{} فرع",
@@ -829,6 +830,27 @@
"merchantWebsiteNotAvailable": "الموقع الإلكتروني للتاجر غير متاح",
"locationServicesRequiredForNearestOffers": "فعّل خدمات الموقع لعرض العروض القريبة منك",
"locationPermissionRequiredForNearestOffers": "اسمح للتطبيق بالوصول إلى موقعك لعرض العروض القريبة منك",
+ "mazayaDeals": "عروض مزايا",
+ "findABrandOrSomething": "ابحث عن علامة تجارية أو شيء بسيط",
+ "exploreYourBenefits": "استكشف مزاياك",
+ "theMazayaEdit": "اختيارات مزايا",
+ "exclusivelyYours": "حصرياً لك",
+ "aLittleExtraJustForMazayaMembers": "مزايا إضافية، خاصة بأعضاء مزايا.",
+ "seeAll": "عرض الكل",
+ "latestOnMazaya": "أحدث ما في مزايا",
+ "goodFoodGreatCompanyEvenBetterPerks": "طعام رائع. رفقة ممتعة. مزايا أفضل.",
+ "travelEssentials": "أساسيات السفر",
+ "fromEverydayCareToYourNextAdventure": "من العناية اليومية إلى مغامرتك القادمة.",
+ "homeAndKitchen": "المنزل والمطبخ",
+ "curatedPicksForYourHomeAndKitchen": "اختيارات مميزة لمنزلك ومطبخك.",
+ "discover": "اكتشف",
+ "saved": "المحفوظات",
+ "myFamily": "عائلتي",
+ "educationAndTraining": "التعليم والتدريب",
+ "foodsAndDrinks": "الأطعمة والمشروبات",
+ "travelAndEntertainment": "السفر والترفيه",
+ "beautyAndFitness": "الجمال واللياقة",
+ "healthcare": "الرعاية الصحية",
"favoriteMerchant": "العروض المفضلة",
"favoriteOffers": "العروض المفضلة",
"goodToKnow": "معلومات مهمة",
diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json
index 47bfca8..5a4bc2c 100644
--- a/assets/langs/en-US.json
+++ b/assets/langs/en-US.json
@@ -786,6 +786,7 @@
"showAllLocations": "Show all {} locations",
"searchBranches": "Search branches or cities...",
"allCities": "All",
+ "allCitiesOption": "All Cities",
"noBranchesFound": "No branches found",
"browseAllBranches": "Browse all {} branches across cities",
"branchesCount": "{} Branches",
@@ -830,6 +831,27 @@
"merchantWebsiteNotAvailable": "Merchant website not available",
"locationServicesRequiredForNearestOffers": "You need to enable location services to show nearest offers",
"locationPermissionRequiredForNearestOffers": "You need to give location permission to show nearest offers",
+ "mazayaDeals": "Mazaya Deals",
+ "findABrandOrSomething": "Find a brand or a little something",
+ "exploreYourBenefits": "Explore your benefits",
+ "theMazayaEdit": "The Mazaya Edit",
+ "exclusivelyYours": "Exclusively yours",
+ "aLittleExtraJustForMazayaMembers": "A little extra, just for Mazaya members.",
+ "seeAll": "See all",
+ "latestOnMazaya": "Latest on Mazaya",
+ "goodFoodGreatCompanyEvenBetterPerks": "Good food. Great company. Even better perks.",
+ "travelEssentials": "Travel Essentials",
+ "fromEverydayCareToYourNextAdventure": "From everyday care to your next adventure.",
+ "homeAndKitchen": "Home & Kitchen",
+ "curatedPicksForYourHomeAndKitchen": "Curated picks for your home and kitchen.",
+ "discover": "Discover",
+ "saved": "Saved",
+ "myFamily": "My Family",
+ "educationAndTraining": "Education & Training",
+ "foodsAndDrinks": "Foods & Drinks",
+ "travelAndEntertainment": "Travel & Entertainment",
+ "beautyAndFitness": "Beauty & Fitness",
+ "healthcare": "Healthcare",
"favoriteAddedSuccess": "Added to favorites successfully",
"favoriteRemovedSuccess": "Removed from favorites successfully",
"favoriteError": "An error occurred while updating favorites",
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 7c56964..1dc6cf7 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 12.0
+ 13.0
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 6aac74d..03a06b1 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -7,8 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
76AE113C28E078EA00513112 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 76AE113B28E078EA00513112 /* GoogleService-Info.plist */; };
@@ -57,7 +57,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */,
+ 0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -144,7 +144,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
C4CFBC4C5CAC00182015ACD5 /* [CP] Embed Pods Frameworks */,
- EF29CE25BB57F5AA48CE1136 /* [CP] Copy Pods Resources */,
+ 8ED1493DD0011941609D6F99 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -242,7 +242,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
- 6C6E76E4FB19FAF11C24005D /* [CP] Copy Pods Resources */ = {
+ 8ED1493DD0011941609D6F99 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -367,7 +367,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -454,7 +454,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -504,7 +504,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 9c12df5..bb7d20f 100644
--- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -3,9 +3,23 @@
LastUpgradeVersion = "1510"
version = "1.3">
+
+
+
+
sms
tel
mailto
+ https
+ http
+ comgooglemaps
+ googlechromes
+ waze
LSRequiresIPhoneOS
@@ -46,19 +51,23 @@
NSCameraUsageDescription
- This app requires camera access to capture & upload picture as profile image.
+ This app requires camera access to capture & upload picture as profile image, and scan QR codes.
NSFaceIDUsageDescription
This app requires Face ID to allow biometric authentication for app login.
+ NSLocalNetworkUsageDescription
+ This app requires access to local network to connect to company Wi-Fi networks for attendance.
NSLocationAlwaysAndWhenInUseUsageDescription
- This App requires access to your location to mark your attendance.
+ This app requires access to your location to mark your attendance and discover nearby offers and merchants.
NSLocationAlwaysUsageDescription
- This App requires access to your location to mark your attendance.
+ This app requires access to your location to mark your attendance and discover nearby offers and merchants.
NSLocationWhenInUseUsageDescription
- This App requires access to your location to mark your attendance.
+ This app requires access to your location to mark your attendance and discover nearby offers and merchants.
NSMicrophoneUsageDescription
- This app requires microphone access to for call.
+ This app requires microphone access for audio calls and recording voice messages.
NSPhotoLibraryUsageDescription
This app requires photo library access to select image as document & upload it.
+ NSPhotoLibraryAddUsageDescription
+ This app requires access to save photos and documents to your photo library.
UIApplicationSupportsIndirectInputEvents
NSSpeechRecognitionUsageDescription
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 304aa00..98a55e2 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -20,6 +20,7 @@
com.apple.developer.nfc.readersession.formats
+ NDEF
TAG
com.apple.developer.ubiquity-container-identifiers
diff --git a/lib/api/login_api_client.dart b/lib/api/login_api_client.dart
index 0a2a3d9..65e5fc6 100644
--- a/lib/api/login_api_client.dart
+++ b/lib/api/login_api_client.dart
@@ -294,9 +294,9 @@ class LoginApiClient {
return await ApiClient().postJsonForObject((json) => GenericResponseModel.fromJson(json).basicMemberInformation, url, postParams);
}
- Future checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName) async {
+ Future checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName, String loginType) async {
String url = "${ApiConsts.erpRest}CheckActivationCode";
- Map postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName};
+ Map postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName, "LoginType":loginType};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject((json) {
GenericResponseModel responseData = GenericResponseModel.fromJson(json);
diff --git a/lib/api/offers_and_discounts_api_client.dart b/lib/api/offers_and_discounts_api_client.dart
index 1ad8144..e85b04b 100644
--- a/lib/api/offers_and_discounts_api_client.dart
+++ b/lib/api/offers_and_discounts_api_client.dart
@@ -258,6 +258,11 @@ class OffersAndDiscountsApiClient {
Future getWalaPlusRetailers({
String? categoryId,
+ // Every existing caller passes categoryId (even "" for "all") and relies on WP_CategoryID
+ // always being present in the request - default true preserves that for everyone. Pass
+ // false only when the category filter must be fully omitted (e.g. a cross-category newest
+ // feed), not just set to empty.
+ bool includeCategoryId = true,
int page = 1,
int perPage = 10,
String? geoLocation,
@@ -285,7 +290,9 @@ class OffersAndDiscountsApiClient {
}
postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1;
postParams["CompanyID"] = 1;
- postParams["WP_CategoryID"] = effectiveCatId;
+ if (includeCategoryId) {
+ postParams["WP_CategoryID"] = effectiveCatId;
+ }
if (offerType != null && offerType.trim().isNotEmpty) {
postParams["WP_Offer_Type"] = offerType.trim();
}
@@ -323,8 +330,6 @@ class OffersAndDiscountsApiClient {
postParams["WP_User_Reference"] = null;
}
- log("postParams: ${postParams.toString()}");
-
return await ApiClient().postJsonForObject(
(response) {
if (response != null) {
diff --git a/lib/api/profile_api_client.dart b/lib/api/profile_api_client.dart
index 23197a5..7d58478 100644
--- a/lib/api/profile_api_client.dart
+++ b/lib/api/profile_api_client.dart
@@ -551,6 +551,7 @@ class ProfileApiClient {
if (personId != null) {
postParams["P_PERSON_ID"] = personId;
}
+ postParams["PatientIdentificationID"] = patientIdentificationId?.replaceAll('-', '').trim();
// Only the Grant call carries these - Revoke's payload stays exactly as it was before
// this feature. P_BIRTH_DATE/P_NHIC_VERIFIED_FLAG are always sent (even as null, if the
// source contact from GET_EMPLOYEE_CONTACTS didn't have a value) once we're granting.
@@ -561,7 +562,7 @@ class ProfileApiClient {
if (patientIdentificationId != null && patientIdentificationId.trim().isNotEmpty) {
// National ID comes formatted with dashes (e.g. "2-5616-1545-7") - the backend wants
// the plain digit string (e.g. "2561615457").
- postParams["PatientIdentificationID"] = patientIdentificationId.replaceAll('-', '').trim();
+
}
}
postParams.addAll(AppState().postParamsJson);
diff --git a/lib/config/routes.dart b/lib/config/routes.dart
index db46a5c..df6c76f 100644
--- a/lib/config/routes.dart
+++ b/lib/config/routes.dart
@@ -14,8 +14,6 @@ import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_create_request.dart';
import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_ovr_home.dart';
import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_request_detailed.dart';
import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart';
-import 'package:mohem_flutter_app/ui/landing/dashboard_screen_for_dependet.dart';
-import 'package:mohem_flutter_app/ui/landing/wala_plus_retailer_details_screen.dart';
import 'package:mohem_flutter_app/ui/landing/itg/change_itg_ad_password_screen.dart';
import 'package:mohem_flutter_app/ui/landing/itg/its_add_screen_video_image.dart';
import 'package:mohem_flutter_app/ui/landing/itg/survey_screen.dart';
@@ -33,10 +31,15 @@ import 'package:mohem_flutter_app/ui/marathon/marathon_screen.dart';
import 'package:mohem_flutter_app/ui/marathon/marathon_sponsor_video_screen.dart';
import 'package:mohem_flutter_app/ui/marathon/marathon_tutorial_viewer_screen.dart';
import 'package:mohem_flutter_app/ui/marathon/marathon_waiting_screen.dart';
+import 'package:mohem_flutter_app/ui/mazaya/retailers_list_screen.dart';
+import 'package:mohem_flutter_app/ui/mazaya/mazaya_discover_screen.dart';
+import 'package:mohem_flutter_app/ui/mazaya/saved_retailers_screen.dart';
+import 'package:mohem_flutter_app/ui/mazaya/retailer_details_screen.dart';
import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart';
import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart';
import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart';
import 'package:mohem_flutter_app/ui/my_attendance/services_menu_list_screen.dart';
+
// import 'package:mohem_flutter_app/ui/my_attendance/my_attendance_screen.dart';
import 'package:mohem_flutter_app/ui/my_team/create_request.dart';
import 'package:mohem_flutter_app/ui/my_team/employee_details.dart';
@@ -64,6 +67,7 @@ import 'package:mohem_flutter_app/ui/screens/announcements/announcement_details.
import 'package:mohem_flutter_app/ui/screens/announcements/announcements.dart';
import 'package:mohem_flutter_app/ui/screens/child_education/child_education_assistance.dart';
import 'package:mohem_flutter_app/ui/screens/courses/courses_screen.dart';
+
// import 'package:mohem_flutter_app/ui/my_attendance/work_from_home_screen.dart';
import 'package:mohem_flutter_app/ui/screens/eit/add_eit.dart';
import 'package:mohem_flutter_app/ui/screens/event_activity/event_activity.dart';
@@ -103,8 +107,10 @@ class AppRoutes {
static const String newPassword = "/newPassword";
static const String loginVerification = "/loginVerification";
static const String dashboard = "/dashboard";
- static const String dashboardScreenForDependet = "/dashboardScreenForDependet";
- static const String walaPlusRetailerDetails = "/walaPlusRetailerDetails";
+ static const String retailersListScreen = "/dashboardScreenForDependet";
+ static const String retailerDetailsScreen = "/walaPlusRetailerDetails";
+ static const String mazayaDiscover = "/mazayaDiscover";
+ static const String savedReyailersScreen = "/mazayaSaved";
static const String todayAttendance = "/todayAttendance";
static const String subMenuScreen = "/submenuScreen";
static const String addEitScreen = "/addeitScreen";
@@ -243,15 +249,14 @@ class AppRoutes {
verifyLogin: (BuildContext context) => VerifyLoginScreen(),
verifyLastLogin: (BuildContext context) => VerifyLastLoginScreen(),
dashboard: (BuildContext context) => DashboardScreen(),
- dashboardScreenForDependet: (BuildContext context) => const DashboardScreenForDependet(),
- walaPlusRetailerDetails: (BuildContext context) => const WalaPlusRetailerDetailsScreen(),
+ retailersListScreen: (BuildContext context) => const RetailersListScreen(),
+ retailerDetailsScreen: (BuildContext context) => const RetailerDetailsScreen(),
+ mazayaDiscover: (BuildContext context) => const MazayaDiscoverScreen(),
+ savedReyailersScreen: (BuildContext context) => const SavedRetailersScreen(),
appInAppWebView: (BuildContext context) {
Object? args = ModalRoute.of(context)?.settings.arguments;
if (args is Map) {
- return AppInAppWebViewScreen(
- url: args['url'] ?? "",
- title: args['title'],
- );
+ return AppInAppWebViewScreen(url: args['url'] ?? "", title: args['title']);
}
return const Scaffold();
},
@@ -262,8 +267,8 @@ class AppRoutes {
newPassword: (BuildContext context) => NewPasswordScreen(),
forgotPassword: (BuildContext context) => ForgotPasswordScreen(),
todayAttendance: (BuildContext context) => TodayAttendanceScreen2(),
- //eit
+ //eit
addEitScreen: (BuildContext context) => AddEITScreen(),
//Work List
@@ -274,7 +279,6 @@ class AppRoutes {
worklistSettings: (BuildContext context) => WorklistSettings(),
// Leave Balance
-
leaveBalance: (BuildContext context) => LeaveBalance(),
addLeaveBalance: (BuildContext context) => AddLeaveBalanceScreen(),
@@ -373,7 +377,7 @@ class AppRoutes {
unsafeDeviceScreen: (BuildContext context) => const UnsafeDeviceScreen(),
appUpdateScreen: (BuildContext context) => const AppUpdateScreen(),
childEducation: (BuildContext context) => ChildEducationAssistance(),
- activityScreen:(BuildContext context) => const EventActivityScreen(),
+ activityScreen: (BuildContext context) => const EventActivityScreen(),
//ETQAN OVR
etqanOvr: (BuildContext context) => EtqanOvrHome(),
@@ -382,6 +386,5 @@ class AppRoutes {
//Parking QR
parkingQr: (BuildContext context) => const ParkingQrScreen(),
-
};
}
diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart
index 6b8457e..42fba96 100644
--- a/lib/generated/codegen_loader.g.dart
+++ b/lib/generated/codegen_loader.g.dart
@@ -798,6 +798,7 @@ class CodegenLoader extends AssetLoader{
"showAllLocations": "عرض جميع الفروع ({})",
"searchBranches": "البحث عن الفروع أو المدن...",
"allCities": "الكل",
+ "allCitiesOption": "جميع المدن",
"noBranchesFound": "لم يتم العثور على فروع مطابقة",
"browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن",
"branchesCount": "{} فرع",
@@ -845,6 +846,27 @@ class CodegenLoader extends AssetLoader{
"merchantWebsiteNotAvailable": "الموقع الإلكتروني للتاجر غير متاح",
"locationServicesRequiredForNearestOffers": "فعّل خدمات الموقع لعرض العروض القريبة منك",
"locationPermissionRequiredForNearestOffers": "اسمح للتطبيق بالوصول إلى موقعك لعرض العروض القريبة منك",
+ "mazayaDeals": "عروض مزايا",
+ "findABrandOrSomething": "ابحث عن علامة تجارية أو شيء بسيط",
+ "exploreYourBenefits": "استكشف مزاياك",
+ "theMazayaEdit": "اختيارات مزايا",
+ "exclusivelyYours": "حصرياً لك",
+ "aLittleExtraJustForMazayaMembers": "مزايا إضافية، خاصة بأعضاء مزايا.",
+ "seeAll": "عرض الكل",
+ "latestOnMazaya": "أحدث ما في مزايا",
+ "goodFoodGreatCompanyEvenBetterPerks": "طعام رائع. رفقة ممتعة. مزايا أفضل.",
+ "travelEssentials": "أساسيات السفر",
+ "fromEverydayCareToYourNextAdventure": "من العناية اليومية إلى مغامرتك القادمة.",
+ "homeAndKitchen": "المنزل والمطبخ",
+ "curatedPicksForYourHomeAndKitchen": "اختيارات مميزة لمنزلك ومطبخك.",
+ "discover": "اكتشف",
+ "saved": "المحفوظات",
+ "myFamily": "عائلتي",
+ "educationAndTraining": "التعليم والتدريب",
+ "foodsAndDrinks": "الأطعمة والمشروبات",
+ "travelAndEntertainment": "السفر والترفيه",
+ "beautyAndFitness": "الجمال واللياقة",
+ "healthcare": "الرعاية الصحية",
"favoriteMerchant": "العروض المفضلة",
"favoriteOffers": "العروض المفضلة",
"goodToKnow": "معلومات مهمة",
@@ -858,15 +880,13 @@ class CodegenLoader extends AssetLoader{
"phone": "الهاتف",
"viewAll": "عرض الكل",
"moreCitiesCount": "+ {} مدن أخرى",
- "mazayaDeals": "عروض مزايا",
- "exploreYourBenefits": "استكشف مزاياك",
"findAnEscapeOrAnExperience": "ابحث عن تجربة أو عطلة",
"allOffers": "جميع العروض",
"sampleAvailability": "حسب التوفر",
"offersCount": "{} عروض",
"mazayaExclusive": "حصري لمزايا",
"youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.",
- "selectCity": "اختر المدينة"
+ "selectCity": "اختر المدينة",
};
static const Map _en_US = {
"mohemm": "Mohemm",
@@ -1652,6 +1672,7 @@ static const Map _en_US = {
"showAllLocations": "Show all {} locations",
"searchBranches": "Search branches or cities...",
"allCities": "All",
+ "allCitiesOption": "All Cities",
"noBranchesFound": "No branches found",
"browseAllBranches": "Browse all {} branches across cities",
"branchesCount": "{} Branches",
@@ -1696,6 +1717,27 @@ static const Map _en_US = {
"merchantWebsiteNotAvailable": "Merchant website not available",
"locationServicesRequiredForNearestOffers": "You need to enable location services to show nearest offers",
"locationPermissionRequiredForNearestOffers": "You need to give location permission to show nearest offers",
+ "mazayaDeals": "Mazaya Deals",
+ "findABrandOrSomething": "Find a brand or a little something",
+ "exploreYourBenefits": "Explore your benefits",
+ "theMazayaEdit": "The Mazaya Edit",
+ "exclusivelyYours": "Exclusively yours",
+ "aLittleExtraJustForMazayaMembers": "A little extra, just for Mazaya members.",
+ "seeAll": "See all",
+ "latestOnMazaya": "Latest on Mazaya",
+ "goodFoodGreatCompanyEvenBetterPerks": "Good food. Great company. Even better perks.",
+ "travelEssentials": "Travel Essentials",
+ "fromEverydayCareToYourNextAdventure": "From everyday care to your next adventure.",
+ "homeAndKitchen": "Home & Kitchen",
+ "curatedPicksForYourHomeAndKitchen": "Curated picks for your home and kitchen.",
+ "discover": "Discover",
+ "saved": "Saved",
+ "myFamily": "My Family",
+ "educationAndTraining": "Education & Training",
+ "foodsAndDrinks": "Foods & Drinks",
+ "travelAndEntertainment": "Travel & Entertainment",
+ "beautyAndFitness": "Beauty & Fitness",
+ "healthcare": "Healthcare",
"favoriteAddedSuccess": "Added to favorites successfully",
"favoriteRemovedSuccess": "Removed from favorites successfully",
"favoriteError": "An error occurred while updating favorites",
@@ -1712,15 +1754,13 @@ static const Map _en_US = {
"phone": "Phone",
"viewAll": "View all",
"moreCitiesCount": "+ {} more cities",
- "mazayaDeals": "Mazaya Deals",
- "exploreYourBenefits": "Explore your benefits",
"findAnEscapeOrAnExperience": "Find an escape or an experience",
"allOffers": "All offers",
"sampleAvailability": "sample availability",
"offersCount": "{} offers",
"mazayaExclusive": "MAZAYA EXCLUSIVE",
"youAreAllCaughtUp": "You're all caught up. More little extras soon.",
- "selectCity": "Select City"
+ "selectCity": "Select City",
};
static const Map> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US};
}
diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart
index f1ee6c4..9bc9bfd 100644
--- a/lib/generated/locale_keys.g.dart
+++ b/lib/generated/locale_keys.g.dart
@@ -770,6 +770,7 @@ abstract class LocaleKeys {
static const showAllLocations = 'showAllLocations';
static const searchBranches = 'searchBranches';
static const allCities = 'allCities';
+ static const allCitiesOption = 'allCitiesOption';
static const noBranchesFound = 'noBranchesFound';
static const browseAllBranches = 'browseAllBranches';
static const branchesCount = 'branchesCount';
@@ -817,6 +818,27 @@ abstract class LocaleKeys {
static const merchantWebsiteNotAvailable = 'merchantWebsiteNotAvailable';
static const locationServicesRequiredForNearestOffers = 'locationServicesRequiredForNearestOffers';
static const locationPermissionRequiredForNearestOffers = 'locationPermissionRequiredForNearestOffers';
+ static const mazayaDeals = 'mazayaDeals';
+ static const findABrandOrSomething = 'findABrandOrSomething';
+ static const exploreYourBenefits = 'exploreYourBenefits';
+ static const theMazayaEdit = 'theMazayaEdit';
+ static const exclusivelyYours = 'exclusivelyYours';
+ static const aLittleExtraJustForMazayaMembers = 'aLittleExtraJustForMazayaMembers';
+ static const seeAll = 'seeAll';
+ static const latestOnMazaya = 'latestOnMazaya';
+ static const goodFoodGreatCompanyEvenBetterPerks = 'goodFoodGreatCompanyEvenBetterPerks';
+ static const travelEssentials = 'travelEssentials';
+ static const fromEverydayCareToYourNextAdventure = 'fromEverydayCareToYourNextAdventure';
+ static const homeAndKitchen = 'homeAndKitchen';
+ static const curatedPicksForYourHomeAndKitchen = 'curatedPicksForYourHomeAndKitchen';
+ static const discover = 'discover';
+ static const saved = 'saved';
+ static const myFamily = 'myFamily';
+ static const educationAndTraining = 'educationAndTraining';
+ static const foodsAndDrinks = 'foodsAndDrinks';
+ static const travelAndEntertainment = 'travelAndEntertainment';
+ static const beautyAndFitness = 'beautyAndFitness';
+ static const healthcare = 'healthcare';
static const favoriteMerchant = 'favoriteMerchant';
static const favoriteOffers = 'favoriteOffers';
static const goodToKnow = 'goodToKnow';
@@ -830,8 +852,6 @@ abstract class LocaleKeys {
static const phone = 'phone';
static const viewAll = 'viewAll';
static const moreCitiesCount = 'moreCitiesCount';
- static const mazayaDeals = 'mazayaDeals';
- static const exploreYourBenefits = 'exploreYourBenefits';
static const findAnEscapeOrAnExperience = 'findAnEscapeOrAnExperience';
static const allOffers = 'allOffers';
static const sampleAvailability = 'sampleAvailability';
diff --git a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart
index 054d741..c124e6b 100644
--- a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart
+++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
+import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
class WalaPlusRetailersResponse {
@@ -208,6 +209,11 @@ class WalaPlusRetailerModel {
String? countryNameEn;
bool? isFavorite;
+ // Drives the "MAZAYA EXCLUSIVE" badge on the retailer card (Figma node 927:496). NOT parsed
+ // from the WalaPlus_Retailers/WalaPlus_RetailersByID API - this is a different, separate
+ // "Exclusive" API/section, so callers set this manually on the model after fetching from there.
+ bool? isExclusive;
+
WalaPlusRetailerModel({
this.id,
this.name,
@@ -221,6 +227,7 @@ class WalaPlusRetailerModel {
this.logoThumb,
this.website,
this.isFavorite,
+ this.isExclusive,
this.type,
this.usage,
this.weight,
@@ -338,38 +345,44 @@ class WalaPlusRetailerModel {
categoriesList.add(WalaPlusCategoryModel(name: rawCats.toString().trim()));
}
- String? singleCatName = (targetJson['category_name'] ??
- targetJson['categoryName'] ??
- targetJson['cat_name'] ??
- targetJson['catName'] ??
- targetJson['CategoryName'] ??
- targetJson['Category_Name'] ??
- targetJson['category_title'] ??
- targetJson['categoryTitle'])?.toString().trim();
- String? singleCatAr = (targetJson['category_name_ar'] ??
- targetJson['categoryNameAr'] ??
- targetJson['cat_name_ar'] ??
- targetJson['catNameAr'] ??
- targetJson['category_ar'] ??
- targetJson['country_name_ar'] ??
- targetJson['countryNameAr'])?.toString().trim();
- String? singleCatEn = (targetJson['category_name_en'] ??
- targetJson['categoryNameEn'] ??
- targetJson['cat_name_en'] ??
- targetJson['catNameEn'] ??
- targetJson['category_en'] ??
- targetJson['country_name_en'] ??
- targetJson['countryNameEn'])?.toString().trim();
- String? foundCategoryId = (targetJson['category_id'] ??
- targetJson['categoryId'] ??
- targetJson['cat_id'] ??
- targetJson['catId'] ??
- targetJson['WP_CategoryID'] ??
- targetJson['WP_CategoryId'])?.toString().trim();
+ String? singleCatName =
+ (targetJson['category_name'] ??
+ targetJson['categoryName'] ??
+ targetJson['cat_name'] ??
+ targetJson['catName'] ??
+ targetJson['CategoryName'] ??
+ targetJson['Category_Name'] ??
+ targetJson['category_title'] ??
+ targetJson['categoryTitle'])
+ ?.toString()
+ .trim();
+ String? singleCatAr =
+ (targetJson['category_name_ar'] ??
+ targetJson['categoryNameAr'] ??
+ targetJson['cat_name_ar'] ??
+ targetJson['catNameAr'] ??
+ targetJson['category_ar'] ??
+ targetJson['country_name_ar'] ??
+ targetJson['countryNameAr'])
+ ?.toString()
+ .trim();
+ String? singleCatEn =
+ (targetJson['category_name_en'] ??
+ targetJson['categoryNameEn'] ??
+ targetJson['cat_name_en'] ??
+ targetJson['catNameEn'] ??
+ targetJson['category_en'] ??
+ targetJson['country_name_en'] ??
+ targetJson['countryNameEn'])
+ ?.toString()
+ .trim();
+ String? foundCategoryId =
+ (targetJson['category_id'] ?? targetJson['categoryId'] ?? targetJson['cat_id'] ?? targetJson['catId'] ?? targetJson['WP_CategoryID'] ?? targetJson['WP_CategoryId'])?.toString().trim();
String? rawType = targetJson['type']?.toString().trim();
String? lowerType = rawType?.toLowerCase();
- bool isTypeStoreKeyword = lowerType == null ||
+ bool isTypeStoreKeyword =
+ lowerType == null ||
lowerType.isEmpty ||
lowerType == 'online' ||
lowerType == 'instore' ||
@@ -387,12 +400,9 @@ class WalaPlusRetailerModel {
if ((singleCatName != null && singleCatName.isNotEmpty && singleCatName != "null") ||
(singleCatAr != null && singleCatAr.isNotEmpty && singleCatAr != "null") ||
(singleCatEn != null && singleCatEn.isNotEmpty && singleCatEn != "null")) {
- categoriesList.add(WalaPlusCategoryModel(
- id: foundCategoryId,
- name: singleCatName ?? singleCatEn ?? singleCatAr,
- countryNameAr: singleCatAr ?? singleCatName,
- countryNameEn: singleCatEn ?? singleCatName,
- ));
+ categoriesList.add(
+ WalaPlusCategoryModel(id: foundCategoryId, name: singleCatName ?? singleCatEn ?? singleCatAr, countryNameAr: singleCatAr ?? singleCatName, countryNameEn: singleCatEn ?? singleCatName),
+ );
}
}
@@ -429,7 +439,8 @@ class WalaPlusRetailerModel {
}
}
- String? foundWebsite = targetJson['website']?.toString() ??
+ String? foundWebsite =
+ targetJson['website']?.toString() ??
targetJson['merchant_website']?.toString() ??
targetJson['merchantWebsite']?.toString() ??
targetJson['website_url']?.toString() ??
@@ -464,25 +475,13 @@ class WalaPlusRetailerModel {
bool? parsedIsFavorite;
if (targetJson.containsKey('isfavorite')) {
- parsedIsFavorite = targetJson['isfavorite'] == true ||
- targetJson['isfavorite'] == 1 ||
- targetJson['isfavorite'] == 'true' ||
- targetJson['isfavorite'] == '1';
+ parsedIsFavorite = targetJson['isfavorite'] == true || targetJson['isfavorite'] == 1 || targetJson['isfavorite'] == 'true' || targetJson['isfavorite'] == '1';
} else if (targetJson.containsKey('is_favorite')) {
- parsedIsFavorite = targetJson['is_favorite'] == true ||
- targetJson['is_favorite'] == 1 ||
- targetJson['is_favorite'] == 'true' ||
- targetJson['is_favorite'] == '1';
+ parsedIsFavorite = targetJson['is_favorite'] == true || targetJson['is_favorite'] == 1 || targetJson['is_favorite'] == 'true' || targetJson['is_favorite'] == '1';
} else if (targetJson.containsKey('isFavorite')) {
- parsedIsFavorite = targetJson['isFavorite'] == true ||
- targetJson['isFavorite'] == 1 ||
- targetJson['isFavorite'] == 'true' ||
- targetJson['isFavorite'] == '1';
+ parsedIsFavorite = targetJson['isFavorite'] == true || targetJson['isFavorite'] == 1 || targetJson['isFavorite'] == 'true' || targetJson['isFavorite'] == '1';
} else if (targetJson.containsKey('favorite')) {
- parsedIsFavorite = targetJson['favorite'] == true ||
- targetJson['favorite'] == 1 ||
- targetJson['favorite'] == 'true' ||
- targetJson['favorite'] == '1';
+ parsedIsFavorite = targetJson['favorite'] == true || targetJson['favorite'] == 1 || targetJson['favorite'] == 'true' || targetJson['favorite'] == '1';
}
return WalaPlusRetailerModel(
@@ -532,6 +531,54 @@ class WalaPlusRetailerModel {
);
}
+ // Adapter for the Mazaya "Exclusive Offers" section, which reuses the legacy Offers &
+ // Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) rather than a
+ // WalaPlus endpoint. Maps whatever overlaps onto this model so the existing grid/card/detail
+ // screen can render it unchanged - fields with no equivalent on OffersListModel (favorite,
+ // redeem, locations, contact, ...) are simply left null, and isExclusive hides the actions
+ // that depend on them (see WalaPlusRetailerCard, RetailerDetailsScreen).
+ factory WalaPlusRetailerModel.fromExclusiveOffer(OffersListModel offer, {bool isAr = false}) {
+ String? name = decodeUnicodeString(isAr ? (offer.titleAr ?? offer.titleEn) : (offer.titleEn ?? offer.titleAr));
+ String? description = decodeUnicodeString(isAr ? (offer.descriptionAr ?? offer.descriptionEn) : (offer.descriptionEn ?? offer.descriptionAr));
+ String? categoryName = decodeUnicodeString(isAr ? (offer.categoryNameAr ?? offer.categoryNameEn) : (offer.categoryNameEn ?? offer.categoryNameAr));
+ String? discountText = _formatExclusiveDiscountText(decodeUnicodeString(offer.discountDescription) ?? decodeUnicodeString(offer.discount));
+ return WalaPlusRetailerModel(
+ id: "exclusive-${offer.offersDiscountId ?? ''}",
+ name: name,
+ // WalaPlusRetailerCard reads offerTitle as the promo subtitle shown under the name (e.g.
+ // "20% off") - the discount text is the closest equivalent on a legacy offer.
+ offerTitle: discountText,
+ description: description,
+ info: description,
+ image: offer.bannerImage,
+ logo: offer.logo,
+ website: offer.websiteUrl,
+ endDate: offer.endDate,
+ validTo: offer.endDate,
+ categoryName: categoryName,
+ categories: categoryName != null ? [WalaPlusCategoryModel(name: categoryName)] : null,
+ isExclusive: true,
+ );
+ }
+
+ // The legacy Offers & Discounts API sometimes returns a bare number ("10", "20") for its
+ // discount fields rather than a formatted "10% off" string. WalaPlusRetailerCard's badge
+ // logic (getDiscountBadge) only recognizes a "%" pattern, so a bare number would
+ // otherwise render verbatim instead of as a discount. Already-formatted text (containing "%",
+ // "off", or "خصم") is left untouched.
+ static String? _formatExclusiveDiscountText(String? raw) {
+ if (raw == null) return null;
+ String trimmed = raw.trim();
+ if (trimmed.isEmpty) return null;
+ if (trimmed.contains('%') || trimmed.toLowerCase().contains('off') || trimmed.contains('خصم')) {
+ return trimmed;
+ }
+ if (RegExp(r'^\d+(\.\d+)?$').hasMatch(trimmed)) {
+ return LocaleKeys.offDiscount.tr(args: ['$trimmed%']);
+ }
+ return trimmed;
+ }
+
static String? decodeUnicodeString(Object? val) {
if (val == null) return null;
String s = val.toString().trim();
@@ -614,6 +661,7 @@ class WalaPlusRetailerModel {
data['country_name_en'] = countryNameEn;
data['is_favorite'] = isFavorite;
data['isfavorite'] = isFavorite;
+ data['is_exclusive'] = isExclusive;
if (offers != null) {
data['offers'] = offers!.map((v) => v.toJson()).toList();
}
@@ -633,9 +681,8 @@ class WalaPlusRetailerModel {
for (var cat in categories!) {
String? primary = isAr ? cat.countryNameAr : cat.countryNameEn;
String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr);
- String? resolved = (primary != null && primary.trim().isNotEmpty && primary != "null")
- ? primary.trim()
- : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
+ String? resolved =
+ (primary != null && primary.trim().isNotEmpty && primary != "null") ? primary.trim() : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
if (resolved != null && resolved.isNotEmpty) {
return resolved;
}
@@ -649,9 +696,10 @@ class WalaPlusRetailerModel {
if (cat.id?.toString() == categoryId?.toString()) {
String? primary = isAr ? cat.countryNameAr : cat.countryNameEn;
String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr);
- String? resolved = (primary != null && primary.trim().isNotEmpty && primary != "null")
- ? primary.trim()
- : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
+ String? resolved =
+ (primary != null && primary.trim().isNotEmpty && primary != "null")
+ ? primary.trim()
+ : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
if (resolved != null && resolved.isNotEmpty) {
return resolved;
}
diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart
index 065ebf5..b1337da 100644
--- a/lib/ui/landing/dashboard_screen.dart
+++ b/lib/ui/landing/dashboard_screen.dart
@@ -460,7 +460,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
- Navigator.pushNamed(context, AppRoutes.dashboardScreenForDependet);
+ Navigator.pushNamed(context, AppRoutes.mazayaDiscover);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
diff --git a/lib/ui/landing/widget/services_widget.dart b/lib/ui/landing/widget/services_widget.dart
index f1e03f8..74cfc0f 100644
--- a/lib/ui/landing/widget/services_widget.dart
+++ b/lib/ui/landing/widget/services_widget.dart
@@ -111,7 +111,7 @@ class ServicesWidget extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
- Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true)),
+ Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true, maxLine: 3)),
RotatedBox(quarterTurns: AppState().isArabic(context) ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)),
],
),
diff --git a/lib/ui/login/login_screen.dart b/lib/ui/login/login_screen.dart
index fa83d3d..f6cb4ab 100644
--- a/lib/ui/login/login_screen.dart
+++ b/lib/ui/login/login_screen.dart
@@ -920,7 +920,7 @@ class _LoginScreenState extends State {
if (effectiveToken.isNotEmpty && mounted) {
Utils.hideLoading(context);
- Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (Route route) => false);
+ Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route route) => false);
return true;
}
} catch (e) {
@@ -990,7 +990,7 @@ class _LoginScreenState extends State {
Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString());
if (navigateToDashboard && ApiConsts.ISDependetLogin) {
- Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (Route route) => false);
+ Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route route) => false);
}
}
diff --git a/lib/ui/mazaya/mazaya_discover_screen.dart b/lib/ui/mazaya/mazaya_discover_screen.dart
new file mode 100644
index 0000000..eec1603
--- /dev/null
+++ b/lib/ui/mazaya/mazaya_discover_screen.dart
@@ -0,0 +1,1022 @@
+import 'dart:async';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
+import 'package:mohem_flutter_app/app_state/app_state.dart';
+import 'package:mohem_flutter_app/classes/colors.dart';
+import 'package:mohem_flutter_app/classes/consts.dart';
+import 'package:mohem_flutter_app/classes/utils.dart';
+import 'package:mohem_flutter_app/config/routes.dart';
+import 'package:mohem_flutter_app/extensions/string_extensions.dart';
+import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
+import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
+import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
+import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
+import 'package:mohem_flutter_app/models/post_params_model.dart';
+import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart';
+import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart';
+import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
+import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
+import 'package:shimmer/shimmer.dart';
+
+// A single, fixed-category brand-list section (Travel Essentials, Home & Kitchen, ...). Swapping
+// which category a section shows is a one-line change - just the categoryId passed to the
+// constructor where the section is declared - since the fetch, the title resolution, and the UI
+// binding all read off this one object instead of each having their own copy of the id.
+class _MazayaCategorySection {
+ final String categoryId;
+ bool isLoading = true;
+ List retailers = [];
+
+ // Resolved from the retailer response's own embedded `categories` data - fallback source for
+ // the title when the id isn't found in the top-level categories list (see _sectionTitle).
+ String? resolvedName;
+
+ _MazayaCategorySection(this.categoryId);
+}
+
+// Mazaya Discover screen (Figma node 890:2050, "Mazaya Deals - Curated for you"). Reached from
+// the Mazaya banner on the main dashboard. Most sections are wired to the WalaPlus APIs
+// (categories + per-category retailers). "Exclusive offers" instead reuses the legacy Offers &
+// Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) - see
+// _fetchExclusiveOffers and WalaPlusRetailerModel.fromExclusiveOffer.
+class MazayaDiscoverScreen extends StatefulWidget {
+ const MazayaDiscoverScreen({Key? key}) : super(key: key);
+
+ @override
+ State createState() => _MazayaDiscoverScreenState();
+}
+
+class _MazayaDiscoverScreenState extends State {
+ final TextEditingController _searchController = TextEditingController();
+
+ bool _isLoadingCategories = true;
+ List _categories = [];
+
+ bool _isLoadingExclusiveOffers = true;
+ List _exclusiveOffers = [];
+
+ String selectedCity = "All Cities";
+ bool _isLoadingLatest = true;
+ List _latestRetailers = [];
+
+ // Fixed-category sections - to point a section at a different category, change only the id
+ // passed here. Everything else (fetch, title, UI) reads off the _MazayaCategorySection object.
+ final _MazayaCategorySection _travelSection = _MazayaCategorySection('Q4');
+ final _MazayaCategorySection _homeKitchenSection = _MazayaCategorySection(
+ 'Bw',
+ );
+
+ @override
+ void initState() {
+ super.initState();
+ _fetchCategories();
+ _fetchExclusiveOffers();
+ _fetchLatestOnMazaya();
+ _fetchCategorySection(_travelSection);
+ _fetchCategorySection(_homeKitchenSection);
+ }
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ // Only drives the benefits strip now - Travel Essentials/Home & Kitchen/Latest on Mazaya each
+ // fetch independently (see _fetchCategorySection/_fetchLatestOnMazaya). Still fetched in
+ // parallel with those though, since it also feeds each fixed-category section's title (the
+ // top-level categories list is the primary source there - see _sectionTitle).
+ Future _fetchCategories() async {
+ setState(() => _isLoadingCategories = true);
+ try {
+ List categories =
+ await OffersAndDiscountsApiClient().getWalaPlusCategories(
+ context: context,
+ );
+ if (!mounted) return;
+ setState(() {
+ _categories = categories;
+ _isLoadingCategories = false;
+ });
+ } catch (e) {
+ debugPrint("MazayaDiscoverScreen: error fetching categories: $e");
+ if (mounted) setState(() => _isLoadingCategories = false);
+ }
+ }
+
+ // Fetches one fixed-category section (Travel Essentials, Home & Kitchen, ...) by whichever
+ // categoryId is set on the given _MazayaCategorySection. Title data comes from the first
+ // matching entry in each returned retailer's own embedded `categories` list (each retailer
+ // carries its full category info straight from the API) - used as a fallback by _sectionTitle
+ // when the id isn't found in the top-level categories list.
+ Future _fetchCategorySection(_MazayaCategorySection section) async {
+ setState(() => section.isLoading = true);
+ try {
+ WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
+ .getWalaPlusRetailers(
+ categoryId: section.categoryId,
+ page: 1,
+ perPage: 10,
+ context: context,
+ );
+ if (!mounted) return;
+ List retailers =
+ response?.retailers ?? [];
+ String? categoryName;
+ for (WalaPlusRetailerModel r in retailers) {
+ for (WalaPlusCategoryModel c
+ in r.categories ?? []) {
+ if (c.id == section.categoryId && (c.name ?? '').isNotEmpty) {
+ categoryName = c.name;
+ break;
+ }
+ }
+ if (categoryName != null) break;
+ }
+ setState(() {
+ section.retailers = retailers;
+ section.resolvedName = categoryName;
+ section.isLoading = false;
+ });
+ } catch (e) {
+ debugPrint(
+ "MazayaDiscoverScreen: error fetching category section '${section.categoryId}': $e",
+ );
+ if (mounted) setState(() => section.isLoading = false);
+ }
+ }
+
+ // "Latest on Mazaya" = newest retailers across all categories (WP_Sort_By=newest, WP_CategoryID
+ // omitted entirely - not just empty) - same WP_Sort_By param already wired for the main
+ // retailers grid's Sort by filter. No category filter here on purpose: the Figma mock itself
+ // mixes categories in this section (a logistics brand next to a food brand), confirming it's a
+ // cross-category feed.
+ Future _fetchLatestOnMazaya() async {
+ setState(() => _isLoadingLatest = true);
+ try {
+ WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
+ .getWalaPlusRetailers(
+ includeCategoryId: false,
+ sortBy: 'newest',
+ page: 1,
+ perPage: 10,
+ context: context,
+ );
+ if (!mounted) return;
+ setState(() {
+ _latestRetailers = response?.retailers ?? [];
+ _isLoadingLatest = false;
+ });
+ } catch (e) {
+ debugPrint("MazayaDiscoverScreen: error fetching latest-on-Mazaya: $e");
+ if (mounted) setState(() => _isLoadingLatest = false);
+ }
+ }
+
+ // Resolved live rather than stored once, since the two sources load independently and either
+ // can arrive first: the top-level categories list (the primary/most reliable source - present
+ // even if the section's category currently has zero retailers) and that category's own data
+ // embedded in each retailer (kept as a fallback for when the id isn't in the top-level list).
+ // fallbackTitle only shows while neither source has resolved a name yet.
+ String _sectionTitle(_MazayaCategorySection section, String fallbackTitle) {
+ for (WalaPlusCategoryModel c in _categories) {
+ if (c.id == section.categoryId && (c.name ?? '').isNotEmpty) {
+ return c.name!;
+ }
+ }
+ if (section.resolvedName != null && section.resolvedName!.isNotEmpty) {
+ return section.resolvedName!;
+ }
+ return fallbackTitle;
+ }
+
+ // Same id-first/resolvedName-fallback lookup as _sectionTitle, but returns the full category
+ // model so "See all" can hand RetailersListScreen the actual category to pre-select (it re-syncs
+ // this against its own category list once that loads, so an id match is all that's required).
+ WalaPlusCategoryModel _categoryForSection(
+ _MazayaCategorySection section,
+ String fallbackTitle,
+ ) {
+ for (WalaPlusCategoryModel c in _categories) {
+ if (c.id == section.categoryId) return c;
+ }
+ return WalaPlusCategoryModel(
+ id: section.categoryId,
+ name: _sectionTitle(section, fallbackTitle),
+ );
+ }
+
+ // "Exclusive offers" reuses the legacy Offers & Discounts (ITG) API/implementation
+ // (lib/ui/screens/offers_and_discounts) rather than a WalaPlus endpoint - categoryID 0 is the
+ // same "all categories" value that screen's own initial load uses. Each result is mapped onto
+ // WalaPlusRetailerModel (see fromExclusiveOffer) so it renders through the same card/grid/detail
+ // screen as every other Mazaya section, flagged via isExclusive.
+ Future _fetchExclusiveOffers() async {
+ setState(() => _isLoadingExclusiveOffers = true);
+ try {
+ bool isAr = AppState().isArabic(context);
+ List offers = await OffersAndDiscountsApiClient()
+ .getOffersList(0, 10);
+ if (!mounted) return;
+ setState(() {
+ _exclusiveOffers =
+ offers
+ .map(
+ (o) =>
+ WalaPlusRetailerModel.fromExclusiveOffer(o, isAr: isAr),
+ )
+ .toList();
+ _isLoadingExclusiveOffers = false;
+ });
+ } catch (e) {
+ debugPrint("MazayaDiscoverScreen: error fetching exclusive offers: $e");
+ if (mounted) setState(() => _isLoadingExclusiveOffers = false);
+ }
+ }
+
+ void _openRetailer(WalaPlusRetailerModel item) {
+ Navigator.pushNamed(
+ context,
+ AppRoutes.retailerDetailsScreen,
+ arguments: item,
+ );
+ }
+
+ void _openRetailersGrid({WalaPlusCategoryModel? category}) {
+ Navigator.pushNamed(
+ context,
+ AppRoutes.retailersListScreen,
+ arguments: category,
+ );
+ }
+
+ // Exclusive offers reuse the legacy Offers & Discounts API (see fetchExclusiveOffers in both
+ // this screen and RetailersListScreen), so "See all" opens RetailersListScreen's exclusive
+ // mode instead of a WalaPlus category.
+ void _openExclusiveOffersGrid() {
+ Navigator.pushNamed(
+ context,
+ AppRoutes.retailersListScreen,
+ arguments: const {'exclusiveMode': true},
+ );
+ }
+
+ Future _onRefresh() async {
+ await Future.wait(>[
+ _fetchCategories(),
+ _fetchLatestOnMazaya(),
+ _fetchCategorySection(_travelSection),
+ _fetchCategorySection(_homeKitchenSection),
+ _fetchExclusiveOffers(),
+ ]);
+ }
+
+ Future _clearDependentPrefs() async {
+ await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN");
+ await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID");
+ await Utils.removeStringFromPrefs("P_DEP_BIRTH_DATE");
+ await Utils.removeStringFromPrefs("P_DEP_TOKEN_ID");
+ await Utils.removeStringFromPrefs("P_DEP_LOGIN_TOKEN_ID");
+ await Utils.removeStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID");
+ await Utils.removeStringFromPrefs("P_DEP_MOBILE_NUMBER");
+ await Utils.removeStringFromPrefs("P_DEPENDENT_NAME");
+ await Utils.removeStringFromPrefs("P_DEP_LOGIN_TYPE");
+ }
+
+ // Same app bar actions as the previous dependent-login landing page
+ // (DashboardScreenForDependent, now reached from here instead): language toggle + logout,
+ // shown only for dependent-login sessions. Tapping back when there's nothing to pop (this
+ // screen is now the dependent-login landing page, so that's the common case) logs out instead.
+ void _performLogout() {
+ ConfirmBottomSheet.show(
+ context,
+ message: LocaleKeys.areYouSureWantToLogOut.tr(),
+ confirmText: LocaleKeys.yes.tr(),
+ cancelText: LocaleKeys.no.tr(),
+ isDestructive: false,
+ onConfirm: () async {
+ if (ApiConsts.isDependetLogin) {
+ await _clearDependentPrefs();
+ }
+ ApiConsts.selectedMemberType = MemberType.employee;
+ ApiConsts.ISDependetLogin = false;
+ AppState().setDependentName = null;
+ AppState().setDependentDob = null;
+ AppState().setDependentLoginType = null;
+ AppState().setWalaplusDepInfoModel = null;
+ AppState().setPostParamsInitConfig();
+ if (mounted) {
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ AppRoutes.login,
+ (Route route) => false,
+ );
+ }
+ },
+ );
+ }
+
+ Future _changeLanguage(Locale newLocale) async {
+ if (context.locale == newLocale) return;
+ await context.setLocale(newLocale);
+ PostParamsModel? obj = AppState().postParamsObject;
+ if (obj != null) {
+ obj.languageID = (newLocale.languageCode == "ar" ? 1 : 2);
+ AppState().setPostParamsModel(obj);
+ }
+ if (mounted) {
+ setState(() {});
+ await _onRefresh();
+ }
+ }
+
+ Widget _buildLanguageIconButton(bool isAr) {
+ return Tooltip(
+ message: isAr ? 'English' : 'العربية',
+ child: InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: () {
+ if (isAr) {
+ _changeLanguage(const Locale("en", "US"));
+ } else {
+ _changeLanguage(const Locale("ar", "SA"));
+ }
+ },
+ child: const Padding(
+ padding: EdgeInsets.all(3),
+ child: Icon(
+ Icons.language_rounded,
+ size: 24,
+ color: Color(0xFF1E293B),
+ ),
+ ),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ bool isAr = AppState().isArabic(context);
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBarWidget(
+ context,
+ title: LocaleKeys.mazayaDeals.tr(),
+ showHomeButton: false,
+ onBackTapped: () {
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ } else if (ApiConsts.isDependetLogin) {
+ _performLogout();
+ }
+ },
+ actions:
+ ApiConsts.isDependetLogin
+ ? [
+ _buildLanguageIconButton(isAr),
+ const SizedBox(width: 4),
+ InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: _performLogout,
+ child: Padding(
+ padding: const EdgeInsets.all(6),
+ child: SvgPicture.asset(
+ 'assets/images/drawer/logout.svg',
+ height: 20,
+ width: 20,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ ]
+ : null,
+ ),
+ bottomNavigationBar: const MazayaBottomNav(
+ currentTab: MazayaTab.discover,
+ ),
+ body: RefreshIndicator(
+ onRefresh: _onRefresh,
+ color: MyColors.textMixColor,
+ child: SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(
+ parent: BouncingScrollPhysics(),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(22, 16, 22, 0),
+ child: _buildSearchField(),
+ ),
+ const SizedBox(height: 18),
+ _buildCategoriesSection(),
+ const SizedBox(height: 24),
+ _buildExclusiveOffersSection(),
+ const SizedBox(height: 24),
+ _buildBrandSection(
+ title: LocaleKeys.latestOnMazaya.tr(),
+ subtitle: LocaleKeys.goodFoodGreatCompanyEvenBetterPerks.tr(),
+ isLoading: _isLoadingLatest,
+ retailers: _latestRetailers,
+ backgroundColor: Colors.white,
+ onSeeAll:
+ () => _openRetailersGrid(
+ category: WalaPlusCategoryModel(
+ id: 'all',
+ name: LocaleKeys.allCategories.tr(),
+ ),
+ ),
+ ),
+ _buildBrandSection(
+ title: _sectionTitle(
+ _travelSection,
+ LocaleKeys.travelEssentials.tr(),
+ ),
+ subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(),
+ isLoading: _travelSection.isLoading,
+ retailers: _travelSection.retailers,
+ backgroundColor: const Color(0xFFEFF2EF),
+ onSeeAll:
+ () => _openRetailersGrid(
+ category: _categoryForSection(
+ _travelSection,
+ LocaleKeys.travelEssentials.tr(),
+ ),
+ ),
+ ),
+ _buildBrandSection(
+ title: _sectionTitle(
+ _homeKitchenSection,
+ LocaleKeys.homeAndKitchen.tr(),
+ ),
+ subtitle: LocaleKeys.curatedPicksForYourHomeAndKitchen.tr(),
+ isLoading: _homeKitchenSection.isLoading,
+ retailers: _homeKitchenSection.retailers,
+ backgroundColor: Colors.white,
+ onSeeAll:
+ () => _openRetailersGrid(
+ category: _categoryForSection(
+ _homeKitchenSection,
+ LocaleKeys.homeAndKitchen.tr(),
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildSearchField() {
+ return Container(
+ height: 47,
+ padding: const EdgeInsets.symmetric(horizontal: 14),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border.all(color: const Color(0xFFE8EAE4)),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ children: [
+ SvgPicture.asset(
+ "assets/icons/mazaya_search.svg",
+ width: 18,
+ height: 18,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFF78817F),
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: TextField(
+ controller: _searchController,
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ isDense: true,
+ hintText: LocaleKeys.findABrandOrSomething.tr(),
+ hintStyle: const TextStyle(
+ color: Color(0xFF78817F),
+ fontSize: 12,
+ ),
+ ),
+ style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)),
+ onTap: _openRetailersGrid,
+ readOnly: true,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildCategoriesSection() {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 22),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ LocaleKeys.exploreYourBenefits.tr().toText12(
+ color: const Color(0xFF78817F),
+ weight: FontWeight.w500,
+ ),
+ InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap:
+ () => showMazayaCitySelectionBottomSheet(
+ context,
+ selectedCity: selectedCity,
+ onCitySelected:
+ (city) => setState(() => selectedCity = city),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SvgPicture.asset(
+ "assets/icons/mazaya_map_pin.svg",
+ width: 12,
+ height: 12,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFF147D6B),
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(width: 3),
+ selectedCity.toText12(color: const Color(0xFF147D6B)),
+ const SizedBox(width: 3),
+ SvgPicture.asset(
+ "assets/icons/mazaya_chevron_down.svg",
+ width: 12,
+ height: 12,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFF147D6B),
+ BlendMode.srcIn,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ SizedBox(
+ height: 112,
+ child:
+ _isLoadingCategories
+ ? Shimmer.fromColors(
+ baseColor: const Color(0xFFE2E8F0),
+ highlightColor: const Color(0xFFF8FAFC),
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: 6,
+ separatorBuilder:
+ (BuildContext _, int __) =>
+ const SizedBox(width: 16),
+ itemBuilder:
+ (BuildContext _, int __) => Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 72,
+ height: 72,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ width: 60,
+ height: 10,
+ color: Colors.white,
+ ),
+ ],
+ ),
+ ),
+ )
+ : ListView.separated(
+ scrollDirection: Axis.horizontal,
+ physics: const BouncingScrollPhysics(),
+ itemCount: _categories.length,
+ separatorBuilder:
+ (BuildContext _, int __) => const SizedBox(width: 16),
+ itemBuilder: (BuildContext _, int index) {
+ WalaPlusCategoryModel category = _categories[index];
+ return InkWell(
+ borderRadius: BorderRadius.circular(12),
+ onTap: () => _openRetailersGrid(category: category),
+ child: SizedBox(
+ width: 72,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child:
+ (category.image != null &&
+ category.image!.isNotEmpty)
+ ? Image.network(
+ category.image!,
+ width: 72,
+ height: 72,
+ fit: BoxFit.cover,
+ errorBuilder:
+ (_, __, ___) => Container(
+ width: 72,
+ height: 72,
+ color: const Color(
+ 0xFFF1F5F9,
+ ),
+ child: const Icon(
+ Icons.category_outlined,
+ color: Color(0xFF94A3B8),
+ ),
+ ),
+ )
+ : Container(
+ width: 72,
+ height: 72,
+ color: const Color(0xFFF1F5F9),
+ child: const Icon(
+ Icons.category_outlined,
+ color: Color(0xFF94A3B8),
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ (category.name ?? '').toText10(
+ color: const Color(0xFF111111),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildExclusiveOffersSection() {
+ return Container(
+ width: double.infinity,
+ color: const Color(0xFF143F36),
+ padding: const EdgeInsets.only(top: 23, bottom: 24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 22),
+ child: LocaleKeys.theMazayaEdit.tr().toText10(
+ color: const Color(0xFFDCC58F),
+ ),
+ ),
+ const SizedBox(height: 4),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 22),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ LocaleKeys.exclusivelyYours.tr().toText20(
+ color: Colors.white,
+ weight: FontWeight.w600,
+ ),
+ InkWell(
+ onTap: _openExclusiveOffersGrid,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ LocaleKeys.seeAll.tr().toText11(
+ color: const Color(0xFFF1EBDD),
+ weight: FontWeight.w500,
+ ),
+ const SizedBox(width: 4),
+ SvgPicture.asset(
+ "assets/icons/mazaya_arrow_right.svg",
+ width: 15,
+ height: 15,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFFF1EBDD),
+ BlendMode.srcIn,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ LocaleKeys.aLittleExtraJustForMazayaMembers.tr().toText11(
+ color: const Color(0xFFC2D4CC),
+ weight: FontWeight.w400,
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 17),
+ SizedBox(
+ height: 206,
+ child:
+ _isLoadingExclusiveOffers
+ ? Shimmer.fromColors(
+ baseColor: const Color(0xFF1B4C41),
+ highlightColor: const Color(0xFF225B4E),
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: const EdgeInsets.only(left: 22),
+ itemCount: 2,
+ separatorBuilder:
+ (BuildContext _, int __) =>
+ const SizedBox(width: 12),
+ itemBuilder:
+ (BuildContext _, int __) => Container(
+ width: 260,
+ height: 206,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ ),
+ ),
+ )
+ : ListView.separated(
+ scrollDirection: Axis.horizontal,
+ physics: const BouncingScrollPhysics(),
+ padding: const EdgeInsets.only(left: 22),
+ itemCount: _exclusiveOffers.length,
+ separatorBuilder:
+ (BuildContext _, int __) => const SizedBox(width: 12),
+ itemBuilder:
+ (BuildContext _, int index) =>
+ _buildExclusiveOfferCard(_exclusiveOffers[index]),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildExclusiveOfferCard(WalaPlusRetailerModel offer) {
+ String? imageUrl = offer.displayImage;
+ String offerText = offer.offerTitle ?? offer.description ?? '';
+ return InkWell(
+ borderRadius: BorderRadius.circular(16),
+ onTap: () => _openRetailer(offer),
+ child: Container(
+ width: 260,
+ height: 206,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF1EBDD),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ clipBehavior: Clip.antiAlias,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ height: 128,
+ width: double.infinity,
+ child:
+ (imageUrl != null && imageUrl.isNotEmpty)
+ ? Image.network(
+ imageUrl,
+ fit: BoxFit.cover,
+ errorBuilder:
+ (_, __, ___) => Container(
+ color: const Color(0xFFE4DCC8),
+ child: const Icon(
+ Icons.image_outlined,
+ color: Color(0xFF9C8F6E),
+ ),
+ ),
+ )
+ : Container(
+ color: const Color(0xFFE4DCC8),
+ child: const Icon(
+ Icons.image_outlined,
+ color: Color(0xFF9C8F6E),
+ ),
+ ),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 15),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ (offer.name ?? '').toText13(
+ color: const Color(0xFF143F36),
+ weight: FontWeight.w600,
+ maxlines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 3),
+ offerText.toText12(
+ color: const Color(0xFF143F36),
+ weight: FontWeight.w400,
+ maxLine: 1,
+ ),
+ ],
+ ),
+ ),
+ SvgPicture.asset(
+ "assets/icons/mazaya_arrow_up_right.svg",
+ width: 20,
+ height: 20,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFF143F36),
+ BlendMode.srcIn,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBrandSection({
+ required String title,
+ required String subtitle,
+ required bool isLoading,
+ required List retailers,
+ required Color backgroundColor,
+ required VoidCallback onSeeAll,
+ }) {
+ if (!isLoading && retailers.isEmpty) return const SizedBox.shrink();
+ return Container(
+ width: double.infinity,
+ color: backgroundColor,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 22),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ title.toText20(
+ color: const Color(0xFF202F32),
+ weight: FontWeight.w600,
+ ),
+ InkWell(
+ onTap: onSeeAll,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ LocaleKeys.seeAll.tr().toText11(
+ color: const Color(0xFF147D6B),
+ weight: FontWeight.w500,
+ ),
+ const SizedBox(width: 4),
+ SvgPicture.asset(
+ "assets/icons/mazaya_arrow_right.svg",
+ width: 15,
+ height: 15,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFF147D6B),
+ BlendMode.srcIn,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ subtitle.toText11(
+ color: const Color(0xFF78817F),
+ weight: FontWeight.w400,
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ SizedBox(
+ height: 110,
+ child:
+ isLoading
+ ? Shimmer.fromColors(
+ baseColor: const Color(0xFFE2E8F0),
+ highlightColor: const Color(0xFFF8FAFC),
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: const EdgeInsets.only(left: 22),
+ itemCount: 4,
+ separatorBuilder:
+ (BuildContext _, int __) =>
+ const SizedBox(width: 20),
+ itemBuilder:
+ (BuildContext _, int __) => const Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ CircleAvatar(
+ radius: 36,
+ backgroundColor: Colors.white,
+ ),
+ ],
+ ),
+ ),
+ )
+ : ListView.separated(
+ scrollDirection: Axis.horizontal,
+ physics: const BouncingScrollPhysics(),
+ padding: const EdgeInsets.only(left: 22),
+ itemCount: retailers.length,
+ separatorBuilder:
+ (BuildContext _, int __) => const SizedBox(width: 20),
+ itemBuilder:
+ (BuildContext _, int index) =>
+ _buildBrandItem(retailers[index]),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildBrandItem(WalaPlusRetailerModel item) {
+ String? logoUrl =
+ (item.logoThumb != null && item.logoThumb!.isNotEmpty)
+ ? item.logoThumb
+ : item.logo;
+ return InkWell(
+ onTap: () => _openRetailer(item),
+ borderRadius: BorderRadius.circular(36),
+ child: SizedBox(
+ width: 80,
+ child: Column(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(36),
+ child:
+ (logoUrl != null && logoUrl.isNotEmpty)
+ ? Image.network(
+ logoUrl,
+ width: 72,
+ height: 72,
+ fit: BoxFit.cover,
+ errorBuilder:
+ (_, __, ___) => Container(
+ width: 72,
+ height: 72,
+ color: const Color(0xFFF1F5F9),
+ child: const Icon(
+ Icons.storefront_outlined,
+ color: Color(0xFF94A3B8),
+ ),
+ ),
+ )
+ : Container(
+ width: 72,
+ height: 72,
+ color: const Color(0xFFF1F5F9),
+ child: const Icon(
+ Icons.storefront_outlined,
+ color: Color(0xFF94A3B8),
+ ),
+ ),
+ ),
+ const SizedBox(height: 6),
+ (item.name ?? '').toText11(
+ color: const Color(0xFF202F32),
+ weight: FontWeight.w600,
+ maxLine: 1,
+ overflow: TextOverflow.ellipsis,
+ isCenter: true,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/ui/landing/wala_plus_retailer_details_screen.dart b/lib/ui/mazaya/retailer_details_screen.dart
similarity index 56%
rename from lib/ui/landing/wala_plus_retailer_details_screen.dart
rename to lib/ui/mazaya/retailer_details_screen.dart
index 5ac2792..2c0f286 100644
--- a/lib/ui/landing/wala_plus_retailer_details_screen.dart
+++ b/lib/ui/mazaya/retailer_details_screen.dart
@@ -28,18 +28,23 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:shimmer/shimmer.dart';
import 'package:url_launcher/url_launcher.dart';
-class WalaPlusRetailerDetailsScreen extends StatefulWidget {
+class RetailerDetailsScreen extends StatefulWidget {
final WalaPlusRetailerModel? retailer;
final String? retailerId;
final String? userReference;
- const WalaPlusRetailerDetailsScreen({Key? key, this.retailer, this.retailerId, this.userReference}) : super(key: key);
+ const RetailerDetailsScreen({
+ Key? key,
+ this.retailer,
+ this.retailerId,
+ this.userReference,
+ }) : super(key: key);
@override
- State createState() => _WalaPlusRetailerDetailsScreenState();
+ State createState() => _RetailerDetailsScreenState();
}
-class _WalaPlusRetailerDetailsScreenState extends State {
+class _RetailerDetailsScreenState extends State {
WalaPlusRetailerModel? _retailer;
bool _isLoading = true;
bool _hasDetailsData = false;
@@ -91,7 +96,14 @@ class _WalaPlusRetailerDetailsScreenState extends State AppInAppWebViewScreen(url: url.startsWith("http") ? url : "https://$url", title: _retailer?.name ?? LocaleKeys.visitMerchantWebsite.tr()),
+ builder:
+ (BuildContext context) => AppInAppWebViewScreen(
+ url: url.startsWith("http") ? url : "https://$url",
+ title:
+ _retailer?.name ?? LocaleKeys.visitMerchantWebsite.tr(),
+ ),
),
);
} catch (err) {
@@ -229,9 +285,17 @@ class _WalaPlusRetailerDetailsScreenState extends State AppInAppWebViewScreen(url: url, title: title, onPageLoaded: shouldStartScanning ? _clickStartScanningButton : null)),
+ MaterialPageRoute(
+ builder:
+ (BuildContext context) => AppInAppWebViewScreen(
+ url: url,
+ title: title,
+ onPageLoaded:
+ shouldStartScanning ? _clickStartScanningButton : null,
+ ),
+ ),
);
}
@@ -410,8 +509,14 @@ class _WalaPlusRetailerDetailsScreenState extends State _redeemOffer({required String offerId, required String offerType}) async {
+ Future _redeemOffer({
+ required String offerId,
+ required String offerType,
+ }) async {
if (_isRedeeming) return;
if (offerId.isEmpty) {
Utils.showToast(LocaleKeys.offerIdNotAvailable.tr());
@@ -436,13 +547,24 @@ class _WalaPlusRetailerDetailsScreenState extends State? res = await OffersAndDiscountsApiClient().redeemWalaPlusOffer(offerId: offerId, offerType: offerType, context: context);
+ Map? res = await OffersAndDiscountsApiClient()
+ .redeemWalaPlusOffer(
+ offerId: offerId,
+ offerType: offerType,
+ context: context,
+ );
Utils.hideLoading(context);
String? transactionUrl;
if (res != null) {
- transactionUrl = res['Transaction_Url'] ?? res['TransactionUrl'] ?? res['transaction_url'] ?? res['transactionUrl'] ?? res['Url'] ?? res['url'];
+ transactionUrl =
+ res['Transaction_Url'] ??
+ res['TransactionUrl'] ??
+ res['transaction_url'] ??
+ res['transactionUrl'] ??
+ res['Url'] ??
+ res['url'];
}
log("transactionUrl: ${transactionUrl.toString()}");
@@ -450,7 +572,11 @@ class _WalaPlusRetailerDetailsScreenState extends State offers) {
- bool isAr = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context);
+ bool isAr =
+ Directionality.of(context) == ui.TextDirection.rtl ||
+ AppState().isArabic(context);
showModalBottomSheet(
context: context,
@@ -481,14 +609,28 @@ class _WalaPlusRetailerDetailsScreenState extends State Navigator.pop(sheetContext),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
@@ -526,7 +677,11 @@ class _WalaPlusRetailerDetailsScreenState extends State 14.height,
itemBuilder: (BuildContext _, int index) {
WalaPlusOfferModel offer = offers[index];
- return _buildOfferBottomSheetCard(sheetContext, offer, isAr);
+ return _buildOfferBottomSheetCard(
+ sheetContext,
+ offer,
+ isAr,
+ );
},
),
),
@@ -537,19 +692,33 @@ class _WalaPlusRetailerDetailsScreenState extends State(Color(0xFF168A89)),
- ),
- Expanded(
- child: SingleChildScrollView(
- physics: const BouncingScrollPhysics(),
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // 1. Breadcrumbs (Category / Company Name)
- _buildBreadcrumbs(isAr),
- 12.height,
+ body:
+ (_isLoading && !_hasDetailsData)
+ ? _buildDetailsShimmer()
+ : Column(
+ children: [
+ if (_isLoading && _hasDetailsData)
+ const LinearProgressIndicator(
+ minHeight: 2.5,
+ backgroundColor: Colors.transparent,
+ valueColor: AlwaysStoppedAnimation(
+ Color(0xFF168A89),
+ ),
+ ),
+ Expanded(
+ child: SingleChildScrollView(
+ physics: const BouncingScrollPhysics(),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 1. Breadcrumbs (Category / Company Name)
+ _buildBreadcrumbs(isAr),
+ 12.height,
- // 2. Hero Card (Top Image with Favorite Heart + Bottom #143F36 Dark Green Container)
- _buildHeroCard(isAr),
- 14.height,
+ // 2. Hero Card (Top Image with Favorite Heart + Bottom #143F36 Dark Green Container)
+ _buildHeroCard(isAr),
+ 14.height,
- // 3. Store Type & City Filter Chips
- _buildChipsRow(isAr),
- 20.height,
+ // 3. Store Type & City Filter Chips
+ _buildChipsRow(isAr),
+ 20.height,
- // 4. Merchant Contact (Website, Phone, Email)
- _buildMerchantContactSection(isAr),
+ // 4. Merchant Contact (Website, Phone, Email)
+ _buildMerchantContactSection(isAr),
- // 5. Locations Section (Preview Cards + View All)
- _buildLocationsSection(isAr),
+ // 5. Locations Section (Preview Cards + View All)
+ _buildLocationsSection(isAr),
- // 6. Good to Know Section
- _buildGoodToKnowSection(isAr),
+ // 6. Good to Know Section
+ _buildGoodToKnowSection(isAr),
- 20.height,
- ],
+ 20.height,
+ ],
+ ),
),
),
- ),
- // 7. Bottom Redeem Offer Action Bar
- _buildBottomActionBar(isAr),
- ],
- ),
+ // 7. Bottom Redeem Offer Action Bar - not shown for exclusive offers (mapped
+ // from the legacy Offers & Discounts API), since there's no real WalaPlus
+ // offer/retailer ID behind them for redeemWalaPlusOffer to act on.
+ if (_retailer?.isExclusive != true)
+ _buildBottomActionBar(isAr),
+ ],
+ ),
);
}
// Skeleton mirroring the real card layout (breadcrumbs, hero card, chips, contact, locations)
Widget _buildDetailsShimmer() {
Widget block({double? width, double height = 14, double radius = 4}) {
- return Container(width: width, height: height, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(radius)));
+ return Container(
+ width: width,
+ height: height,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(radius),
+ ),
+ );
}
return Shimmer.fromColors(
@@ -673,7 +889,10 @@ class _WalaPlusRetailerDetailsScreenState extends State[
- block(width: 80, height: 32, radius: 20),
- const SizedBox(width: 8),
- block(width: 80, height: 32, radius: 20),
- const SizedBox(width: 8),
- block(width: 100, height: 32, radius: 20),
- ]),
+ Row(
+ children: [
+ block(width: 80, height: 32, radius: 20),
+ const SizedBox(width: 8),
+ block(width: 80, height: 32, radius: 20),
+ const SizedBox(width: 8),
+ block(width: 100, height: 32, radius: 20),
+ ],
+ ),
20.height,
// Merchant contact placeholder
block(width: 140, height: 18),
@@ -724,7 +945,10 @@ class _WalaPlusRetailerDetailsScreenState extends State Container(
+ child:
+ (_retailer?.displayImage != null &&
+ _retailer!.displayImage!.isNotEmpty)
+ ? Image.network(
+ _retailer!.displayImage!,
+ fit: BoxFit.cover,
+ errorBuilder:
+ (_, __, ___) => Container(
+ color: const Color(0xFFF3F4F6),
+ child: const Icon(
+ Icons.broken_image,
+ size: 48,
+ color: Color(0xFF9CA3AF),
+ ),
+ ),
+ )
+ : Container(
color: const Color(0xFFF3F4F6),
- child: const Icon(Icons.broken_image, size: 48, color: Color(0xFF9CA3AF)),
+ child: const Icon(
+ Icons.image_outlined,
+ size: 48,
+ color: Color(0xFF9CA3AF),
+ ),
),
- )
- : Container(
- color: const Color(0xFFF3F4F6),
- child: const Icon(Icons.image_outlined, size: 48, color: Color(0xFF9CA3AF)),
- ),
),
- PositionedDirectional(
- top: 14,
- end: 14,
- child: Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: Colors.white,
- boxShadow: [
- BoxShadow(color: Colors.black.withOpacity(0.14), blurRadius: 8, offset: const Offset(0, 2)),
- ],
- ),
- child: IconButton(
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints(),
- icon: Icon(
- _isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded,
- size: 22,
- color: _isFavorite ? Colors.black : const Color(0xFF1E293B),
+ // Exclusive offers (mapped from the legacy Offers & Discounts API) have no
+ // WalaPlus retailer ID, so there's nothing for add/removeWalaPlusFavorite to
+ // act on - hide the button rather than wiring it to a call that can't work.
+ if (_retailer?.isExclusive != true)
+ PositionedDirectional(
+ top: 14,
+ end: 14,
+ child: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.white,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.14),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ icon: Icon(
+ _isFavorite
+ ? Icons.favorite_rounded
+ : Icons.favorite_border_rounded,
+ size: 22,
+ color:
+ _isFavorite
+ ? Colors.black
+ : const Color(0xFF1E293B),
+ ),
+ tooltip: LocaleKeys.favorite.tr(),
+ onPressed:
+ _isTogglingFavorite ? null : _toggleFavorite,
),
- tooltip: LocaleKeys.favorite.tr(),
- onPressed: _isTogglingFavorite ? null : _toggleFavorite,
),
),
- ),
],
),
),
@@ -826,14 +1090,26 @@ class _WalaPlusRetailerDetailsScreenState extends State chips = [];
+ // Exclusive offers (mapped from the legacy Offers & Discounts API) carry no real
+ // type/locations/offers data, so there's nothing to infer Online/In Store from - the
+ // "neither known -> assume In Store" fallback below would otherwise show a wrong chip.
+ bool isExclusive = _retailer?.isExclusive == true;
+
List types = _retailer?.normalizedTypes ?? [];
- bool hasOnline = types.contains('online') || (_retailer?.isOnlineStore == true);
- bool hasInstore = types.contains('instore') || (_retailer?.isInstore == true) || (_retailer?.hasPhysicalLocation == true) || (_retailer?.locations != null && _retailer!.locations!.isNotEmpty);
+ bool hasOnline =
+ types.contains('online') || (_retailer?.isOnlineStore == true);
+ bool hasInstore =
+ types.contains('instore') ||
+ (_retailer?.isInstore == true) ||
+ (_retailer?.hasPhysicalLocation == true) ||
+ (_retailer?.locations != null && _retailer!.locations!.isNotEmpty);
String rType = (_retailer?.type ?? '').toLowerCase();
String rOfferType = (_retailer?.offerType ?? '').toLowerCase();
- if (rType.contains('online') || rOfferType.contains('online')) hasOnline = true;
- if (rType.contains('store') || rOfferType.contains('store')) hasInstore = true;
+ if (rType.contains('online') || rOfferType.contains('online'))
+ hasOnline = true;
+ if (rType.contains('store') || rOfferType.contains('store'))
+ hasInstore = true;
if (rType.isNotEmpty && !rType.contains('online')) hasInstore = true;
if (_retailer?.offers != null) {
for (var offer in _retailer!.offers!) {
String oType = (offer.type ?? '').toLowerCase();
if (oType.contains('online')) hasOnline = true;
- if (oType.contains('store') || (oType.isNotEmpty && !oType.contains('online'))) hasInstore = true;
+ if (oType.contains('store') ||
+ (oType.isNotEmpty && !oType.contains('online')))
+ hasInstore = true;
}
}
- if (!hasOnline && !hasInstore) {
+ if (!isExclusive && !hasOnline && !hasInstore) {
hasInstore = true;
}
+ if (isExclusive) {
+ hasOnline = false;
+ hasInstore = false;
+ }
// Online chip
if (hasOnline) {
@@ -876,9 +1170,11 @@ class _WalaPlusRetailerDetailsScreenState extends State _showAllLocationsBottomSheet(_retailer!.locations!, isAr),
+ onTap:
+ () => _showAllLocationsBottomSheet(_retailer!.locations!, isAr),
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
@@ -948,7 +1250,9 @@ class _WalaPlusRetailerDetailsScreenState extends State(Colors.white)),
+ child:
+ _isRedeeming
+ ? const Center(
+ child: SizedBox(
+ width: 22,
+ height: 22,
+ child: CircularProgressIndicator(
+ strokeWidth: 2.5,
+ valueColor: AlwaysStoppedAnimation(
+ Colors.white,
+ ),
+ ),
+ ),
+ )
+ : Row(
+ children: [
+ LocaleKeys.redeemOffer.tr().toText16(
+ isBold: true,
+ color: Colors.white,
+ isCentered: false,
+ ),
+ const Spacer(),
+ Icon(
+ isAr
+ ? Icons.arrow_back_rounded
+ : Icons.arrow_forward_rounded,
+ color: Colors.white,
+ size: 20,
+ ),
+ ],
),
- )
- : Row(
- children: [
- LocaleKeys.redeemOffer.tr().toText16(isBold: true, color: Colors.white, isCentered: false),
- const Spacer(),
- Icon(isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, color: Colors.white, size: 20),
- ],
- ),
),
),
),
@@ -1188,7 +1605,8 @@ class _WalaPlusRetailerDetailsScreenState extends State previewLocations = totalCount <= 2 ? locations : locations.take(2).toList();
+ List previewLocations =
+ totalCount <= 2 ? locations : locations.take(2).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1197,7 +1615,10 @@ class _WalaPlusRetailerDetailsScreenState extends State _showAllLocationsBottomSheet(locations, isAr),
borderRadius: BorderRadius.circular(6),
@@ -1206,9 +1627,18 @@ class _WalaPlusRetailerDetailsScreenState extends State _openMapForLocation(loc),
@@ -1252,7 +1685,11 @@ class _WalaPlusRetailerDetailsScreenState extends State locations, bool isAr) {
+ void _showAllLocationsBottomSheet(
+ List locations,
+ bool isAr,
+ ) {
Map cityCounts = {};
for (var loc in locations) {
String c = loc.getLocalizedCity(isAr);
@@ -1294,7 +1749,9 @@ class _WalaPlusRetailerDetailsScreenState extends State sortedCities = cityCounts.keys.toList()..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0));
+ List sortedCities =
+ cityCounts.keys.toList()
+ ..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0));
String selectedCity = "ALL";
String searchQuery = "";
@@ -1320,7 +1777,10 @@ class _WalaPlusRetailerDetailsScreenState extends State Navigator.pop(sheetContext),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
@@ -1370,20 +1853,36 @@ class _WalaPlusRetailerDetailsScreenState extends State 10.height,
itemBuilder: (BuildContext _, int index) {
- return _buildLocationCard(filtered[index], isAr);
+ return _buildLocationCard(
+ filtered[index],
+ isAr,
+ );
},
),
),
@@ -1495,17 +2046,25 @@ class _WalaPlusRetailerDetailsScreenState extends State _openMapForLocation(WalaPlusRetailerLocation loc) async {
- bool hasCoordinates = loc.latitude != null && loc.longitude != null && loc.latitude != 0 && loc.longitude != 0;
+ bool hasCoordinates =
+ loc.latitude != null &&
+ loc.longitude != null &&
+ loc.latitude != 0 &&
+ loc.longitude != 0;
Uri googleMapsUrl;
if (hasCoordinates) {
- googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${loc.latitude},${loc.longitude}");
+ googleMapsUrl = Uri.parse(
+ "https://www.google.com/maps/search/?api=1&query=${loc.latitude},${loc.longitude}",
+ );
} else {
String query = "${loc.address ?? ''} ${loc.city ?? ''}".trim();
if (query.isEmpty) {
query = _retailer?.name ?? "";
}
- googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${Uri.encodeComponent(query)}");
+ googleMapsUrl = Uri.parse(
+ "https://www.google.com/maps/search/?api=1&query=${Uri.encodeComponent(query)}",
+ );
}
try {
diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/mazaya/retailers_list_screen.dart
similarity index 55%
rename from lib/ui/landing/dashboard_screen_for_dependet.dart
rename to lib/ui/mazaya/retailers_list_screen.dart
index 2da332c..e1c65d1 100644
--- a/lib/ui/landing/dashboard_screen_for_dependet.dart
+++ b/lib/ui/mazaya/retailers_list_screen.dart
@@ -14,11 +14,13 @@ import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
-import 'package:mohem_flutter_app/main.dart';
import 'package:mohem_flutter_app/models/generic_response_model.dart';
+import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
import 'package:mohem_flutter_app/models/post_params_model.dart';
+import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart';
+import 'package:mohem_flutter_app/ui/mazaya/widgets/wala_plus_retailer_grid.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:mohem_flutter_app/widgets/info_chip_widget.dart';
@@ -26,23 +28,30 @@ import 'package:mohem_flutter_app/widgets/location/Location.dart' as location;
import 'package:permission_handler/permission_handler.dart';
import 'package:shimmer/shimmer.dart';
-class DashboardScreenForDependet extends StatefulWidget {
- const DashboardScreenForDependet({Key? key}) : super(key: key);
+class RetailersListScreen extends StatefulWidget {
+ const RetailersListScreen({Key? key}) : super(key: key);
@override
- State createState() => _DashboardScreenForDependetState();
+ State createState() => _RetailersListScreenState();
}
-class _DashboardScreenForDependetState extends State {
+class _RetailersListScreenState extends State {
List retailersList = [];
List _filteredRetailersList = [];
List categoriesList = [];
WalaPlusCategoryModel? selectedCategory;
String currentCategoryId = "";
String selectedTypeFilter = "all";
- String selectedCity = "Riyadh";
+ String selectedCity = "All Cities";
bool _isInitRouteArgs = false;
+ // Reached from Discover's "Exclusive offers" See all - reuses the legacy Offers & Discounts
+ // (ITG) API/implementation (lib/ui/screens/offers_and_discounts) instead of a WalaPlus
+ // endpoint, so fetchCategories/fetchOffers/fetchSubcategories all short-circuit to
+ // fetchExclusiveOffers below rather than touching any WalaPlus-specific state or UI.
+ bool isExclusiveMode = false;
+ List _exclusiveOffers = [];
+
// Subcategories of whichever main category is currently selected - fetched via
// getWalaPlusCategories(parentId: ...) whenever a main category card is tapped. Empty/cleared
// when "All Categories" (or nothing) is selected, since there's no specific parent to ask for.
@@ -94,7 +103,9 @@ class _DashboardScreenForDependetState extends State
if (!_isInitRouteArgs) {
_isInitRouteArgs = true;
Object? args = ModalRoute.of(context)?.settings.arguments;
- if (args is WalaPlusCategoryModel) {
+ if (args is Map && args['exclusiveMode'] == true) {
+ isExclusiveMode = true;
+ } else if (args is WalaPlusCategoryModel) {
selectedCategory = args;
currentCategoryId = args.id ?? "";
} else if (args is Map && args['category'] is WalaPlusCategoryModel) {
@@ -116,19 +127,27 @@ class _DashboardScreenForDependetState extends State
String name = AppState().dependentName ?? "";
String mobile = AppState().postParamsObject?.mobileNumber ?? "";
String tokenID =
- (AppState().postParamsObject?.tokenID != null && AppState().postParamsObject!.tokenID!.isNotEmpty) ? AppState().postParamsObject!.tokenID! : (AppState().authenticationTokenID ?? "");
+ (AppState().postParamsObject?.tokenID != null &&
+ AppState().postParamsObject!.tokenID!.isNotEmpty)
+ ? AppState().postParamsObject!.tokenID!
+ : (AppState().authenticationTokenID ?? "");
if (tokenID.isEmpty) {
tokenID = await Utils.getStringFromPrefs("P_DEP_TOKEN_ID");
}
int sessionId = AppState().postParamsObject?.pSessionId ?? 0;
String email = AppState().postParamsObject?.pEmailAddress ?? "";
- String deviceToken = AppState().getIsHuawei
- ? AppState().getHuaweiPushToken
- : ((AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null")
- ? AppState().getDeviceToken!
- : "");
+ String deviceToken =
+ AppState().getIsHuawei
+ ? AppState().getHuaweiPushToken
+ : ((AppState().getDeviceToken != null &&
+ AppState().getDeviceToken!.isNotEmpty &&
+ AppState().getDeviceToken != "null")
+ ? AppState().getDeviceToken!
+ : "");
if (deviceToken.isEmpty || deviceToken == "null") {
- String savedToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN");
+ String savedToken = await Utils.getStringFromPrefs(
+ "P_DEP_DEVICE_TOKEN",
+ );
if (savedToken.isNotEmpty && savedToken != "null") {
deviceToken = savedToken;
}
@@ -154,13 +173,18 @@ class _DashboardScreenForDependetState extends State
}
void _loadDependentName() {
- if (AppState().dependentName != null && AppState().dependentName!.isNotEmpty && mounted) {
+ if (AppState().dependentName != null &&
+ AppState().dependentName!.isNotEmpty &&
+ mounted) {
setState(() {});
}
}
String _cleanDependentName(String name) {
- return name.replaceAll(RegExp(r'\s*-\s*'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
+ return name
+ .replaceAll(RegExp(r'\s*-\s*'), ' ')
+ .replaceAll(RegExp(r'\s+'), ' ')
+ .trim();
}
@override
@@ -184,16 +208,73 @@ class _DashboardScreenForDependetState extends State
}
}
+ // Reuses the legacy Offers & Discounts (ITG) API/implementation
+ // (lib/ui/screens/offers_and_discounts) rather than a WalaPlus endpoint - categoryID 0 is the
+ // same "all categories" value that screen's own initial load uses. There's no query param on
+ // getOffersList, so search filters the already-fetched list client-side instead of re-fetching.
+ // Each result is mapped onto WalaPlusRetailerModel (see fromExclusiveOffer) so it renders
+ // through the exact same grid/card used everywhere else, flagged via isExclusive.
+ Future fetchExclusiveOffers({
+ String? query,
+ bool forceRefresh = false,
+ }) async {
+ if (query != null) searchQuery = query;
+ if (forceRefresh) _exclusiveOffers = [];
+ setState(() {
+ isLoading = true;
+ hasMore = false;
+ });
+ try {
+ if (_exclusiveOffers.isEmpty) {
+ bool isAr = AppState().isArabic(context);
+ List offers = await OffersAndDiscountsApiClient()
+ .getOffersList(0, 100);
+ _exclusiveOffers =
+ offers
+ .map(
+ (o) =>
+ WalaPlusRetailerModel.fromExclusiveOffer(o, isAr: isAr),
+ )
+ .toList();
+ }
+ retailersList = _exclusiveOffers;
+ String q = searchQuery.trim().toLowerCase();
+ _filteredRetailersList =
+ q.isEmpty
+ ? _exclusiveOffers
+ : _exclusiveOffers
+ .where((o) => (o.name ?? '').toLowerCase().contains(q))
+ .toList();
+ } catch (e) {
+ debugPrint("RetailersListScreen: error fetching exclusive offers: $e");
+ _filteredRetailersList = [];
+ } finally {
+ if (mounted) {
+ setState(() {
+ isLoading = false;
+ isCategoriesLoading = false;
+ });
+ }
+ }
+ }
+
Future fetchCategories() async {
+ if (isExclusiveMode) {
+ await fetchExclusiveOffers();
+ return;
+ }
setState(() {
isCategoriesLoading = true;
});
try {
- List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context);
+ List list = await OffersAndDiscountsApiClient()
+ .getWalaPlusCategories(context: context);
if (list.isEmpty) {
await Future.delayed(const Duration(milliseconds: 600));
- list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context);
+ list = await OffersAndDiscountsApiClient().getWalaPlusCategories(
+ context: context,
+ );
}
if (mounted) {
@@ -210,14 +291,29 @@ class _DashboardScreenForDependetState extends State
WalaPlusCategoryModel? defaultCat;
for (WalaPlusCategoryModel cat in list) {
String n = (cat.name ?? "").toLowerCase();
- if (n.contains("travel") || n.contains("entertain") || n.contains("سفر") || n.contains("ترفيه")) {
+ if (n.contains("travel") ||
+ n.contains("entertain") ||
+ n.contains("سفر") ||
+ n.contains("ترفيه")) {
defaultCat = cat;
break;
}
}
defaultCat ??= list.firstWhere(
- (c) => c.id != "all" && c.id != "0" && (c.name ?? "").toLowerCase() != "all" && c.name != "الكل" && c.name != "الجميع",
- orElse: () => list.isNotEmpty ? list.first : WalaPlusCategoryModel(id: "1", name: "Travel & Entertainment"),
+ (c) =>
+ c.id != "all" &&
+ c.id != "0" &&
+ (c.name ?? "").toLowerCase() != "all" &&
+ c.name != "الكل" &&
+ c.name != "الجميع",
+ orElse:
+ () =>
+ list.isNotEmpty
+ ? list.first
+ : WalaPlusCategoryModel(
+ id: "1",
+ name: "Travel & Entertainment",
+ ),
);
selectedCategory = defaultCat;
}
@@ -234,7 +330,10 @@ class _DashboardScreenForDependetState extends State
debugPrint("Error fetching WalaPlus categories (attempt 1): $e");
try {
await Future.delayed(const Duration(milliseconds: 600));
- List retryList = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context);
+ List retryList =
+ await OffersAndDiscountsApiClient().getWalaPlusCategories(
+ context: context,
+ );
if (mounted && retryList.isNotEmpty) {
categoriesList = retryList;
if (selectedCategory != null) {
@@ -248,13 +347,21 @@ class _DashboardScreenForDependetState extends State
WalaPlusCategoryModel? defaultCat;
for (WalaPlusCategoryModel cat in retryList) {
String n = (cat.name ?? "").toLowerCase();
- if (n.contains("travel") || n.contains("entertain") || n.contains("سفر") || n.contains("ترفيه")) {
+ if (n.contains("travel") ||
+ n.contains("entertain") ||
+ n.contains("سفر") ||
+ n.contains("ترفيه")) {
defaultCat = cat;
break;
}
}
defaultCat ??= retryList.firstWhere(
- (c) => c.id != "all" && c.id != "0" && (c.name ?? "").toLowerCase() != "all" && c.name != "الكل" && c.name != "الجميع",
+ (c) =>
+ c.id != "all" &&
+ c.id != "0" &&
+ (c.name ?? "").toLowerCase() != "all" &&
+ c.name != "الكل" &&
+ c.name != "الجميع",
orElse: () => retryList.first,
);
selectedCategory = defaultCat;
@@ -273,7 +380,10 @@ class _DashboardScreenForDependetState extends State
if (mounted) {
if (selectedCategory == null) {
- selectedCategory = WalaPlusCategoryModel(id: "1", name: "Travel & Entertainment");
+ selectedCategory = WalaPlusCategoryModel(
+ id: "1",
+ name: "Travel & Entertainment",
+ );
currentCategoryId = "1";
}
setState(() {});
@@ -291,6 +401,7 @@ class _DashboardScreenForDependetState extends State
// Fetches the subcategories under a main category (WP_ParentId). "All Categories" (or no
// category) has no specific parent, so that just clears the list instead of calling the API.
Future fetchSubcategories(String? parentId) async {
+ if (isExclusiveMode) return;
if (parentId == null || parentId.isEmpty || parentId == "all") {
setState(() {
subcategoriesList = [];
@@ -304,7 +415,8 @@ class _DashboardScreenForDependetState extends State
});
try {
- List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context, parentId: parentId);
+ List list = await OffersAndDiscountsApiClient()
+ .getWalaPlusCategories(context: context, parentId: parentId);
if (mounted) {
setState(() {
subcategoriesList = list;
@@ -336,7 +448,16 @@ class _DashboardScreenForDependetState extends State
}
Future fetchOffers({String? categoryId, String? query}) async {
- String targetCategoryId = categoryId ?? _selectedSubcategoryId ?? (currentCategoryId.isEmpty ? (selectedCategory?.id ?? "") : currentCategoryId);
+ if (isExclusiveMode) {
+ await fetchExclusiveOffers(query: query);
+ return;
+ }
+ String targetCategoryId =
+ categoryId ??
+ _selectedSubcategoryId ??
+ (currentCategoryId.isEmpty
+ ? (selectedCategory?.id ?? "")
+ : currentCategoryId);
if (query != null) {
searchQuery = query;
}
@@ -353,30 +474,42 @@ class _DashboardScreenForDependetState extends State
});
try {
- String catParam = (targetCategoryId == "all" || targetCategoryId == "0") ? "" : targetCategoryId;
- String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter);
- String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
+ String catParam =
+ (targetCategoryId == "all" || targetCategoryId == "0")
+ ? ""
+ : targetCategoryId;
+ String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(
+ selectedTypeFilter,
+ );
+ String? queryParam =
+ searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
int? radiusParam = _radiusFromFilterDistance(_filterDistance);
- int? minDiscountParam = _filterMinDiscount == 'any' ? null : int.tryParse(_filterMinDiscount);
- print("Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', page=1, per_page=$pageSize");
- WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
- categoryId: catParam,
- page: 1,
- perPage: pageSize,
- offerType: offerTypeParam,
- query: queryParam,
- sortBy: _filterSortBy,
- radius: radiusParam,
- minDiscount: minDiscountParam,
- context: wantsNearest ? context : null,
- userReference: null,
+ int? minDiscountParam =
+ _filterMinDiscount == 'any' ? null : int.tryParse(_filterMinDiscount);
+ print(
+ "Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', page=1, per_page=$pageSize",
);
+ WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
+ .getWalaPlusRetailers(
+ categoryId: catParam,
+ page: 1,
+ perPage: pageSize,
+ offerType: offerTypeParam,
+ query: queryParam,
+ sortBy: _filterSortBy,
+ radius: radiusParam,
+ minDiscount: minDiscountParam,
+ context: wantsNearest ? context : null,
+ userReference: null,
+ );
retailersList = response?.retailers ?? [];
_filteredRetailersList = retailersList;
currentPage = 1;
totalPages = response?.pagesCount ?? 1;
- hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false);
+ hasMore =
+ currentPage < totalPages &&
+ (response?.retailers?.isNotEmpty ?? false);
} catch (e) {
print("Error fetching WalaPlus retailers: $e");
retailersList = [];
@@ -406,26 +539,38 @@ class _DashboardScreenForDependetState extends State
try {
int nextPage = currentPage + 1;
String effectiveCategoryId = _selectedSubcategoryId ?? currentCategoryId;
- String catParam = (effectiveCategoryId == "all" || effectiveCategoryId == "0") ? "" : effectiveCategoryId;
- String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter);
- String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
+ String catParam =
+ (effectiveCategoryId == "all" || effectiveCategoryId == "0")
+ ? ""
+ : effectiveCategoryId;
+ String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(
+ selectedTypeFilter,
+ );
+ String? queryParam =
+ searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
int? radiusParam = _radiusFromFilterDistance(_filterDistance);
- int? minDiscountParam = (_filterMinDiscount == 'any') ? null : int.tryParse(_filterMinDiscount);
- print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'");
-
- WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
- categoryId: catParam,
- page: nextPage,
- perPage: pageSize,
- offerType: offerTypeParam,
- query: queryParam,
- sortBy: _filterSortBy,
- radius: radiusParam,
- minDiscount: minDiscountParam,
- context: wantsNearest ? context : null,
- userReference: null,
+ int? minDiscountParam =
+ (_filterMinDiscount == 'any')
+ ? null
+ : int.tryParse(_filterMinDiscount);
+ print(
+ "Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'",
);
+ WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
+ .getWalaPlusRetailers(
+ categoryId: catParam,
+ page: nextPage,
+ perPage: pageSize,
+ offerType: offerTypeParam,
+ query: queryParam,
+ sortBy: _filterSortBy,
+ radius: radiusParam,
+ minDiscount: minDiscountParam,
+ context: wantsNearest ? context : null,
+ userReference: null,
+ );
+
List newRetailers = response?.retailers ?? [];
if (newRetailers.isNotEmpty) {
currentPage = nextPage;
@@ -449,10 +594,24 @@ class _DashboardScreenForDependetState extends State
}
Future _onRefresh() async {
- String effectiveCategoryId = _selectedSubcategoryId ?? (currentCategoryId.isEmpty ? (selectedCategory?.id ?? "") : currentCategoryId);
+ if (isExclusiveMode) {
+ await fetchExclusiveOffers(
+ query: _searchController.text,
+ forceRefresh: true,
+ );
+ return;
+ }
+ String effectiveCategoryId =
+ _selectedSubcategoryId ??
+ (currentCategoryId.isEmpty
+ ? (selectedCategory?.id ?? "")
+ : currentCategoryId);
await Future.wait(>[
fetchCategories(),
- fetchOffers(categoryId: effectiveCategoryId, query: _searchController.text),
+ fetchOffers(
+ categoryId: effectiveCategoryId,
+ query: _searchController.text,
+ ),
]);
}
@@ -491,13 +650,16 @@ class _DashboardScreenForDependetState extends State
Completer permissionCompleter = Completer();
location.Location.havePermission((bool hasPermission) {
- if (!permissionCompleter.isCompleted) permissionCompleter.complete(hasPermission);
+ if (!permissionCompleter.isCompleted)
+ permissionCompleter.complete(hasPermission);
});
bool hasPermission = await permissionCompleter.future;
if (!hasPermission) {
bool granted = await _requestLocationPermission();
if (!granted) {
- Utils.showToast(LocaleKeys.locationPermissionRequiredForNearestOffers.tr());
+ Utils.showToast(
+ LocaleKeys.locationPermissionRequiredForNearestOffers.tr(),
+ );
return false;
}
}
@@ -505,8 +667,10 @@ class _DashboardScreenForDependetState extends State
}
Future _requestLocationPermission() async {
- Map result = await [Permission.location].request();
- return result[Permission.location] == PermissionStatus.granted || result[Permission.locationAlways] == PermissionStatus.granted;
+ Map result =
+ await [Permission.location].request();
+ return result[Permission.location] == PermissionStatus.granted ||
+ result[Permission.locationAlways] == PermissionStatus.granted;
}
void _onSearchChanged(String query) {
@@ -532,6 +696,7 @@ class _DashboardScreenForDependetState extends State
setState(() {
item.isFavorite = targetFav;
});
+ Utils.showLoading(context);
try {
GenericResponseModel? response;
@@ -550,15 +715,23 @@ class _DashboardScreenForDependetState extends State
);
}
+ Utils.hideLoading(context);
+
if (response != null && response.errorMessage == null) {
bool isAr = AppState().isArabic(context);
String msg;
if (isAr) {
- msg = targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr();
+ msg =
+ targetFav
+ ? LocaleKeys.favoriteAddedSuccess.tr()
+ : LocaleKeys.favoriteRemovedSuccess.tr();
} else {
msg = response.wpFavoritesAddRemoveMessage ?? "";
if (msg.trim().isEmpty) {
- msg = targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr();
+ msg =
+ targetFav
+ ? LocaleKeys.favoriteAddedSuccess.tr()
+ : LocaleKeys.favoriteRemovedSuccess.tr();
}
}
Utils.showToast(msg, isSuccess: true);
@@ -568,10 +741,14 @@ class _DashboardScreenForDependetState extends State
item.isFavorite = currentFav;
});
}
- String error = response?.errorEndUserMessage ?? response?.errorMessage ?? LocaleKeys.favoriteError.tr();
+ String error =
+ response?.errorEndUserMessage ??
+ response?.errorMessage ??
+ LocaleKeys.favoriteError.tr();
Utils.showToast(error);
}
} catch (e) {
+ Utils.hideLoading(context);
debugPrint("Error toggling favorite: $e");
if (mounted) {
setState(() {
@@ -595,7 +772,11 @@ class _DashboardScreenForDependetState extends State
}
}
}
- await Navigator.pushNamed(context, AppRoutes.walaPlusRetailerDetails, arguments: item);
+ await Navigator.pushNamed(
+ context,
+ AppRoutes.retailerDetailsScreen,
+ arguments: item,
+ );
if (mounted) {
setState(() {});
}
@@ -622,7 +803,10 @@ class _DashboardScreenForDependetState extends State
await Utils.saveStringFromPrefs("P_DEP_MOBILE_NUMBER", mobile);
}
String tokenID =
- (AppState().postParamsObject?.tokenID != null && AppState().postParamsObject!.tokenID!.isNotEmpty) ? AppState().postParamsObject!.tokenID! : (AppState().authenticationTokenID ?? "");
+ (AppState().postParamsObject?.tokenID != null &&
+ AppState().postParamsObject!.tokenID!.isNotEmpty)
+ ? AppState().postParamsObject!.tokenID!
+ : (AppState().authenticationTokenID ?? "");
if (tokenID.isNotEmpty) {
await Utils.saveStringFromPrefs("P_DEP_TOKEN_ID", tokenID);
}
@@ -632,11 +816,17 @@ class _DashboardScreenForDependetState extends State
}
String authTokenID = AppState().authenticationTokenID ?? "";
if (authTokenID.isNotEmpty) {
- await Utils.saveStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID", authTokenID);
+ await Utils.saveStringFromPrefs(
+ "P_DEP_AUTHENTICATION_TOKEN_ID",
+ authTokenID,
+ );
}
int loginType = AppState().dependentLoginType ?? 1;
await Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString());
- String deviceToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? "");
+ String deviceToken =
+ AppState().getIsHuawei
+ ? AppState().getHuaweiPushToken
+ : (AppState().getDeviceToken ?? "");
if (deviceToken.isNotEmpty) {
await Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", deviceToken);
}
@@ -676,7 +866,11 @@ class _DashboardScreenForDependetState extends State
AppState().setWalaplusDepInfoModel = null;
AppState().setPostParamsInitConfig();
if (mounted) {
- Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route route) => false);
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ AppRoutes.login,
+ (Route route) => false,
+ );
}
},
);
@@ -693,7 +887,9 @@ class _DashboardScreenForDependetState extends State
if (mounted) {
setState(() {});
await fetchCategories();
- await fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId);
+ await fetchOffers(
+ categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId,
+ );
}
}
@@ -709,7 +905,14 @@ class _DashboardScreenForDependetState extends State
_changeLanguage(const Locale("ar", "SA"));
}
},
- child: const Padding(padding: EdgeInsets.all(3), child: Icon(Icons.language_rounded, size: 24, color: Color(0xFF1E293B))),
+ child: const Padding(
+ padding: EdgeInsets.all(3),
+ child: Icon(
+ Icons.language_rounded,
+ size: 24,
+ color: Color(0xFF1E293B),
+ ),
+ ),
),
);
}
@@ -740,11 +943,18 @@ class _DashboardScreenForDependetState extends State
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8),
- child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B)),
+ child: Icon(
+ Icons.arrow_back_ios_new_rounded,
+ size: 20,
+ color: Color(0xFF1E293B),
+ ),
),
),
const SizedBox(width: 8),
- LocaleKeys.mazayaDeals.tr().toText22(isBold: true, color: const Color(0xFF202F31)),
+ LocaleKeys.mazayaDeals.tr().toText22(
+ isBold: true,
+ color: const Color(0xFF202F31),
+ ),
],
),
),
@@ -757,7 +967,11 @@ class _DashboardScreenForDependetState extends State
onTap: _performLogout,
child: Padding(
padding: const EdgeInsets.all(6),
- child: SvgPicture.asset('assets/images/drawer/logout.svg', height: 20, width: 20),
+ child: SvgPicture.asset(
+ 'assets/images/drawer/logout.svg',
+ height: 20,
+ width: 20,
+ ),
),
),
const SizedBox(width: 12),
@@ -770,7 +984,9 @@ class _DashboardScreenForDependetState extends State
color: MyColors.textMixColor,
child: SingleChildScrollView(
controller: _scrollController,
- physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()),
+ physics: const AlwaysScrollableScrollPhysics(
+ parent: BouncingScrollPhysics(),
+ ),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -784,38 +1000,22 @@ class _DashboardScreenForDependetState extends State
// 3. Search Bar: "Find an escape or an experience"
_buildSearchBar(isAr),
- // 4. Filters & Subcategories Row
- _buildSubcategoriesAndFilterRow(isAr),
+ // 4. Filters & Subcategories Row - the filters (sort/distance/discount) and
+ // subcategories are WalaPlus-specific concepts that don't apply to exclusive
+ // offers (legacy Offers & Discounts API), so this row is skipped in that mode.
+ if (!isExclusiveMode) _buildSubcategoriesAndFilterRow(isAr),
const SizedBox(height: 16),
// 5. Offers Grid
- if (isLoading)
- _buildRetailersShimmer()
- else if (_filteredRetailersList.isNotEmpty) ...[
- GridView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: isTablet ? 4 : 2,
- mainAxisExtent: 235,
- crossAxisSpacing: 12,
- mainAxisSpacing: 16,
- ),
- itemCount: _filteredRetailersList.length,
- itemBuilder: (context, index) {
- return _buildOfferCard(_filteredRetailersList[index], isAr);
- },
- ),
- if (isLoadingMore) ...[
- 16.height,
- Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))),
- ],
- ] else
- Padding(
- padding: const EdgeInsets.only(top: 40),
- child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B))),
- ),
+ WalaPlusRetailerGrid(
+ isLoading: isLoading,
+ retailers: _filteredRetailersList,
+ isAr: isAr,
+ isLoadingMore: isLoadingMore,
+ onTapRetailer: _openRetailer,
+ onFavoriteTap: _toggleFavorite,
+ ),
],
),
),
@@ -830,7 +1030,9 @@ class _DashboardScreenForDependetState extends State
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- LocaleKeys.exploreYourBenefits.tr().toText13(color: const Color(0xFF79807F)),
+ LocaleKeys.exploreYourBenefits.tr().toText13(
+ color: const Color(0xFF79807F),
+ ),
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: _showCitySelectionBottomSheet,
@@ -839,9 +1041,16 @@ class _DashboardScreenForDependetState extends State
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
- const Icon(Icons.location_on_outlined, size: 16, color: Color(0xFF157D6B)),
+ const Icon(
+ Icons.location_on_outlined,
+ size: 16,
+ color: Color(0xFF157D6B),
+ ),
const SizedBox(width: 4),
- selectedCity.toText13(isBold: true, color: const Color(0xFF157D6B)),
+ selectedCity.toText13(
+ isBold: true,
+ color: const Color(0xFF157D6B),
+ ),
],
),
),
@@ -852,8 +1061,22 @@ class _DashboardScreenForDependetState extends State
}
Widget _buildCategoryHero(bool isAr) {
- String categoryName = selectedCategory?.name ?? (isAr ? "السفر والترفيه" : "Travel & Entertainment");
- String subtitle = isAr ? "مغامرتك القادمة، مع القليل من التميز." : "Your next adventure, with a little extra.";
+ bool isLatest = selectedCategory?.id == 'all';
+ String categoryName =
+ isExclusiveMode
+ ? LocaleKeys.exclusivelyYours.tr()
+ : isLatest
+ ? LocaleKeys.latestOnMazaya.tr()
+ : selectedCategory?.name ??
+ (isAr ? "السفر والترفيه" : "Travel & Entertainment");
+ String subtitle =
+ isExclusiveMode
+ ? LocaleKeys.aLittleExtraJustForMazayaMembers.tr()
+ : isLatest
+ ? LocaleKeys.goodFoodGreatCompanyEvenBetterPerks.tr()
+ : (isAr
+ ? "مغامرتك القادمة، مع القليل من التميز."
+ : "Your next adventure, with a little extra.");
return Padding(
padding: const EdgeInsets.only(bottom: 16),
@@ -864,24 +1087,55 @@ class _DashboardScreenForDependetState extends State
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- categoryName.toText24(isBold: true, color: const Color(0xFF202F31)),
+ categoryName.toText24(
+ isBold: true,
+ color: const Color(0xFF202F31),
+ ),
const SizedBox(height: 6),
subtitle.toText12(color: const Color(0xFF7A8180)),
],
),
),
const SizedBox(width: 16),
- _buildCategoryHeroImage(selectedCategory),
+ _buildCategoryHeroImage(
+ selectedCategory,
+ isLatest: isLatest,
+ isExclusive: isExclusiveMode,
+ ),
],
),
);
}
- Widget _buildCategoryHeroImage(WalaPlusCategoryModel? category) {
+ Widget _buildCategoryHeroImage(
+ WalaPlusCategoryModel? category, {
+ bool isLatest = false,
+ bool isExclusive = false,
+ }) {
const double imgWidth = 84.0;
const double imgHeight = 84.0;
const double radius = 18.0;
+ if (isExclusive) {
+ return _buildCategoryHeroFallback(
+ category,
+ imgWidth,
+ imgHeight,
+ radius,
+ isExclusive: true,
+ );
+ }
+
+ if (isLatest) {
+ return _buildCategoryHeroFallback(
+ category,
+ imgWidth,
+ imgHeight,
+ radius,
+ isLatest: true,
+ );
+ }
+
String? imgUrl = category?.categoryImageUrl;
if (imgUrl != null && imgUrl.isNotEmpty) {
if (imgUrl.toLowerCase().endsWith('.svg') || (category?.isSvg ?? false)) {
@@ -893,7 +1147,13 @@ class _DashboardScreenForDependetState extends State
child: SvgPicture.network(
imgUrl,
fit: BoxFit.cover,
- placeholderBuilder: (_) => _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius),
+ placeholderBuilder:
+ (_) => _buildCategoryHeroFallback(
+ category,
+ imgWidth,
+ imgHeight,
+ radius,
+ ),
),
),
);
@@ -906,7 +1166,13 @@ class _DashboardScreenForDependetState extends State
child: Image.network(
imgUrl,
fit: BoxFit.cover,
- errorBuilder: (_, __, ___) => _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius),
+ errorBuilder:
+ (_, __, ___) => _buildCategoryHeroFallback(
+ category,
+ imgWidth,
+ imgHeight,
+ radius,
+ ),
),
),
);
@@ -914,21 +1180,41 @@ class _DashboardScreenForDependetState extends State
return _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius);
}
- Widget _buildCategoryHeroFallback(WalaPlusCategoryModel? category, double w, double h, double r) {
+ Widget _buildCategoryHeroFallback(
+ WalaPlusCategoryModel? category,
+ double w,
+ double h,
+ double r, {
+ bool isLatest = false,
+ bool isExclusive = false,
+ }) {
return ClipRRect(
borderRadius: BorderRadius.circular(r),
child: Container(
width: w,
height: h,
- decoration: const BoxDecoration(
+ decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
- colors: [Color(0xFF38BDF8), Color(0xFF0284C7)],
+ colors:
+ isExclusive
+ ? const [Color(0xFF143F36), Color(0xFF0B2A23)]
+ : isLatest
+ ? const [Color(0xFF1FA98D), Color(0xFF147D6B)]
+ : const [Color(0xFF38BDF8), Color(0xFF0284C7)],
),
),
- child: const Center(
- child: Icon(Icons.flight_takeoff_rounded, size: 36, color: Colors.white),
+ child: Center(
+ child: Icon(
+ isExclusive
+ ? Icons.workspace_premium_rounded
+ : isLatest
+ ? Icons.local_offer_rounded
+ : Icons.flight_takeoff_rounded,
+ size: 36,
+ color: isExclusive ? const Color(0xFFDCC58F) : Colors.white,
+ ),
),
),
);
@@ -960,19 +1246,31 @@ class _DashboardScreenForDependetState extends State
},
textInputAction: TextInputAction.search,
textAlignVertical: TextAlignVertical.center,
- style: const TextStyle(color: Color(0xFF202F31), fontSize: 13, fontWeight: FontWeight.w400),
+ style: const TextStyle(
+ color: Color(0xFF202F31),
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ ),
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
hintText: LocaleKeys.findAnEscapeOrAnExperience.tr(),
- hintStyle: const TextStyle(color: Color(0xFF7A8180), fontSize: 13, fontWeight: FontWeight.w400),
+ hintStyle: const TextStyle(
+ color: Color(0xFF7A8180),
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ ),
border: InputBorder.none,
),
),
),
ValueListenableBuilder(
valueListenable: _searchController,
- builder: (BuildContext context, TextEditingValue value, Widget? child) {
+ builder: (
+ BuildContext context,
+ TextEditingValue value,
+ Widget? child,
+ ) {
if (value.text.isEmpty) return const SizedBox.shrink();
return InkWell(
borderRadius: BorderRadius.circular(16),
@@ -1002,7 +1300,10 @@ class _DashboardScreenForDependetState extends State
}
Widget _buildSubcategoriesAndFilterRow(bool isAr) {
- bool showSubcategoriesRow = currentCategoryId.isNotEmpty && currentCategoryId != "all" && (isSubcategoriesLoading || subcategoriesList.isNotEmpty);
+ bool showSubcategoriesRow =
+ currentCategoryId.isNotEmpty &&
+ currentCategoryId != "all" &&
+ (isSubcategoriesLoading || subcategoriesList.isNotEmpty);
int activeFilterCount = _activeFilterCount();
return Padding(
@@ -1018,13 +1319,27 @@ class _DashboardScreenForDependetState extends State
borderRadius: BorderRadius.circular(6),
onTap: _showFiltersBottomSheet,
child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 14,
+ vertical: 8,
+ ),
alignment: Alignment.center,
- decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: MyColors.lightGreyEAColor),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ color: MyColors.lightGreyEAColor,
+ ),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
- SvgPicture.asset("assets/icons/filter_icon.svg", width: 14, height: 14, colorFilter: const ColorFilter.mode(MyColors.black, BlendMode.srcIn)),
+ SvgPicture.asset(
+ "assets/icons/filter_icon.svg",
+ width: 14,
+ height: 14,
+ colorFilter: const ColorFilter.mode(
+ MyColors.black,
+ BlendMode.srcIn,
+ ),
+ ),
const SizedBox(width: 5),
LocaleKeys.filters.tr().toText12(color: MyColors.black),
],
@@ -1039,14 +1354,25 @@ class _DashboardScreenForDependetState extends State
width: 18,
height: 18,
alignment: Alignment.center,
- decoration: const BoxDecoration(color: MyColors.darkIconColor, shape: BoxShape.circle),
- child: '$activeFilterCount'.toText10(isBold: true, color: Colors.white),
+ decoration: const BoxDecoration(
+ color: MyColors.darkIconColor,
+ shape: BoxShape.circle,
+ ),
+ child: '$activeFilterCount'.toText10(
+ isBold: true,
+ color: Colors.white,
+ ),
),
),
],
),
if (showSubcategoriesRow) ...[
- Container(margin: const EdgeInsets.symmetric(horizontal: 10), width: 1, height: 20, color: const Color(0xFFE2E8F0)),
+ Container(
+ margin: const EdgeInsets.symmetric(horizontal: 10),
+ width: 1,
+ height: 20,
+ color: const Color(0xFFE2E8F0),
+ ),
Expanded(
child:
isSubcategoriesLoading
@@ -1057,34 +1383,76 @@ class _DashboardScreenForDependetState extends State
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
itemCount: 4,
- separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8),
- itemBuilder: (BuildContext _, int __) => Container(width: 70, height: 32, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))),
+ separatorBuilder:
+ (BuildContext _, int __) =>
+ const SizedBox(width: 8),
+ itemBuilder:
+ (BuildContext _, int __) => Container(
+ width: 70,
+ height: 32,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(6),
+ ),
+ ),
),
)
: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
itemCount: subcategoriesList.length + 1,
- separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8),
+ separatorBuilder:
+ (BuildContext _, int __) =>
+ const SizedBox(width: 8),
itemBuilder: (BuildContext context, int index) {
bool isAllChip = index == 0;
- WalaPlusCategoryModel? subcategory = isAllChip ? null : subcategoriesList[index - 1];
- bool isSelected = isAllChip ? _selectedSubcategoryId == null : _selectedSubcategoryId == subcategory!.id;
- String label = isAllChip ? LocaleKeys.allCategories.tr() : (subcategory!.name ?? "");
+ WalaPlusCategoryModel? subcategory =
+ isAllChip ? null : subcategoriesList[index - 1];
+ bool isSelected =
+ isAllChip
+ ? _selectedSubcategoryId == null
+ : _selectedSubcategoryId == subcategory!.id;
+ String label =
+ isAllChip
+ ? LocaleKeys.allCategories.tr()
+ : (subcategory!.name ?? "");
return InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () {
setState(() {
- _selectedSubcategoryId = isAllChip ? null : (isSelected ? null : subcategory!.id);
+ _selectedSubcategoryId =
+ isAllChip
+ ? null
+ : (isSelected
+ ? null
+ : subcategory!.id);
});
- fetchOffers(categoryId: _selectedSubcategoryId ?? currentCategoryId);
+ fetchOffers(
+ categoryId:
+ _selectedSubcategoryId ??
+ currentCategoryId,
+ );
},
child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
alignment: Alignment.center,
- decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor),
- child: label.toText12(color: isSelected ? MyColors.white : MyColors.black),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ color:
+ isSelected
+ ? MyColors.darkIconColor
+ : MyColors.lightGreyEAColor,
+ ),
+ child: label.toText12(
+ color:
+ isSelected
+ ? MyColors.white
+ : MyColors.black,
+ ),
),
);
},
@@ -1098,69 +1466,10 @@ class _DashboardScreenForDependetState extends State
}
void _showCitySelectionBottomSheet() {
- List cities = [
- LocaleKeys.riyadh.tr(),
- AppState().isArabic(context) ? "جدة" : "Jeddah",
- AppState().isArabic(context) ? "الدمام" : "Dammam",
- AppState().isArabic(context) ? "الخبر" : "Khobar",
- AppState().isArabic(context) ? "مكة المكرمة" : "Mecca",
- AppState().isArabic(context) ? "المدينة المنورة" : "Medina",
- ];
-
- showModalBottomSheet(
- context: context,
- backgroundColor: Colors.transparent,
- builder: (BuildContext sheetContext) {
- return Container(
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
- decoration: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
- ),
- child: SafeArea(
- top: false,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(
- width: 40,
- height: 4,
- margin: const EdgeInsets.only(bottom: 12),
- decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2)),
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- LocaleKeys.selectCity.tr().toText16(isBold: true, color: const Color(0xFF1E293B)),
- InkWell(
- onTap: () => Navigator.pop(sheetContext),
- borderRadius: BorderRadius.circular(16),
- child: const Padding(
- padding: EdgeInsets.all(4),
- child: Icon(Icons.close_rounded, size: 20, color: Color(0xFF64748B)),
- ),
- ),
- ],
- ),
- const SizedBox(height: 12),
- ...cities.map((city) {
- bool isSelected = selectedCity == city;
- return ListTile(
- title: city.toText14(isBold: isSelected, color: isSelected ? const Color(0xFF0D9488) : const Color(0xFF1E293B)),
- trailing: isSelected ? const Icon(Icons.check_rounded, color: Color(0xFF0D9488), size: 20) : null,
- onTap: () {
- setState(() {
- selectedCity = city;
- });
- Navigator.pop(sheetContext);
- },
- );
- }).toList(),
- ],
- ),
- ),
- );
- },
+ showMazayaCitySelectionBottomSheet(
+ context,
+ selectedCity: selectedCity,
+ onCitySelected: (city) => setState(() => selectedCity = city),
);
}
@@ -1183,7 +1492,12 @@ class _DashboardScreenForDependetState extends State
builder: (BuildContext sheetContext) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setSheetState) {
- Widget buildOption(String label, bool isSelected, VoidCallback onTap, {IconData? icon}) {
+ Widget buildOption(
+ String label,
+ bool isSelected,
+ VoidCallback onTap, {
+ IconData? icon,
+ }) {
return Expanded(
child: InkWell(
borderRadius: BorderRadius.circular(8),
@@ -1193,15 +1507,44 @@ class _DashboardScreenForDependetState extends State
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
- color: isSelected ? MyColors.lightGreyEAColor : Colors.white,
- border: Border.all(color: isSelected ? MyColors.lightGreyEAColor : borderColor),
+ color:
+ isSelected ? MyColors.lightGreyEAColor : Colors.white,
+ border: Border.all(
+ color:
+ isSelected
+ ? MyColors.lightGreyEAColor
+ : borderColor,
+ ),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
- if (icon != null) ...[Icon(icon, size: 14, color: isSelected ? MyColors.black : const Color(0xFF64748B)), const SizedBox(width: 5)],
- Flexible(child: label.toText13(isBold: true, color: isSelected ? MyColors.black : darkBg, maxlines: 1)),
- if (isSelected) ...[const SizedBox(width: 5), const Icon(Icons.check, size: 14, color: MyColors.black)],
+ if (icon != null) ...[
+ Icon(
+ icon,
+ size: 14,
+ color:
+ isSelected
+ ? MyColors.black
+ : const Color(0xFF64748B),
+ ),
+ const SizedBox(width: 5),
+ ],
+ Flexible(
+ child: label.toText13(
+ isBold: true,
+ color: isSelected ? MyColors.black : darkBg,
+ maxlines: 1,
+ ),
+ ),
+ if (isSelected) ...[
+ const SizedBox(width: 5),
+ const Icon(
+ Icons.check,
+ size: 14,
+ color: MyColors.black,
+ ),
+ ],
],
),
),
@@ -1212,7 +1555,11 @@ class _DashboardScreenForDependetState extends State
Widget sectionHeader(String title, {String? hint}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [title.toText14(isBold: true, color: darkBg), if (hint != null) hint.toText12(color: const Color(0xFF94A3B8))],
+ children: [
+ title.toText14(isBold: true, color: darkBg),
+ if (hint != null)
+ hint.toText12(color: const Color(0xFF94A3B8)),
+ ],
);
}
@@ -1220,7 +1567,10 @@ class _DashboardScreenForDependetState extends State
top: false,
child: Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
- decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
+ ),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1230,13 +1580,19 @@ class _DashboardScreenForDependetState extends State
width: 44,
height: 4.5,
margin: const EdgeInsets.only(bottom: 16),
- decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5)),
+ decoration: BoxDecoration(
+ color: const Color(0xFFCBD5E1),
+ borderRadius: BorderRadius.circular(2.5),
+ ),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- LocaleKeys.filters.tr().toText20(isBold: true, color: darkBg),
+ LocaleKeys.filters.tr().toText20(
+ isBold: true,
+ color: darkBg,
+ ),
InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => Navigator.pop(sheetContext),
@@ -1244,8 +1600,15 @@ class _DashboardScreenForDependetState extends State
width: 32,
height: 32,
alignment: Alignment.center,
- decoration: const BoxDecoration(shape: BoxShape.circle, color: Color(0xFFF1F5F9)),
- child: const Icon(Icons.close, size: 18, color: darkBg),
+ decoration: const BoxDecoration(
+ shape: BoxShape.circle,
+ color: Color(0xFFF1F5F9),
+ ),
+ child: const Icon(
+ Icons.close,
+ size: 18,
+ color: darkBg,
+ ),
),
),
],
@@ -1253,59 +1616,133 @@ class _DashboardScreenForDependetState extends State
20.height,
// Availability - single select, drives the real offerType filter.
- LocaleKeys.availability.tr().toText14(isBold: true, color: darkBg),
+ LocaleKeys.availability.tr().toText14(
+ isBold: true,
+ color: darkBg,
+ ),
10.height,
Row(
children: [
- buildOption(LocaleKeys.allCategories.tr(), draftAvailability == 'all', () => setSheetState(() => draftAvailability = 'all')),
+ buildOption(
+ LocaleKeys.allCategories.tr(),
+ draftAvailability == 'all',
+ () => setSheetState(() => draftAvailability = 'all'),
+ ),
const SizedBox(width: 10),
- buildOption(LocaleKeys.online.tr(), draftAvailability == 'online', () => setSheetState(() => draftAvailability = 'online')),
+ buildOption(
+ LocaleKeys.online.tr(),
+ draftAvailability == 'online',
+ () =>
+ setSheetState(() => draftAvailability = 'online'),
+ ),
const SizedBox(width: 10),
- buildOption(LocaleKeys.inStore.tr(), draftAvailability == 'instore', () => setSheetState(() => draftAvailability = 'instore')),
+ buildOption(
+ LocaleKeys.inStore.tr(),
+ draftAvailability == 'instore',
+ () => setSheetState(
+ () => draftAvailability = 'instore',
+ ),
+ ),
],
),
20.height,
// Sort by - single select, Nearest and Newest only.
- LocaleKeys.sortBy.tr().toText14(isBold: true, color: darkBg),
+ LocaleKeys.sortBy.tr().toText14(
+ isBold: true,
+ color: darkBg,
+ ),
10.height,
Row(
children: [
- buildOption(LocaleKeys.nearest.tr(), draftSortBy == 'nearest', () => setSheetState(() => draftSortBy = 'nearest'), icon: Icons.location_on_outlined),
+ buildOption(
+ LocaleKeys.nearest.tr(),
+ draftSortBy == 'nearest',
+ () => setSheetState(() => draftSortBy = 'nearest'),
+ icon: Icons.location_on_outlined,
+ ),
const SizedBox(width: 10),
- buildOption(LocaleKeys.newest.tr(), draftSortBy == 'newest', () => setSheetState(() => draftSortBy = 'newest')),
+ buildOption(
+ LocaleKeys.newest.tr(),
+ draftSortBy == 'newest',
+ () => setSheetState(() => draftSortBy = 'newest'),
+ ),
],
),
20.height,
// Distance - single select.
- sectionHeader(LocaleKeys.distance.tr(), hint: LocaleKeys.fromYourCurrentLocation.tr()),
+ sectionHeader(
+ LocaleKeys.distance.tr(),
+ hint: LocaleKeys.fromYourCurrentLocation.tr(),
+ ),
10.height,
Row(
children: [
- buildOption(LocaleKeys.any.tr(), draftDistance == 'any', () => setSheetState(() => draftDistance = 'any')),
+ buildOption(
+ LocaleKeys.any.tr(),
+ draftDistance == 'any',
+ () => setSheetState(() => draftDistance = 'any'),
+ ),
const SizedBox(width: 8),
- buildOption(LocaleKeys.distanceKm.tr(args: ['5']), draftDistance == '5km', () => setSheetState(() => draftDistance = '5km')),
+ buildOption(
+ LocaleKeys.distanceKm.tr(args: ['5']),
+ draftDistance == '5km',
+ () => setSheetState(() => draftDistance = '5km'),
+ ),
const SizedBox(width: 8),
- buildOption(LocaleKeys.distanceKm.tr(args: ['10']), draftDistance == '10km', () => setSheetState(() => draftDistance = '10km')),
+ buildOption(
+ LocaleKeys.distanceKm.tr(args: ['10']),
+ draftDistance == '10km',
+ () => setSheetState(() => draftDistance = '10km'),
+ ),
const SizedBox(width: 8),
- buildOption(LocaleKeys.distanceKm.tr(args: ['25']), draftDistance == '25km', () => setSheetState(() => draftDistance = '25km')),
+ buildOption(
+ LocaleKeys.distanceKm.tr(args: ['25']),
+ draftDistance == '25km',
+ () => setSheetState(() => draftDistance = '25km'),
+ ),
],
),
20.height,
// Minimum discount - single select.
- LocaleKeys.minimumDiscount.tr().toText14(isBold: true, color: darkBg),
+ LocaleKeys.minimumDiscount.tr().toText14(
+ isBold: true,
+ color: darkBg,
+ ),
10.height,
Row(
children: [
- buildOption(LocaleKeys.any.tr(), draftMinDiscount == 'any', () => setSheetState(() => draftMinDiscount = 'any')),
+ buildOption(
+ LocaleKeys.any.tr(),
+ draftMinDiscount == 'any',
+ () => setSheetState(() => draftMinDiscount = 'any'),
+ ),
const SizedBox(width: 8),
- buildOption(LocaleKeys.discountPercentPlus.tr(args: ['10']), draftMinDiscount == '10', () => setSheetState(() => draftMinDiscount = '10')),
+ buildOption(
+ LocaleKeys.discountPercentPlus.tr(
+ args: ['10'],
+ ),
+ draftMinDiscount == '10',
+ () => setSheetState(() => draftMinDiscount = '10'),
+ ),
const SizedBox(width: 8),
- buildOption(LocaleKeys.discountPercentPlus.tr(args: ['20']), draftMinDiscount == '20', () => setSheetState(() => draftMinDiscount = '20')),
+ buildOption(
+ LocaleKeys.discountPercentPlus.tr(
+ args: ['20'],
+ ),
+ draftMinDiscount == '20',
+ () => setSheetState(() => draftMinDiscount = '20'),
+ ),
const SizedBox(width: 8),
- buildOption(LocaleKeys.discountPercentPlus.tr(args: ['30']), draftMinDiscount == '30', () => setSheetState(() => draftMinDiscount = '30')),
+ buildOption(
+ LocaleKeys.discountPercentPlus.tr(
+ args: ['30'],
+ ),
+ draftMinDiscount == '30',
+ () => setSheetState(() => draftMinDiscount = '30'),
+ ),
],
),
20.height,
@@ -1331,7 +1768,12 @@ class _DashboardScreenForDependetState extends State
_filterMinDiscount = draftMinDiscount;
});
Navigator.pop(sheetContext);
- fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId);
+ fetchOffers(
+ categoryId:
+ currentCategoryId.isEmpty
+ ? "all"
+ : currentCategoryId,
+ );
},
colors: const [Colors.white, Colors.white],
border: Border.all(color: borderColor),
@@ -1340,16 +1782,22 @@ class _DashboardScreenForDependetState extends State
),
const SizedBox(width: 12),
Expanded(
- child: DefaultButton(LocaleKeys.applyFilters.tr(), () {
- setState(() {
- selectedTypeFilter = draftAvailability;
- _filterSortBy = draftSortBy;
- _filterDistance = draftDistance;
- _filterMinDiscount = draftMinDiscount;
- });
- Navigator.pop(sheetContext);
- fetchOffers(categoryId: _selectedSubcategoryId ?? currentCategoryId);
- }),
+ child: DefaultButton(
+ LocaleKeys.applyFilters.tr(),
+ () {
+ setState(() {
+ selectedTypeFilter = draftAvailability;
+ _filterSortBy = draftSortBy;
+ _filterDistance = draftDistance;
+ _filterMinDiscount = draftMinDiscount;
+ });
+ Navigator.pop(sheetContext);
+ fetchOffers(
+ categoryId:
+ _selectedSubcategoryId ?? currentCategoryId,
+ );
+ },
+ ),
),
],
),
@@ -1377,56 +1825,6 @@ class _DashboardScreenForDependetState extends State
setState(() {});
}
}
-
- Widget _buildOfferCard(WalaPlusRetailerModel item, bool isAr) {
- return WalaPlusRetailerCard(
- item: item,
- isAr: isAr,
- onTap: () => _openRetailer(item),
- onFavoriteTap: () => _toggleFavorite(item),
- );
- }
-
- Widget _buildRetailersShimmer() {
- return _buildRetailersShimmerWidget(isTablet);
- }
-}
-
-Widget _buildRetailersShimmerWidget(bool isTablet) {
- return Shimmer.fromColors(
- baseColor: const Color(0xFFE2E8F0),
- highlightColor: const Color(0xFFF8FAFC),
- child: GridView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16),
- itemCount: 4,
- itemBuilder: (_, __) {
- return Container(
- decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
- padding: const EdgeInsets.all(10),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(height: 120, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))),
- const SizedBox(height: 8),
- Container(height: 14, width: 110, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
- const SizedBox(height: 6),
- Container(height: 14, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
- const SizedBox(height: 6),
- Row(
- children: [
- Container(height: 28, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))),
- const SizedBox(width: 5),
- Container(height: 28, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))),
- ],
- ),
- ],
- ),
- );
- },
- ),
- );
}
class WalaPlusRetailerCard extends StatelessWidget {
@@ -1435,7 +1833,13 @@ class WalaPlusRetailerCard extends StatelessWidget {
final VoidCallback onTap;
final VoidCallback? onFavoriteTap;
- const WalaPlusRetailerCard({Key? key, required this.item, required this.isAr, required this.onTap, this.onFavoriteTap}) : super(key: key);
+ const WalaPlusRetailerCard({
+ Key? key,
+ required this.item,
+ required this.isAr,
+ required this.onTap,
+ this.onFavoriteTap,
+ }) : super(key: key);
static String getDiscountBadge(WalaPlusRetailerModel item) {
if (item.offerTitle != null) {
@@ -1475,20 +1879,19 @@ class WalaPlusRetailerCard extends StatelessWidget {
return Icons.tune_rounded;
}
- static Widget buildMerchantMIcon() {
- return Container(color: const Color(0xFFED1C24), alignment: Alignment.center, child: "M".toText10(isBold: true, color: Colors.white));
- }
-
@override
Widget build(BuildContext context) {
- String? bannerUrl = (item.imageThumb != null && item.imageThumb!.isNotEmpty) ? item.imageThumb : item.image;
- String? logoUrl = (item.logoThumb != null && item.logoThumb!.isNotEmpty) ? item.logoThumb : item.logo;
+ String? bannerUrl =
+ (item.imageThumb != null && item.imageThumb!.isNotEmpty)
+ ? item.imageThumb
+ : item.image;
String discountText = getDiscountBadge(item);
String offerSubtitle = "";
if (discountText.isNotEmpty) {
- if (item.offerTitle != null && item.offerTitle!.toLowerCase().contains("off")) {
+ if (item.offerTitle != null &&
+ item.offerTitle!.toLowerCase().contains("off")) {
offerSubtitle = item.offerTitle!;
} else {
offerSubtitle = LocaleKeys.offDiscount.tr(args: [discountText]);
@@ -1500,7 +1903,9 @@ class WalaPlusRetailerCard extends StatelessWidget {
List offerTypes = item.normalizedTypes;
String usageText = "";
if (item.displayUsage.isNotEmpty) {
- usageText = item.displayUsage.replaceAll('usage:', LocaleKeys.usageLabel.tr()).replaceAll('Usage :', LocaleKeys.usageLabel.tr());
+ usageText = item.displayUsage
+ .replaceAll('usage:', LocaleKeys.usageLabel.tr())
+ .replaceAll('Usage :', LocaleKeys.usageLabel.tr());
}
String cityText = item.getDisplayCity(isAr: isAr);
if (cityText.trim().isEmpty) {
@@ -1508,13 +1913,19 @@ class WalaPlusRetailerCard extends StatelessWidget {
}
return InkWell(
- borderRadius: BorderRadius.circular(20),
+ borderRadius: BorderRadius.circular(18),
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
- borderRadius: BorderRadius.circular(20),
- boxShadow: [BoxShadow(color: const Color(0xFF0D1720).withOpacity(0.0392), offset: const Offset(0, 5), blurRadius: 18, spreadRadius: 0)],
+ borderRadius: BorderRadius.circular(18),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 24,
+ spreadRadius: 0,
+ ),
+ ],
),
padding: const EdgeInsets.all(10),
child: SingleChildScrollView(
@@ -1522,10 +1933,17 @@ class WalaPlusRetailerCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
+ // Figma redesign (node 927:496): shorter image, no bottom gradient/logo overlay
+ // (brand moved into the card body below), no discount ribbon - the percentage now
+ // lives inline in the teal offer subtitle text instead. Image height trimmed from
+ // 120->110 (not Figma's 82) and padding/gaps left untouched on purpose: this card's
+ // overall size is mainAxisExtent-driven (see the grid's gridDelegate) and was
+ // already tuned against real devices - copying Figma's absolute pixels here would
+ // reintroduce the overflow/empty-space problems that tuning fixed.
ClipRRect(
borderRadius: BorderRadius.circular(14),
child: SizedBox(
- height: 120,
+ height: 110,
width: double.infinity,
child: Stack(
fit: StackFit.expand,
@@ -1534,84 +1952,114 @@ class WalaPlusRetailerCard extends StatelessWidget {
? Image.network(
bannerUrl,
fit: BoxFit.cover,
- errorBuilder: (_, __, ___) => Container(color: const Color(0xFFF1F5F9), child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32)),
+ errorBuilder:
+ (_, __, ___) => Container(
+ color: const Color(0xFFF1F5F9),
+ child: const Icon(
+ Icons.image_outlined,
+ color: Color(0xFF94A3B8),
+ size: 32,
+ ),
+ ),
)
- : Container(color: const Color(0xFFF1F5F9), child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32)),
- Positioned(
- bottom: 0,
- left: 0,
- right: 0,
- height: 50,
- child: Container(
- decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black.withOpacity(0.75), Colors.transparent])),
- ),
- ),
- if (discountText.isNotEmpty)
+ : Container(
+ color: const Color(0xFFF1F5F9),
+ child: const Icon(
+ Icons.image_outlined,
+ color: Color(0xFF94A3B8),
+ size: 32,
+ ),
+ ),
+ // Exclusive offers (mapped from the legacy Offers & Discounts API) have no
+ // real WalaPlus retailer id, so there's nothing for add/removeWalaPlusFavorite
+ // to act on - hide the heart rather than wiring it to a call that can't work.
+ if (item.isExclusive != true)
PositionedDirectional(
- top: 44,
- end: 0,
- child: Container(
- padding: const EdgeInsetsDirectional.only(start: 14, end: 10, top: 4, bottom: 4),
- decoration: const BoxDecoration(color: Color(0xFFED1C24), borderRadius: BorderRadiusDirectional.only(topStart: Radius.circular(20), bottomStart: Radius.circular(20))),
- child: discountText.toText14(isBold: true, color: Colors.white),
+ top: 7,
+ end: 7,
+ child: InkWell(
+ borderRadius: BorderRadius.circular(16),
+ onTap: onFavoriteTap,
+ child: Container(
+ width: 27,
+ height: 27,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ shape: BoxShape.circle,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.12),
+ blurRadius: 5,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ alignment: Alignment.center,
+ child: Icon(
+ item.isFavorite == true
+ ? Icons.favorite_rounded
+ : Icons.favorite_border_rounded,
+ size: 15,
+ color:
+ item.isFavorite == true
+ ? Colors.black
+ : const Color(0xFF1E293B),
+ ),
+ ),
),
),
- PositionedDirectional(
- top: 8,
- end: 8,
- child: InkWell(
- borderRadius: BorderRadius.circular(16),
- onTap: onFavoriteTap,
+ if (item.isExclusive == true)
+ PositionedDirectional(
+ bottom: 7,
+ start: 8,
child: Container(
- width: 30,
- height: 30,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 7,
+ vertical: 4,
+ ),
decoration: BoxDecoration(
- color: Colors.white,
- shape: BoxShape.circle,
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.12),
- blurRadius: 5,
- offset: const Offset(0, 2),
- ),
- ],
+ color: const Color(0xFF143F36),
+ borderRadius: BorderRadius.circular(6),
),
- alignment: Alignment.center,
- child: Icon(
- item.isFavorite == true ? Icons.favorite_rounded : Icons.favorite_border_rounded,
- size: 17,
- color: item.isFavorite == true ? const Color(0xFFE52332) : const Color(0xFF1E293B),
+ child: LocaleKeys.mazayaExclusive.tr().toText10(
+ color: const Color(0xFFF1EBDD),
),
),
),
- ),
- PositionedDirectional(
- bottom: 8,
- start: 8,
- end: 8,
- child: Row(
- children: [
- Container(
- width: 18,
- height: 18,
- decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4)),
- clipBehavior: Clip.antiAlias,
- child: (logoUrl != null && logoUrl.isNotEmpty) ? Image.network(logoUrl, fit: BoxFit.contain, errorBuilder: (_, __, ___) => buildMerchantMIcon()) : buildMerchantMIcon(),
- ),
- const SizedBox(width: 5),
- Expanded(child: (item.name ?? "").toText11(isBold: true, maxLine: 1, color: Colors.white)),
- ],
- ),
- ),
],
),
),
),
const SizedBox(height: 8),
- (item.name ?? "").toText14(isBold: true, maxlines: 1, overflow: TextOverflow.ellipsis, color: const Color(0xFF1E293B)),
+ // Brand name + "view details" affordance arrow, replacing the old image-overlay name.
+ Row(
+ children: [
+ Expanded(
+ child: (item.name ?? "").toText13(
+ isBold: true,
+ maxlines: 1,
+ overflow: TextOverflow.ellipsis,
+ color: const Color(0xFF202F32),
+ ),
+ ),
+ const SizedBox(width: 4),
+ const Icon(
+ Icons.north_east_rounded,
+ size: 16,
+ color: Color(0xFF147D6B),
+ ),
+ ],
+ ),
const SizedBox(height: 2),
- offerSubtitle.toText14(maxlines: 1, overflow: TextOverflow.ellipsis, isBold: true, color: const Color(0xFF1E293B)),
+ offerSubtitle.toText14(
+ maxlines: 1,
+ overflow: TextOverflow.ellipsis,
+ isBold: true,
+ color: const Color(0xFF147D6B),
+ ),
const SizedBox(height: 6),
+ // Fully-rounded pill badges (no icon, no border) matching the new design - type
+ // chips in blue (In-store) / green (Online), city in grey.
SizedBox(
height: 28,
child: ListView(
@@ -1621,33 +2069,49 @@ class WalaPlusRetailerCard extends StatelessWidget {
for (String offerType in offerTypes) ...[
InfoChipWidget(
text: getTypeFilterTitle(offerType),
- icon: getTypeFilterIcon(offerType),
- backgroundColor: offerType == 'online' ? const Color(0xFFE6F7F0) : const Color(0xFFEFF6FF),
- textColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF2563EB),
- borderColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF3B82F6),
+ backgroundColor:
+ offerType == 'online'
+ ? const Color(0xFFEAF3EE)
+ : const Color(0x14062CFF),
+ textColor:
+ offerType == 'online'
+ ? const Color(0xFF147D6B)
+ : const Color(0xFF2A56B6),
+ borderColor: Colors.transparent,
+ borderWidth: 0,
+ isBold: false,
horizontalPadding: 10,
- verticalPadding: 5,
- borderRadius: 12,
- iconSize: 14,
+ verticalPadding: 6,
+ borderRadius: 99,
),
const SizedBox(width: 5),
],
if (cityText.trim().isNotEmpty) ...[
InfoChipWidget(
text: cityText,
- icon: Icons.location_on_outlined,
- backgroundColor: const Color(0xFFF8FAFC),
- textColor: const Color(0xFF475569),
- borderColor: const Color(0xFFCBD5E1),
+ backgroundColor: const Color(0xFFEFF2EF),
+ textColor: const Color(0xFF464746),
+ borderColor: Colors.transparent,
+ borderWidth: 0,
+ isBold: false,
horizontalPadding: 10,
- verticalPadding: 5,
- borderRadius: 12,
- iconSize: 14,
+ verticalPadding: 6,
+ borderRadius: 99,
),
const SizedBox(width: 5),
],
if (usageText.trim().isNotEmpty)
- InfoChipWidget(text: usageText, backgroundColor: const Color(0xFFFEF3C7), textColor: const Color(0xFFB45309), horizontalPadding: 10, verticalPadding: 5, borderRadius: 12),
+ InfoChipWidget(
+ text: usageText,
+ backgroundColor: const Color(0xFFFEF3C7),
+ textColor: const Color(0xFFB45309),
+ borderColor: Colors.transparent,
+ borderWidth: 0,
+ isBold: false,
+ horizontalPadding: 10,
+ verticalPadding: 6,
+ borderRadius: 99,
+ ),
],
),
),
@@ -1661,13 +2125,17 @@ class WalaPlusRetailerCard extends StatelessWidget {
class FavoriteMerchantsBottomSheet extends StatefulWidget {
final List? categoriesList;
- const FavoriteMerchantsBottomSheet({Key? key, this.categoriesList}) : super(key: key);
+
+ const FavoriteMerchantsBottomSheet({Key? key, this.categoriesList})
+ : super(key: key);
@override
- State createState() => _FavoriteMerchantsBottomSheetState();
+ State createState() =>
+ _FavoriteMerchantsBottomSheetState();
}
-class _FavoriteMerchantsBottomSheetState extends State {
+class _FavoriteMerchantsBottomSheetState
+ extends State {
List favoriteRetailers = [];
bool isLoading = true;
bool isLoadingMore = false;
@@ -1732,30 +2200,36 @@ class _FavoriteMerchantsBottomSheetState extends State[];
for (WalaPlusRetailerModel r in favoriteRetailers) {
r.isFavorite = true;
}
currentPage = 1;
totalPages = response?.pagesCount ?? 1;
- hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false);
+ hasMore =
+ currentPage < totalPages &&
+ (response?.retailers?.isNotEmpty ?? false);
} catch (e) {
debugPrint("Error fetching favorite retailers: $e");
favoriteRetailers = [];
@@ -1778,25 +2252,30 @@ class _FavoriteMerchantsBottomSheetState extends State newRetailers = response?.retailers ?? [];
+ WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
+ .getWalaPlusRetailers(
+ categoryId: "",
+ page: nextPage,
+ perPage: pageSize,
+ offerType: null,
+ query: queryParam,
+ sortBy: null,
+ radius: null,
+ minDiscount: null,
+ context: null,
+ userReference: userRef,
+ );
+
+ List newRetailers =
+ response?.retailers ?? [];
if (newRetailers.isNotEmpty) {
for (WalaPlusRetailerModel r in newRetailers) {
r.isFavorite = true;
@@ -1845,6 +2324,7 @@ class _FavoriteMerchantsBottomSheetState extends State r.id == item.id);
}
});
+ Utils.showLoading(context);
try {
GenericResponseModel? response;
@@ -1863,23 +2343,40 @@ class _FavoriteMerchantsBottomSheetState extends State(
valueListenable: _searchController,
- builder: (BuildContext context, TextEditingValue value, Widget? child) {
- if (value.text.isEmpty) return const SizedBox.shrink();
+ builder: (
+ BuildContext context,
+ TextEditingValue value,
+ Widget? child,
+ ) {
+ if (value.text.isEmpty)
+ return const SizedBox.shrink();
return InkWell(
- child: const Icon(Icons.clear, size: 18, color: Color(0xFF848486)),
+ child: const Icon(
+ Icons.clear,
+ size: 18,
+ color: Color(0xFF848486),
+ ),
onTap: () {
_searchController.clear();
_onSearchChanged("");
@@ -1990,9 +2547,16 @@ class _FavoriteMerchantsBottomSheetState extends State fetchFavorites(query: _searchController.text),
+ onRefresh:
+ () => fetchFavorites(query: _searchController.text),
color: MyColors.textMixColor,
- child:
- isLoading
- ? Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: _buildRetailersShimmerWidget(isTablet))
- : favoriteRetailers.isNotEmpty
- ? SingleChildScrollView(
- controller: _scrollController,
- physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()),
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: Column(
- children: [
- GridView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16),
- itemCount: favoriteRetailers.length,
- itemBuilder: (context, index) {
- return WalaPlusRetailerCard(
- item: favoriteRetailers[index],
- isAr: isAr,
- onTap: () => _openRetailer(favoriteRetailers[index]),
- onFavoriteTap: () => _toggleFavorite(favoriteRetailers[index]),
- );
- },
- ),
- if (isLoadingMore) ...[
- 16.height,
- Center(
- child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0)),
- ),
- ],
- const SizedBox(height: 20),
- ],
- ),
- )
- : SingleChildScrollView(
- physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()),
- child: Padding(padding: const EdgeInsets.only(top: 80), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))),
- ),
+ child: SingleChildScrollView(
+ controller: _scrollController,
+ physics: const AlwaysScrollableScrollPhysics(
+ parent: BouncingScrollPhysics(),
+ ),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ child: Column(
+ children: [
+ WalaPlusRetailerGrid(
+ isLoading: isLoading,
+ retailers: favoriteRetailers,
+ isAr: isAr,
+ isLoadingMore: isLoadingMore,
+ onTapRetailer: _openRetailer,
+ onFavoriteTap: _toggleFavorite,
+ ),
+ const SizedBox(height: 20),
+ ],
+ ),
+ ),
),
),
],
diff --git a/lib/ui/mazaya/saved_retailers_screen.dart b/lib/ui/mazaya/saved_retailers_screen.dart
new file mode 100644
index 0000000..4e2bf0e
--- /dev/null
+++ b/lib/ui/mazaya/saved_retailers_screen.dart
@@ -0,0 +1,386 @@
+import 'dart:async';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
+import 'package:mohem_flutter_app/app_state/app_state.dart';
+import 'package:mohem_flutter_app/classes/colors.dart';
+import 'package:mohem_flutter_app/classes/utils.dart';
+import 'package:mohem_flutter_app/config/routes.dart';
+import 'package:mohem_flutter_app/extensions/string_extensions.dart';
+import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
+import 'package:mohem_flutter_app/models/generic_response_model.dart';
+import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
+import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart';
+import 'package:mohem_flutter_app/ui/mazaya/widgets/wala_plus_retailer_grid.dart';
+import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
+
+// Mazaya Saved tab - full-screen version of what was FavoriteMerchantsBottomSheet (a modal
+// bottom sheet). Same fetch/search/pagination/toggle-favorite logic, now its own pushed route
+// with the shared MazayaBottomNav instead of a dismissible sheet - matches Discover's "own
+// route, fetches fresh on mount" pattern.
+class SavedRetailersScreen extends StatefulWidget {
+ const SavedRetailersScreen({Key? key}) : super(key: key);
+
+ @override
+ State createState() => _SavedRetailersScreenState();
+}
+
+class _SavedRetailersScreenState extends State {
+ List favoriteRetailers = [];
+ bool isLoading = true;
+ bool isLoadingMore = false;
+ bool hasMore = true;
+ bool _isFetchingPage = false;
+ int currentPage = 1;
+ int totalPages = 1;
+ final int pageSize = 10;
+ String searchQuery = "";
+ Timer? _debounceTimer;
+ final TextEditingController _searchController = TextEditingController();
+ final ScrollController _scrollController = ScrollController();
+
+ @override
+ void initState() {
+ super.initState();
+ _scrollController.addListener(_scrollListener);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) fetchFavorites();
+ });
+ }
+
+ @override
+ void dispose() {
+ _debounceTimer?.cancel();
+ _scrollController.removeListener(_scrollListener);
+ _scrollController.dispose();
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ void _scrollListener() {
+ if (!_scrollController.hasClients) return;
+ double maxScroll = _scrollController.position.maxScrollExtent;
+ double currentScroll = _scrollController.position.pixels;
+ if (currentScroll >= maxScroll - 35) {
+ if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) {
+ fetchNextPage();
+ }
+ }
+ }
+
+ String? _getUserReferenceForFavorites() {
+ String ref = OffersAndDiscountsApiClient().resolveUserReference();
+ if (ref.trim().isNotEmpty) return ref.trim();
+ String? userName = AppState().getUserName;
+ if (userName != null && userName.trim().isNotEmpty) return userName.trim();
+ return null;
+ }
+
+ Future fetchFavorites({String? query}) async {
+ if (query != null) searchQuery = query;
+ setState(() {
+ isLoading = true;
+ currentPage = 1;
+ hasMore = true;
+ });
+ try {
+ String? queryParam =
+ searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
+ String? userRef = _getUserReferenceForFavorites();
+ WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
+ .getWalaPlusRetailers(
+ categoryId: "",
+ page: 1,
+ perPage: pageSize,
+ offerType: null,
+ query: queryParam,
+ sortBy: null,
+ radius: null,
+ minDiscount: null,
+ context: null,
+ userReference: userRef,
+ );
+ favoriteRetailers = response?.retailers ??