diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 2acd5e3..0aa8afe 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -808,6 +808,7 @@ "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.", "shareAccessGuide": "مشاركة دليل الوصول", - "nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة" + "nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة", + "multipleCities": "مدن متعددة" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 89eb9ff..f128426 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -553,7 +553,7 @@ "confirmRevokeMazayaAccess": "Are you sure you want to revoke Mazaya access to \"{}\"?", "yes": "Yes", "no": "No", - "mazayaInviteInstructions": "Please download the Mohemm app from the above link and use the following credentials to login", + "mazayaInviteInstructions": "Please download the MoheM app from the above link and use the following credentials to login", "cameraPermissionRequiredToScan": "Please grant camera permission to scan the QR code", "cameraPermissionPermanentlyDenied": "Please allow camera permission from the app settings > Permissions", "openSettings": "Open Settings", @@ -808,7 +808,8 @@ "pleaseProvideLocationPermission": "Please provide location permission", "microphoneAccessRequired": "The app needs microphone access to be able to record audio.", "shareAccessGuide": "Share Access Guide", - "nationalIdOrIqamaNumber": "National ID or Iqama Number" + "nationalIdOrIqamaNumber": "National ID or Iqama Number", + "multipleCities": "Multiple Cities" } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index efd27f9..178192a 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -820,7 +820,8 @@ class CodegenLoader extends AssetLoader{ "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.", "shareAccessGuide": "مشاركة دليل الوصول", - "nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة" + "nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة", + "multipleCities": "مدن متعددة" }; static const Map _en_US = { "mohemm": "Mohemm", @@ -1377,7 +1378,7 @@ static const Map _en_US = { "confirmRevokeMazayaAccess": "Are you sure you want to revoke Mazaya access to \"{}\"?", "yes": "Yes", "no": "No", - "mazayaInviteInstructions": "Please download the Mohemm app from the above link and use the following credentials to login", + "mazayaInviteInstructions": "Please download the MoheM app from the above link and use the following credentials to login", "cameraPermissionRequiredToScan": "Please grant camera permission to scan the QR code", "cameraPermissionPermanentlyDenied": "Please allow camera permission from the app settings > Permissions", "openSettings": "Open Settings", @@ -1628,7 +1629,8 @@ static const Map _en_US = { "pleaseProvideLocationPermission": "Please provide location permission", "microphoneAccessRequired": "The app needs microphone access to be able to record audio.", "shareAccessGuide": "Share Access Guide", - "nationalIdOrIqamaNumber": "National ID or Iqama Number" + "nationalIdOrIqamaNumber": "National ID or Iqama Number", + "multipleCities": "Multiple Cities" }; 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 6ee8472..c06f3ab 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -793,5 +793,6 @@ abstract class LocaleKeys { static const microphoneAccessRequired = 'microphoneAccessRequired'; static const shareAccessGuide = 'shareAccessGuide'; static const nationalIdOrIqamaNumber = 'nationalIdOrIqamaNumber'; + static const multipleCities = 'multipleCities'; } 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 8823b97..16c1104 100644 --- a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart +++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart @@ -471,33 +471,40 @@ class WalaPlusRetailerModel { return false; } + // Resolves a single location's city using the same 3-pass priority this used to apply + // globally: specific city name first, then a non-generic-country city, then any city at all. + String? _resolveLocationCity(WalaPlusRetailerLocation loc, bool isAr) { + String? cName = isAr ? loc.cityNameAr : loc.cityNameEn; + if (cName != null && cName.trim().isNotEmpty && cName != "null") { + return cName.trim(); + } + if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { + String c = loc.city!.trim(); + if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') { + return c; + } + } + if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { + return loc.city!.trim(); + } + return null; + } + + // Multiple distinct cities across this retailer's locations -> "Multiple Cities" instead of + // just picking the first one found, so the badge doesn't silently hide the other branches. String getDisplayCity({bool isAr = false}) { log("locations: ${locations?.map((e) => e.toJson()).toList()}"); - if (locations != null && locations!.isNotEmpty) { - // 1. First pass: look for specific city name - for (var loc in locations!) { - String? cName = isAr ? loc.cityNameAr : loc.cityNameEn; - if (cName != null && cName.trim().isNotEmpty && cName != "null") { - return cName.trim(); - } - } - // 2. Second pass: fallback to city if not generic country - for (var loc in locations!) { - if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { - String c = loc.city!.trim(); - if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') { - return c; - } - } - } - // 3. Third pass: any non-empty city - for (var loc in locations!) { - if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { - return loc.city!.trim(); - } - } + if (locations == null || locations!.isEmpty) return ''; + + Set distinctCities = {}; + for (var loc in locations!) { + String? resolved = _resolveLocationCity(loc, isAr); + if (resolved != null) distinctCities.add(resolved); } - return ''; + + if (distinctCities.isEmpty) return ''; + if (distinctCities.length > 1) return LocaleKeys.multipleCities.tr(); + return distinctCities.first; } String get displayCity => getDisplayCity(isAr: false); @@ -583,37 +590,62 @@ class WalaPlusRetailerModel { return null; } - bool get isOnlineStore => (type?.trim().toLowerCase() == 'online'); - - bool get isInstore => - (type?.trim().toLowerCase() == 'instore' || - type?.trim().toLowerCase() == 'in_store' || - type?.trim().toLowerCase() == 'in store' || - type?.trim().toLowerCase() == 'store' || - (type == null && locations != null && locations!.isNotEmpty)); - - String? get normalizedType { + // type can be a comma-separated combo (e.g. "online,instore") when a retailer offers both - + // this splits and normalizes every part instead of only ever matching a single exact value, + // so a combined retailer is correctly recognized as both rather than neither/just one. + List get normalizedTypes { if (type != null && type!.trim().isNotEmpty) { - String t = type!.trim().toLowerCase(); - if (t == 'online') return 'online'; - if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') return 'instore'; - return t; + Set result = {}; + for (String part in type!.split(',')) { + String t = part.trim().toLowerCase(); + if (t.isEmpty) continue; + if (t == 'online') { + result.add('online'); + } else if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') { + result.add('instore'); + } else { + result.add(t); + } + } + if (result.isNotEmpty) return result.toList(); } if (locations != null && locations!.isNotEmpty) { - return 'instore'; + return ['instore']; } - return null; + return []; } - String? getDisplayType() { - String? norm = normalizedType; + bool get isOnlineStore => normalizedTypes.contains('online'); + + bool get isInstore => normalizedTypes.contains('instore'); + + // Kept for any existing single-value callers - returns the first normalized type found. + String? get normalizedType { + List types = normalizedTypes; + return types.isEmpty ? null : types.first; + } + + String _displayTypeLabel(String norm) { if (norm == 'online') return LocaleKeys.online.tr(); if (norm == 'instore') return LocaleKeys.inStore.tr(); - if (type != null && type!.trim().isNotEmpty) { - return type!.trim(); - } + return norm; + } + + String? getDisplayType() { + List types = normalizedTypes; + if (types.isNotEmpty) return _displayTypeLabel(types.first); + if (type != null && type!.trim().isNotEmpty) return type!.trim(); return null; } + + // One label per distinct type (e.g. ["Online", "In Store"]) - used to render a separate chip + // for each, instead of collapsing a dual-type retailer down to a single badge. + List getDisplayTypes() { + List types = normalizedTypes; + if (types.isNotEmpty) return types.map(_displayTypeLabel).toList(); + if (type != null && type!.trim().isNotEmpty) return [type!.trim()]; + return []; + } } class WalaPlusRetailerContact { diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/landing/dashboard_screen_for_dependet.dart index 5455699..f3a0943 100644 --- a/lib/ui/landing/dashboard_screen_for_dependet.dart +++ b/lib/ui/landing/dashboard_screen_for_dependet.dart @@ -513,6 +513,14 @@ class _DashboardScreenForDependetState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Welcome greeting - same "Welcome Back" label + large bold name design used + // on the last-login screen, shown for dependent logins only. + if (ApiConsts.isDependetLogin && (AppState().dependentName ?? "").isNotEmpty) ...[ + LocaleKeys.welcomeBack.tr().toText12(), + AppState().dependentName!.toText24(isBold: true), + const SizedBox(height: 16), + ], + // Search Input Card _buildSearchBar(isAr), @@ -852,8 +860,9 @@ class _DashboardScreenForDependetState extends State offerSubtitle = item.offerTitle!.trim(); } - String? typeText = item.getDisplayType(); - bool isOnline = item.isOnlineStore; + // type can be a comma-separated combo (e.g. "online,instore") - normalizedTypes already + // splits that into its distinct parts, so a retailer offering both ways gets a chip each. + List offerTypes = item.normalizedTypes; String validityText = item.getDisplayValidity(); String usageText = ""; if (item.displayUsage.isNotEmpty) { @@ -949,19 +958,19 @@ class _DashboardScreenForDependetState extends State if (offerSubtitle.isNotEmpty) ...[const SizedBox(height: 2), offerSubtitle.toText14(maxlines: 1, overflow: TextOverflow.ellipsis, isBold: true, color: const Color(0xFF1E293B))], // Badges Row: Type, Validity, Location & Usage - if ((typeText != null && typeText.trim().isNotEmpty) || validityText.trim().isNotEmpty || cityText.trim().isNotEmpty || usageText.trim().isNotEmpty) ...[ + if (offerTypes.isNotEmpty || validityText.trim().isNotEmpty || cityText.trim().isNotEmpty || usageText.trim().isNotEmpty) ...[ const SizedBox(height: 6), Wrap( spacing: 5, runSpacing: 4, children: [ - // Type Chip (Online or In Store) - if (typeText != null && typeText.trim().isNotEmpty) + // One Type Chip per distinct type - Online (green) and/or In Store (blue). + for (String offerType in offerTypes) InfoChipWidget( - text: typeText, - icon: isOnline ? Icons.language_rounded : Icons.storefront_rounded, - backgroundColor: isOnline ? const Color(0xFFEFF6FF) : const Color(0xFFF5F3FF), - textColor: isOnline ? const Color(0xFF2563EB) : const Color(0xFF7C3AED), + text: _getTypeFilterTitle(offerType), + icon: _getTypeFilterIcon(offerType), + backgroundColor: offerType == 'online' ? const Color(0xFFE6F7F0) : const Color(0xFFEFF6FF), + textColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF2563EB), ), // Validity badge diff --git a/lib/ui/landing/wala_plus_retailer_details_screen.dart b/lib/ui/landing/wala_plus_retailer_details_screen.dart index fc17128..de5db02 100644 --- a/lib/ui/landing/wala_plus_retailer_details_screen.dart +++ b/lib/ui/landing/wala_plus_retailer_details_screen.dart @@ -605,69 +605,27 @@ class _WalaPlusRetailerDetailsScreenState extends State offerTypes = _retailer?.normalizedTypes ?? []; return Wrap( spacing: 12, runSpacing: 10, children: [ - InfoChipWidget( - text: storeTypeText, - icon: storeIcon, - backgroundColor: isOnline ? const Color(0xFFEFF6FF) : const Color(0xFFF5F3FF), - textColor: isOnline ? const Color(0xFF2563EB) : const Color(0xFF7C3AED), - ), - InfoChipWidget(text: validityText, backgroundColor: const Color(0xFFE6F7F0), textColor: const Color(0xFF00A651)), - InfoChipWidget( - text: cityText, - svgIcon: "assets/icons/location.svg", - isBold: false, - backgroundColor: const Color(0xFFF3F4F6), - textColor: const Color(0xFF4B5563), - iconColor: const Color(0xFF64748B), - iconTextGap: 2.5, - ), + // One Type Chip per distinct type - Online (green) and/or In Store (blue). + for (String offerType in offerTypes) + InfoChipWidget( + text: offerType == 'online' ? LocaleKeys.online.tr() : (offerType == 'instore' ? LocaleKeys.inStore.tr() : (offerType.isNotEmpty ? (offerType[0].toUpperCase() + offerType.substring(1)) : offerType)), + icon: offerType == 'online' ? Icons.language_rounded : Icons.storefront_rounded, + backgroundColor: offerType == 'online' ? const Color(0xFFE6F7F0) : const Color(0xFFEFF6FF), + textColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF2563EB), + ), ], ); } - String _getDisplayCity(bool isAr) { - if (_retailer?.locations != null && _retailer!.locations!.isNotEmpty) { - // 1. First pass: look for a specific city name (e.g. Riyadh, Jeddah, Hail) - for (var loc in _retailer!.locations!) { - String? cName = isAr ? loc.cityNameAr : loc.cityNameEn; - if (cName != null && cName.trim().isNotEmpty && cName != "null") { - return cName.trim(); - } - } - // 2. Second pass: fallback to 'city' field if not generic country - for (var loc in _retailer!.locations!) { - if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { - String c = loc.city!.trim(); - if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') { - return c; - } - } - } - // 3. Third pass: any non-empty city - for (var loc in _retailer!.locations!) { - if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { - return loc.city!.trim(); - } - } - } - if (_retailer?.type?.toLowerCase() == 'online') { - return LocaleKeys.ksa.tr(); - } - return LocaleKeys.riyadh.tr(); - } - String _getHowToUseHtml(bool isAr) { String? content;