|
|
|
|
@ -41,12 +41,14 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
|
|
|
|
|
String? _effectiveRetailerId;
|
|
|
|
|
bool _hasFetched = false;
|
|
|
|
|
bool _isRedeeming = false;
|
|
|
|
|
bool _isFavorite = false;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_retailer = widget.retailer;
|
|
|
|
|
_effectiveRetailerId = widget.retailerId ?? widget.retailer?.id;
|
|
|
|
|
_isFavorite = widget.retailer?.isFavorite ?? false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
@ -57,6 +59,7 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
|
|
|
|
|
if (args is WalaPlusRetailerModel) {
|
|
|
|
|
_retailer ??= args;
|
|
|
|
|
_effectiveRetailerId = args.id;
|
|
|
|
|
_isFavorite = args.isFavorite ?? _isFavorite;
|
|
|
|
|
} else if (args is String) {
|
|
|
|
|
_effectiveRetailerId = args;
|
|
|
|
|
}
|
|
|
|
|
@ -115,6 +118,12 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
|
|
|
|
|
if ((result.offerTitle == null || result.offerTitle!.isEmpty) && _retailer?.offerTitle != null) {
|
|
|
|
|
result.offerTitle = _retailer!.offerTitle;
|
|
|
|
|
}
|
|
|
|
|
if ((result.website == null || result.website!.isEmpty || result.website == "null") && _retailer?.website != null) {
|
|
|
|
|
result.website = _retailer!.website;
|
|
|
|
|
}
|
|
|
|
|
if (result.isFavorite == null && _retailer?.isFavorite != null) {
|
|
|
|
|
result.isFavorite = _retailer!.isFavorite;
|
|
|
|
|
}
|
|
|
|
|
if (result.contact == null && _retailer?.contact != null) {
|
|
|
|
|
result.contact = _retailer!.contact;
|
|
|
|
|
}
|
|
|
|
|
@ -128,6 +137,7 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
|
|
|
|
|
setState(() {
|
|
|
|
|
_retailer = result;
|
|
|
|
|
_hasDetailsData = true;
|
|
|
|
|
_isFavorite = result.isFavorite ?? _isFavorite;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
@ -141,6 +151,41 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _openMerchantWebsite() async {
|
|
|
|
|
String? web = _retailer?.resolvedWebsite;
|
|
|
|
|
if (web == null || web.trim().isEmpty || web == "null") {
|
|
|
|
|
Utils.showToast(LocaleKeys.merchantWebsiteNotAvailable.tr());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
String url = web.trim();
|
|
|
|
|
try {
|
|
|
|
|
Uri uri = Uri.parse(url);
|
|
|
|
|
if (!uri.hasScheme) {
|
|
|
|
|
uri = Uri.parse("https://$url");
|
|
|
|
|
}
|
|
|
|
|
bool launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
|
|
|
if (!launched) {
|
|
|
|
|
await launchUrl(uri);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint("Error opening merchant website with external application: $e");
|
|
|
|
|
try {
|
|
|
|
|
await Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
builder: (BuildContext context) => AppInAppWebViewScreen(
|
|
|
|
|
url: url.startsWith("http") ? url : "https://$url",
|
|
|
|
|
title: _retailer?.name ?? LocaleKeys.visitMerchantWebsite.tr(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
debugPrint("Error opening merchant website in webview: $err");
|
|
|
|
|
Utils.showToast(url);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _openInAppBrowser(String url, {String? offerType}) async {
|
|
|
|
|
String title = _retailer?.name ?? LocaleKeys.offerDetails.tr();
|
|
|
|
|
// Offer types seen so far: "promo", "discount", "unique_promo", "promo_unique_qr" (and
|
|
|
|
|
@ -497,38 +542,86 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// 1. Top Banner Image with rounded corners
|
|
|
|
|
// 1. Top Banner Image with rounded corners (tappable to open merchant website)
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
height: 190,
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
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.image_outlined, size: 48, color: Color(0xFF9CA3AF))),
|
|
|
|
|
child: Material(
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: _openMerchantWebsite,
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
height: 190,
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
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.image_outlined, size: 48, color: Color(0xFF9CA3AF))),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
14.height,
|
|
|
|
|
|
|
|
|
|
// 2. Brand Strip (Human Resources - Retailer Logo - Dr Sulaiman Al Habib)
|
|
|
|
|
_buildBrandStrip(isAr),
|
|
|
|
|
18.height,
|
|
|
|
|
16.height,
|
|
|
|
|
|
|
|
|
|
// 3. Retailer Name (Centered, Bold) & Offer Title Subtitle
|
|
|
|
|
(_retailer?.name ?? "").toText22(isBold: true, isCentered: false, color: const Color(0xFF1E293B)),
|
|
|
|
|
// if (_retailer?.offerTitle != null && _retailer!.offerTitle!.trim().isNotEmpty && _retailer!.offerTitle!.trim() != _retailer!.name?.trim()) ...[
|
|
|
|
|
// 6.height,
|
|
|
|
|
// Center(child: _cleanHtml(_retailer!.offerTitle!).toText16(isCentered: true, color: const Color(0xFF168A89))),
|
|
|
|
|
// ],
|
|
|
|
|
18.height,
|
|
|
|
|
// 3. Retailer Name & Favorite Button
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: (_retailer?.name ?? "").toText22(isBold: true, isCentered: false, color: const Color(0xFF1E293B)),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Container(
|
|
|
|
|
width: 44,
|
|
|
|
|
height: 44,
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
color: Color(0xFFF1F5F9),
|
|
|
|
|
),
|
|
|
|
|
child: IconButton(
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
constraints: const BoxConstraints(),
|
|
|
|
|
icon: Icon(
|
|
|
|
|
_isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded,
|
|
|
|
|
size: 22,
|
|
|
|
|
color: _isFavorite ? const Color(0xFFE11D48) : const Color(0xFF1E293B),
|
|
|
|
|
),
|
|
|
|
|
tooltip: LocaleKeys.favorite.tr(),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isFavorite = !_isFavorite;
|
|
|
|
|
_retailer?.isFavorite = _isFavorite;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
12.height,
|
|
|
|
|
|
|
|
|
|
// 4. 3 Info Badges Row (Store Type, Validity, City)
|
|
|
|
|
// 4. Info Badges Row (Store Type, City Chips)
|
|
|
|
|
_buildInfoBadgesRow(isAr),
|
|
|
|
|
|
|
|
|
|
// 5. Visit Merchant Website Link
|
|
|
|
|
if (_retailer?.resolvedWebsite != null && _retailer!.resolvedWebsite!.trim().isNotEmpty) ...[
|
|
|
|
|
14.height,
|
|
|
|
|
InkWell(
|
|
|
|
|
onTap: _openMerchantWebsite,
|
|
|
|
|
child: LocaleKeys.visitMerchantWebsite.tr().toText14(
|
|
|
|
|
color: const Color(0xFF0D9488),
|
|
|
|
|
isUnderLine: true,
|
|
|
|
|
weight: FontWeight.w600,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
10.height,
|
|
|
|
|
|
|
|
|
|
// 5. Description Section (parsed as HTML)
|
|
|
|
|
@ -609,18 +702,40 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
|
|
|
|
|
// that into its distinct parts, so a retailer offering both ways gets a chip each, same
|
|
|
|
|
// as the dashboard card.
|
|
|
|
|
List<String> offerTypes = _retailer?.normalizedTypes ?? <String>[];
|
|
|
|
|
List<String> cities = _retailer?.getDistinctCities(isAr: isAr) ?? <String>[];
|
|
|
|
|
|
|
|
|
|
return Wrap(
|
|
|
|
|
spacing: 12,
|
|
|
|
|
runSpacing: 10,
|
|
|
|
|
spacing: 8,
|
|
|
|
|
runSpacing: 8,
|
|
|
|
|
children: [
|
|
|
|
|
// 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,
|
|
|
|
|
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_outlined,
|
|
|
|
|
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),
|
|
|
|
|
horizontalPadding: 10,
|
|
|
|
|
verticalPadding: 5,
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
iconSize: 14,
|
|
|
|
|
),
|
|
|
|
|
for (String city in cities)
|
|
|
|
|
InfoChipWidget(
|
|
|
|
|
text: city,
|
|
|
|
|
icon: Icons.location_on_outlined,
|
|
|
|
|
backgroundColor: const Color(0xFFF8FAFC),
|
|
|
|
|
textColor: const Color(0xFF475569),
|
|
|
|
|
borderColor: const Color(0xFFCBD5E1),
|
|
|
|
|
horizontalPadding: 10,
|
|
|
|
|
verticalPadding: 5,
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
iconSize: 14,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|