You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2081 lines
73 KiB
Dart
2081 lines
73 KiB
Dart
import 'dart:developer';
|
|
import 'dart:ui' as ui;
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_html/flutter_html.dart';
|
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:mohem_flutter_app/api/api_client.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/exceptions/api_exception.dart';
|
|
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
|
|
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
|
|
import 'package:mohem_flutter_app/extensions/widget_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/widgets/app_bar_widget.dart';
|
|
import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.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';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class RetailerDetailsScreen extends StatefulWidget {
|
|
final WalaPlusRetailerModel? retailer;
|
|
final String? retailerId;
|
|
final String? userReference;
|
|
|
|
const RetailerDetailsScreen({
|
|
Key? key,
|
|
this.retailer,
|
|
this.retailerId,
|
|
this.userReference,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<RetailerDetailsScreen> createState() => _RetailerDetailsScreenState();
|
|
}
|
|
|
|
class _RetailerDetailsScreenState extends State<RetailerDetailsScreen> {
|
|
WalaPlusRetailerModel? _retailer;
|
|
bool _isLoading = true;
|
|
bool _hasDetailsData = false;
|
|
String? _effectiveRetailerId;
|
|
String? _effectiveUserReference;
|
|
bool _hasFetched = false;
|
|
bool _isRedeeming = false;
|
|
bool _isFavorite = false;
|
|
bool _isTogglingFavorite = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_retailer = widget.retailer;
|
|
_effectiveRetailerId = widget.retailerId ?? widget.retailer?.id;
|
|
_effectiveUserReference = widget.userReference;
|
|
_isFavorite = widget.retailer?.isFavorite ?? false;
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_effectiveRetailerId == null) {
|
|
Object? args = ModalRoute.of(context)?.settings.arguments;
|
|
if (args is WalaPlusRetailerModel) {
|
|
_retailer ??= args;
|
|
_effectiveRetailerId = args.id;
|
|
_isFavorite = args.isFavorite ?? _isFavorite;
|
|
} else if (args is String) {
|
|
_effectiveRetailerId = args;
|
|
} else if (args is Map) {
|
|
if (args['retailer'] is WalaPlusRetailerModel) {
|
|
_retailer ??= args['retailer'];
|
|
_effectiveRetailerId = _retailer?.id;
|
|
_isFavorite = _retailer?.isFavorite ?? _isFavorite;
|
|
} else if (args['retailerId'] != null) {
|
|
_effectiveRetailerId = args['retailerId']?.toString();
|
|
}
|
|
if (args['userReference'] != null) {
|
|
_effectiveUserReference = args['userReference']?.toString();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_effectiveUserReference == null || _effectiveUserReference!.isEmpty) {
|
|
String resolved = OffersAndDiscountsApiClient().resolveUserReference();
|
|
if (resolved.isNotEmpty) {
|
|
_effectiveUserReference = resolved;
|
|
}
|
|
}
|
|
|
|
// Exclusive offers are mapped straight from the legacy Offers & Discounts API into
|
|
// WalaPlusRetailerModel for display - their id isn't a real WalaPlus retailer id, so
|
|
// getWalaPlusRetailerById would just fail. Render the already-passed-in data as-is instead.
|
|
if (!_hasFetched && _retailer?.isExclusive == true) {
|
|
_hasFetched = true;
|
|
_isLoading = false;
|
|
_hasDetailsData = true;
|
|
} else if (!_hasFetched && _effectiveRetailerId != null) {
|
|
_hasFetched = true;
|
|
_fetchRetailerDetails();
|
|
}
|
|
}
|
|
|
|
Future<void> _fetchRetailerDetails() async {
|
|
if (_effectiveRetailerId == null) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
try {
|
|
debugPrint(
|
|
"WalaPlusRetailerDetailsScreen: fetching details for retailer ID: $_effectiveRetailerId",
|
|
);
|
|
WalaPlusRetailerModel? result = await OffersAndDiscountsApiClient()
|
|
.getWalaPlusRetailerById(
|
|
retailerId: _effectiveRetailerId!,
|
|
context: context,
|
|
userReference: _effectiveUserReference,
|
|
);
|
|
if (result != null && mounted) {
|
|
if ((result.image == null ||
|
|
result.image!.isEmpty ||
|
|
result.image == "null") &&
|
|
_retailer?.image != null) {
|
|
result.image = _retailer!.image;
|
|
}
|
|
if ((result.imageThumb == null ||
|
|
result.imageThumb!.isEmpty ||
|
|
result.imageThumb == "null") &&
|
|
_retailer?.imageThumb != null) {
|
|
result.imageThumb = _retailer!.imageThumb;
|
|
}
|
|
if ((result.logo == null ||
|
|
result.logo!.isEmpty ||
|
|
result.logo == "null") &&
|
|
_retailer?.logo != null) {
|
|
result.logo = _retailer!.logo;
|
|
}
|
|
if ((result.logoThumb == null ||
|
|
result.logoThumb!.isEmpty ||
|
|
result.logoThumb == "null") &&
|
|
_retailer?.logoThumb != null) {
|
|
result.logoThumb = _retailer!.logoThumb;
|
|
}
|
|
if ((result.name == null || result.name!.isEmpty) &&
|
|
_retailer?.name != null) {
|
|
result.name = _retailer!.name;
|
|
}
|
|
if ((result.type == null || result.type!.isEmpty) &&
|
|
_retailer?.type != null) {
|
|
result.type = _retailer!.type;
|
|
}
|
|
if ((result.locations == null || result.locations!.isEmpty) &&
|
|
_retailer?.locations != null &&
|
|
_retailer!.locations!.isNotEmpty) {
|
|
result.locations = _retailer!.locations;
|
|
}
|
|
if ((result.offers == null || result.offers!.isEmpty) &&
|
|
_retailer?.offers != null &&
|
|
_retailer!.offers!.isNotEmpty) {
|
|
result.offers = _retailer!.offers;
|
|
}
|
|
if ((result.validity == null || result.validity!.isEmpty) &&
|
|
_retailer?.validity != null) {
|
|
result.validity = _retailer!.validity;
|
|
}
|
|
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;
|
|
}
|
|
if ((result.email == null || result.email!.isEmpty) &&
|
|
_retailer?.email != null) {
|
|
result.email = _retailer!.email;
|
|
}
|
|
if ((result.phone == null || result.phone!.isEmpty) &&
|
|
_retailer?.phone != null) {
|
|
result.phone = _retailer!.phone;
|
|
}
|
|
if ((result.categories == null || result.categories!.isEmpty) &&
|
|
_retailer?.categories != null &&
|
|
_retailer!.categories!.isNotEmpty) {
|
|
result.categories = _retailer!.categories;
|
|
}
|
|
|
|
setState(() {
|
|
_retailer = result;
|
|
_hasDetailsData = true;
|
|
_isFavorite = result.isFavorite ?? _isFavorite;
|
|
_retailer?.isFavorite = _isFavorite;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Error fetching retailer by ID: $e");
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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> _toggleFavorite() async {
|
|
if (_isTogglingFavorite) return;
|
|
String retailerId = _retailer?.id ?? _effectiveRetailerId ?? "";
|
|
if (retailerId.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isTogglingFavorite = true;
|
|
});
|
|
Utils.showLoading(context);
|
|
|
|
try {
|
|
bool targetFavorite = !_isFavorite;
|
|
GenericResponseModel? response;
|
|
if (targetFavorite) {
|
|
response = await OffersAndDiscountsApiClient().addWalaPlusFavorite(
|
|
retailerId: retailerId,
|
|
context: context,
|
|
userReference: _effectiveUserReference,
|
|
);
|
|
} else {
|
|
response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite(
|
|
retailerId: retailerId,
|
|
context: context,
|
|
userReference: _effectiveUserReference,
|
|
);
|
|
}
|
|
|
|
Utils.hideLoading(context);
|
|
|
|
if (response != null && response.errorMessage == null) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isFavorite = targetFavorite;
|
|
_retailer?.isFavorite = targetFavorite;
|
|
});
|
|
}
|
|
bool isAr =
|
|
Directionality.of(context) == ui.TextDirection.rtl ||
|
|
AppState().isArabic(context);
|
|
String? msg;
|
|
if (isAr) {
|
|
msg =
|
|
targetFavorite
|
|
? LocaleKeys.favoriteAddedSuccess.tr()
|
|
: LocaleKeys.favoriteRemovedSuccess.tr();
|
|
} else {
|
|
msg = response.wpFavoritesAddRemoveMessage;
|
|
if (msg == null || msg.trim().isEmpty) {
|
|
msg =
|
|
targetFavorite
|
|
? LocaleKeys.favoriteAddedSuccess.tr()
|
|
: LocaleKeys.favoriteRemovedSuccess.tr();
|
|
}
|
|
}
|
|
Utils.showToast(msg, isSuccess: true);
|
|
} else {
|
|
String error =
|
|
response?.errorEndUserMessage ??
|
|
response?.errorMessage ??
|
|
LocaleKeys.favoriteError.tr();
|
|
Utils.showToast(error);
|
|
}
|
|
} catch (e) {
|
|
Utils.hideLoading(context);
|
|
debugPrint("Error toggling favorite: $e");
|
|
String errorMsg = LocaleKeys.favoriteError.tr();
|
|
if (e is APIException &&
|
|
e.error?.errorMessage != null &&
|
|
e.error!.errorMessage!.isNotEmpty) {
|
|
errorMsg = e.error!.errorMessage!;
|
|
}
|
|
Utils.showToast(errorMsg);
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isTogglingFavorite = false;
|
|
});
|
|
} else {
|
|
_isTogglingFavorite = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
// possibly others) - any type that mentions "discount" or "qr" needs the in-page scanner,
|
|
// regardless of exact spelling/casing, so check by substring rather than exact match.
|
|
String normalizedOfferType = offerType?.trim().toLowerCase() ?? "";
|
|
bool shouldStartScanning =
|
|
normalizedOfferType.contains("discount") ||
|
|
normalizedOfferType.contains("qr");
|
|
log(
|
|
"shouldStartScanning: ${shouldStartScanning.toString()} for offerType: ${offerType.toString()}",
|
|
);
|
|
|
|
// Ask for camera permission app-side, before the WebView (and the page's own
|
|
// getUserMedia() call) even exists. By the time the page asks, the OS decision is
|
|
// already made, so onPermissionRequest can answer instantly - no mid-flight dialog
|
|
// racing the page's own timeout for its original getUserMedia() call.
|
|
// QR/discount-style offers need the camera to work at all, so if the user denies here,
|
|
// don't open the transaction URL - plain promo offers never reach this check.
|
|
if (shouldStartScanning) {
|
|
try {
|
|
PermissionStatus status = await Permission.camera.status;
|
|
log("permission status: ${status.toString()}");
|
|
if (status.isPermanentlyDenied) {
|
|
_showCameraPermissionSheet(
|
|
LocaleKeys.cameraPermissionPermanentlyDenied.tr(),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!status.isGranted) {
|
|
status = await Permission.camera.request();
|
|
}
|
|
|
|
if (status.isPermanentlyDenied) {
|
|
_showCameraPermissionSheet(
|
|
LocaleKeys.cameraPermissionPermanentlyDenied.tr(),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!status.isGranted) {
|
|
_showCameraPermissionSheet(
|
|
LocaleKeys.cameraPermissionRequiredToScan.tr(),
|
|
);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
debugPrint(
|
|
"Error requesting camera permission before opening transaction URL: $e",
|
|
);
|
|
_showCameraPermissionSheet(
|
|
LocaleKeys.cameraPermissionRequiredToScan.tr(),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder:
|
|
(BuildContext context) => AppInAppWebViewScreen(
|
|
url: url,
|
|
title: title,
|
|
onPageLoaded:
|
|
shouldStartScanning ? _clickStartScanningButton : null,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// showModalBottomSheet is dismissible by tapping outside by default (isDismissible: true),
|
|
// so no extra wiring is needed for that. "Open Settings" is the only action - there's nothing
|
|
// useful to retry from here, the user has to leave the app to fix this.
|
|
void _showCameraPermissionSheet(String message) {
|
|
ConfirmBottomSheet.show(
|
|
context,
|
|
message: message,
|
|
confirmText: LocaleKeys.openSettings.tr(),
|
|
showCancelButton: false,
|
|
icon: Icons.videocam_off_outlined,
|
|
isDestructive: false,
|
|
onConfirm: () {
|
|
openAppSettings();
|
|
},
|
|
);
|
|
}
|
|
|
|
// Vue's @click handlers are component methods, not something on window, so we find the
|
|
// actual button by its text and click it (lets Vue's own handler fire).
|
|
// Two buttons matter here: "Start Scanning" (the initial trigger) and "Try Again" (shown
|
|
// instead of "Start Scanning" if the camera permission flow takes a while - clicking it
|
|
// retries the same flow). Either may need clicking more than once if permission stays
|
|
// pending, so instead of a one-shot "clicked" flag we keep watching for as long as the
|
|
// safety-net timeout allows, with a short cooldown after each click so a single button
|
|
// occurrence can't get double-clicked from back-to-back mutation events before the page
|
|
// has a chance to react.
|
|
// This can be injected more than once (e.g. once on page load, again once the native camera
|
|
// permission result comes back) - state lives on `window`, not in the closure, so a repeat
|
|
// call never starts a second observer or spams clicks.
|
|
// Only called for offer types mentioning "discount" or "qr" - plain promo offers just show a
|
|
// promo code, no scanner.
|
|
void _clickStartScanningButton(InAppWebViewController controller) {
|
|
controller.evaluateJavascript(
|
|
source: r"""
|
|
(function() {
|
|
var cooldownMs = 800;
|
|
|
|
function tryClick() {
|
|
var now = Date.now();
|
|
if (now - (window.__mohemmScanLastClickAt || 0) < cooldownMs) return;
|
|
|
|
var buttons = document.querySelectorAll('button');
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
var text = (buttons[i].textContent || '').replace(/\s+/g, ' ').trim();
|
|
if (text.indexOf('Start Scanning') !== -1 || text.indexOf('Try Again') !== -1) {
|
|
buttons[i].click();
|
|
window.__mohemmScanLastClickAt = now;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
tryClick();
|
|
|
|
if (window.__mohemmScanObserverActive) return;
|
|
window.__mohemmScanObserverActive = true;
|
|
|
|
var observer = new MutationObserver(function() {
|
|
tryClick();
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
// Safety net only - stop watching after a while so we don't observe forever
|
|
// (e.g. permission denied for good, or user left the page).
|
|
setTimeout(function() {
|
|
observer.disconnect();
|
|
window.__mohemmScanObserverActive = false;
|
|
}, 120000);
|
|
})();
|
|
""",
|
|
);
|
|
}
|
|
|
|
void _onUseOffer() {
|
|
if (_isRedeeming) return;
|
|
|
|
List<WalaPlusOfferModel> offers = _retailer?.offers ?? [];
|
|
log("offers.length: ${offers.length}");
|
|
|
|
if (offers.length > 1) {
|
|
_showOffersBottomSheet(offers);
|
|
} else if (offers.length == 1) {
|
|
WalaPlusOfferModel singleOffer = offers.first;
|
|
String offerId =
|
|
(singleOffer.id != null && singleOffer.id!.isNotEmpty)
|
|
? singleOffer.id!
|
|
: (_retailer?.id ?? _effectiveRetailerId ?? "");
|
|
String offerType =
|
|
(singleOffer.type != null && singleOffer.type!.isNotEmpty)
|
|
? singleOffer.type!
|
|
: (_retailer?.offerType ?? "promo");
|
|
_redeemOffer(offerId: offerId, offerType: offerType);
|
|
} else {
|
|
String offerId = _retailer?.id ?? _effectiveRetailerId ?? "";
|
|
if (offerId.isEmpty) {
|
|
Utils.showToast(LocaleKeys.offerIdNotAvailable.tr());
|
|
return;
|
|
}
|
|
_redeemOffer(
|
|
offerId: offerId,
|
|
offerType: _retailer?.offerType ?? "promo",
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _redeemOffer({
|
|
required String offerId,
|
|
required String offerType,
|
|
}) async {
|
|
if (_isRedeeming) return;
|
|
if (offerId.isEmpty) {
|
|
Utils.showToast(LocaleKeys.offerIdNotAvailable.tr());
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isRedeeming = true;
|
|
});
|
|
Utils.showLoading(context);
|
|
|
|
try {
|
|
Map<String, dynamic>? 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'];
|
|
}
|
|
|
|
log("transactionUrl: ${transactionUrl.toString()}");
|
|
|
|
if (transactionUrl != null && transactionUrl.trim().isNotEmpty) {
|
|
await _openInAppBrowser(transactionUrl.trim(), offerType: offerType);
|
|
} else {
|
|
String? error =
|
|
res?['Error']?.toString() ??
|
|
res?['error']?.toString() ??
|
|
res?['ErrorMessage']?.toString() ??
|
|
res?['message']?.toString();
|
|
if (error != null && error.isNotEmpty && error != "null") {
|
|
Utils.showToast(error);
|
|
} else {
|
|
Utils.showToast(LocaleKeys.unableToRedeemOffer.tr());
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Utils.hideLoading(context);
|
|
debugPrint("Error redeeming offer: $e");
|
|
Utils.showToast(LocaleKeys.errorRedeemingOffer.tr());
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isRedeeming = false;
|
|
});
|
|
} else {
|
|
_isRedeeming = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showOffersBottomSheet(List<WalaPlusOfferModel> offers) {
|
|
bool isAr =
|
|
Directionality.of(context) == ui.TextDirection.rtl ||
|
|
AppState().isArabic(context);
|
|
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (BuildContext sheetContext) {
|
|
return Container(
|
|
constraints: BoxConstraints(
|
|
maxHeight: MediaQuery.of(sheetContext).size.height * 0.78,
|
|
),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFF8F9FA),
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
12.height,
|
|
Center(
|
|
child: Container(
|
|
width: 44,
|
|
height: 4.5,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFCBD5E1),
|
|
borderRadius: BorderRadius.circular(2.5),
|
|
),
|
|
),
|
|
),
|
|
12.height,
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
LocaleKeys.availableOffers.tr().toText18(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
),
|
|
4.height,
|
|
LocaleKeys.selectOfferToRedeem.tr().toText12(
|
|
color: const Color(0xFF64748B),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.close_rounded,
|
|
color: Color(0xFF64748B),
|
|
size: 24,
|
|
),
|
|
onPressed: () => Navigator.pop(sheetContext),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
14.height,
|
|
const Divider(height: 1, color: Color(0xFFF1F5F9)),
|
|
Flexible(
|
|
child: ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
|
itemCount: offers.length,
|
|
separatorBuilder: (_, __) => 14.height,
|
|
itemBuilder: (BuildContext _, int index) {
|
|
WalaPlusOfferModel offer = offers[index];
|
|
return _buildOfferBottomSheetCard(
|
|
sheetContext,
|
|
offer,
|
|
isAr,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildOfferBottomSheetCard(
|
|
BuildContext sheetContext,
|
|
WalaPlusOfferModel offer,
|
|
bool isAr,
|
|
) {
|
|
String title = offer.title ?? "";
|
|
String description = offer.description ?? "";
|
|
String type =
|
|
(offer.type != null && offer.type!.isNotEmpty)
|
|
? offer.type!
|
|
: "discount";
|
|
String? expiredAt = offer.expiredAt;
|
|
|
|
bool hasDifferentDescription =
|
|
description.trim().isNotEmpty && description.trim() != title.trim();
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xff000000).withOpacity(.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE6F7F0),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: type.toUpperCase().toText10(
|
|
isBold: true,
|
|
color: const Color(0xFF00A651),
|
|
),
|
|
),
|
|
if (expiredAt != null &&
|
|
expiredAt.trim().isNotEmpty &&
|
|
expiredAt != "null") ...[
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.event_outlined,
|
|
size: 12,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
const SizedBox(width: 4),
|
|
expiredAt.toText10(color: const Color(0xFF64748B)),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
10.height,
|
|
title.toText14(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
maxlines: 2,
|
|
),
|
|
if (hasDifferentDescription) ...[
|
|
6.height,
|
|
description.toText12(color: const Color(0xFF64748B), maxLine: 3),
|
|
],
|
|
DefaultButton(
|
|
LocaleKeys.useOffer.tr(),
|
|
() {
|
|
Navigator.pop(sheetContext);
|
|
String offerId =
|
|
(offer.id != null && offer.id!.isNotEmpty)
|
|
? offer.id!
|
|
: (_retailer?.id ?? _effectiveRetailerId ?? "");
|
|
String offerType =
|
|
(offer.type != null && offer.type!.isNotEmpty)
|
|
? offer.type!
|
|
: (_retailer?.offerType ?? "promo");
|
|
_redeemOffer(offerId: offerId, offerType: offerType);
|
|
},
|
|
height: 42,
|
|
borderRadius: 10,
|
|
fontSize: 14,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool isAr =
|
|
Directionality.of(context) == ui.TextDirection.rtl ||
|
|
AppState().isArabic(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
appBar: AppBarWidget(
|
|
context,
|
|
title: LocaleKeys.offerDetails.tr(),
|
|
showHomeButton: false,
|
|
),
|
|
body:
|
|
(_isLoading && !_hasDetailsData)
|
|
? _buildDetailsShimmer()
|
|
: Column(
|
|
children: [
|
|
if (_isLoading && _hasDetailsData)
|
|
const LinearProgressIndicator(
|
|
minHeight: 2.5,
|
|
backgroundColor: Colors.transparent,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
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,
|
|
|
|
// 3. Store Type & City Filter Chips
|
|
_buildChipsRow(isAr),
|
|
20.height,
|
|
|
|
// 4. Merchant Contact (Website, Phone, Email)
|
|
_buildMerchantContactSection(isAr),
|
|
|
|
// 5. Locations Section (Preview Cards + View All)
|
|
_buildLocationsSection(isAr),
|
|
|
|
// 6. Good to Know Section
|
|
_buildGoodToKnowSection(isAr),
|
|
|
|
20.height,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// 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 Shimmer.fromColors(
|
|
baseColor: const Color(0xFFE2E8F0),
|
|
highlightColor: const Color(0xFFF8FAFC),
|
|
child: SingleChildScrollView(
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
// Breadcrumbs placeholder
|
|
block(width: 180, height: 14),
|
|
14.height,
|
|
// Hero card placeholder
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
children: [
|
|
block(width: double.infinity, height: 200, radius: 0),
|
|
Container(
|
|
width: double.infinity,
|
|
color: const Color(0xFFCBD5E1),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
block(width: 160, height: 20),
|
|
8.height,
|
|
block(width: 220, height: 16),
|
|
10.height,
|
|
block(width: 120, height: 12),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
16.height,
|
|
// Chips row placeholder
|
|
Row(
|
|
children: <Widget>[
|
|
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),
|
|
12.height,
|
|
block(width: double.infinity, height: 120, radius: 16),
|
|
20.height,
|
|
// Locations placeholder
|
|
block(width: 100, height: 18),
|
|
12.height,
|
|
block(width: double.infinity, height: 68, radius: 16),
|
|
10.height,
|
|
block(width: double.infinity, height: 68, radius: 16),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBreadcrumbs(bool isAr) {
|
|
String? categoryName = _retailer?.getDisplayCategory(
|
|
isAr: isAr,
|
|
fallbackCategories: OffersAndDiscountsApiClient.cachedCategories,
|
|
);
|
|
String retailerName = _retailer?.name ?? '';
|
|
|
|
return Row(
|
|
children: [
|
|
if (categoryName != null && categoryName.isNotEmpty) ...[
|
|
categoryName.toText13(color: const Color(0xFF0D9488), isBold: true),
|
|
" / ".toText13(color: const Color(0xFF94A3B8)),
|
|
],
|
|
retailerName
|
|
.toText13(color: const Color(0xFF64748B), isBold: false)
|
|
.expanded,
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHeroCard(bool isAr) {
|
|
String? categoryName = _retailer?.getDisplayCategory(
|
|
isAr: isAr,
|
|
fallbackCategories: OffersAndDiscountsApiClient.cachedCategories,
|
|
);
|
|
String displayCity = _retailer?.getDisplayCity(isAr: isAr) ?? '';
|
|
String subtitleText = '';
|
|
if (categoryName != null &&
|
|
categoryName.isNotEmpty &&
|
|
displayCity.isNotEmpty) {
|
|
subtitleText = '$categoryName · $displayCity';
|
|
} else if (categoryName != null && categoryName.isNotEmpty) {
|
|
subtitleText = categoryName;
|
|
} else if (displayCity.isNotEmpty) {
|
|
subtitleText = displayCity;
|
|
}
|
|
|
|
String offerTitle =
|
|
_retailer?.offerTitle ??
|
|
(_retailer?.offers != null && _retailer!.offers!.isNotEmpty
|
|
? (_retailer!.offers!.first.title ?? '')
|
|
: '');
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xff000000).withOpacity(.08),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Top Image Banner with Favorite Heart Button
|
|
SizedBox(
|
|
height: 200,
|
|
width: double.infinity,
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
InkWell(
|
|
onTap: _openMerchantWebsite,
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
// 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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Bottom #143F36 Dark Green Container
|
|
Container(
|
|
width: double.infinity,
|
|
color: const Color(0xFF143F36),
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 18),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
(_retailer?.name ?? "").toText22(
|
|
isBold: true,
|
|
color: Colors.white,
|
|
isCentered: false,
|
|
),
|
|
if (offerTitle.trim().isNotEmpty) ...[
|
|
6.height,
|
|
offerTitle.trim().toText16(
|
|
isBold: false,
|
|
color: Colors.white,
|
|
isCentered: false,
|
|
maxlines: 2,
|
|
),
|
|
],
|
|
if (subtitleText.isNotEmpty) ...[
|
|
8.height,
|
|
subtitleText.toText12(
|
|
color: Colors.white.withOpacity(0.85),
|
|
isCenter: false,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildChipsRow(bool isAr) {
|
|
List<Widget> 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<String> 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);
|
|
|
|
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.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 (!isExclusive && !hasOnline && !hasInstore) {
|
|
hasInstore = true;
|
|
}
|
|
if (isExclusive) {
|
|
hasOnline = false;
|
|
hasInstore = false;
|
|
}
|
|
|
|
// Online chip
|
|
if (hasOnline) {
|
|
chips.add(
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE6F7F0),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: LocaleKeys.online.tr().toText12(
|
|
color: const Color(0xFF00A651),
|
|
isBold: true,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// In Store chip
|
|
if (hasInstore) {
|
|
chips.add(
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFEFF6FF),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: LocaleKeys.inStore.tr().toText12(
|
|
color: const Color(0xFF2563EB),
|
|
isBold: true,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Cities chips
|
|
List<String> distinctCities = [];
|
|
if (_retailer?.locations != null) {
|
|
Set<String> seen = {};
|
|
for (var loc in _retailer!.locations!) {
|
|
String c = loc.getLocalizedCity(isAr);
|
|
if (c.isEmpty) c = loc.city ?? '';
|
|
if (c.trim().isNotEmpty && !seen.contains(c.trim())) {
|
|
seen.add(c.trim());
|
|
distinctCities.add(c.trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (distinctCities.length <= 2) {
|
|
for (String c in distinctCities) {
|
|
chips.add(
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF1F5F9),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: c.toText12(color: const Color(0xFF334155), isBold: false),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
for (int i = 0; i < 2; i++) {
|
|
chips.add(
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF1F5F9),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: distinctCities[i].toText12(
|
|
color: const Color(0xFF334155),
|
|
isBold: false,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
chips.add(
|
|
InkWell(
|
|
onTap:
|
|
() => _showAllLocationsBottomSheet(_retailer!.locations!, isAr),
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF1F5F9),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: LocaleKeys.moreCitiesCount
|
|
.tr(args: ['${distinctCities.length - 2}'])
|
|
.toText12(color: const Color(0xFF334155), isBold: false),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (chips.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return Wrap(spacing: 8, runSpacing: 8, children: chips);
|
|
}
|
|
|
|
Widget _buildMerchantContactSection(bool isAr) {
|
|
String? website = _retailer?.resolvedWebsite;
|
|
String? phone = _retailer?.resolvedPhone;
|
|
String? email = _retailer?.resolvedEmail;
|
|
|
|
bool hasWebsite =
|
|
website != null && website.trim().isNotEmpty && website != "null";
|
|
bool hasPhone = phone != null && phone.trim().isNotEmpty && phone != "null";
|
|
bool hasEmail = email != null && email.trim().isNotEmpty && email != "null";
|
|
|
|
if (!hasWebsite && !hasPhone && !hasEmail) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
LocaleKeys.merchantContact.tr().toText18(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
),
|
|
12.height,
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xff000000).withOpacity(.03),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
if (hasWebsite) ...[
|
|
InkWell(
|
|
onTap: _openMerchantWebsite,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: const Radius.circular(16),
|
|
bottom:
|
|
(!hasPhone && !hasEmail)
|
|
? const Radius.circular(16)
|
|
: Radius.zero,
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 14,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
LocaleKeys.website.tr().toText12(
|
|
color: const Color(0xFF64748B),
|
|
),
|
|
4.height,
|
|
website.toText14(
|
|
color: const Color(0xFF2563EB),
|
|
isUnderLine: true,
|
|
weight: FontWeight.w500,
|
|
),
|
|
],
|
|
).expanded,
|
|
const Icon(
|
|
Icons.open_in_new_rounded,
|
|
size: 20,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
if (hasPhone) ...[
|
|
if (hasWebsite)
|
|
const Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
color: Color(0xFFF1F5F9),
|
|
),
|
|
InkWell(
|
|
onTap: () async {
|
|
try {
|
|
Uri phoneUri = Uri(scheme: 'tel', path: phone);
|
|
await launchUrl(phoneUri);
|
|
} catch (e) {
|
|
debugPrint("Error launching phone: $e");
|
|
}
|
|
},
|
|
borderRadius:
|
|
(!hasWebsite && !hasEmail)
|
|
? BorderRadius.circular(16)
|
|
: (!hasEmail
|
|
? const BorderRadius.vertical(
|
|
bottom: Radius.circular(16),
|
|
)
|
|
: BorderRadius.zero),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 14,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
LocaleKeys.phone.tr().toText12(
|
|
color: const Color(0xFF64748B),
|
|
),
|
|
4.height,
|
|
phone.toText14(
|
|
color: const Color(0xFF1E293B),
|
|
weight: FontWeight.w600,
|
|
),
|
|
],
|
|
).expanded,
|
|
const Icon(
|
|
Icons.phone_outlined,
|
|
size: 20,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
if (hasEmail) ...[
|
|
if (hasWebsite || hasPhone)
|
|
const Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
color: Color(0xFFF1F5F9),
|
|
),
|
|
InkWell(
|
|
onTap: () async {
|
|
try {
|
|
Uri emailUri = Uri(scheme: 'mailto', path: email);
|
|
await launchUrl(emailUri);
|
|
} catch (e) {
|
|
debugPrint("Error launching email: $e");
|
|
}
|
|
},
|
|
borderRadius: BorderRadius.vertical(
|
|
top:
|
|
(!hasWebsite && !hasPhone)
|
|
? const Radius.circular(16)
|
|
: Radius.zero,
|
|
bottom: const Radius.circular(16),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 14,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
LocaleKeys.email.tr().toText12(
|
|
color: const Color(0xFF64748B),
|
|
),
|
|
4.height,
|
|
email.toText14(
|
|
color: const Color(0xFF1E293B),
|
|
weight: FontWeight.w600,
|
|
),
|
|
],
|
|
).expanded,
|
|
const Icon(
|
|
Icons.mail_outline_rounded,
|
|
size: 20,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
20.height,
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildGoodToKnowSection(bool isAr) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
10.height,
|
|
LocaleKeys.goodToKnow.tr().toText18(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
),
|
|
14.height,
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Padding(
|
|
padding: EdgeInsets.only(top: 2),
|
|
child: Icon(
|
|
Icons.info_outline_rounded,
|
|
color: Color(0xFF0D9488),
|
|
size: 20,
|
|
),
|
|
),
|
|
10.width,
|
|
LocaleKeys.goodToKnowDesc
|
|
.tr()
|
|
.toText13(color: const Color(0xFF475569))
|
|
.expanded,
|
|
],
|
|
),
|
|
16.height,
|
|
const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)),
|
|
14.height,
|
|
InkWell(
|
|
onTap: () {
|
|
// Terms & conditions link will be provided later
|
|
},
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
LocaleKeys.termsAndConditions.tr().toText14(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
),
|
|
const Icon(
|
|
Icons.open_in_new_rounded,
|
|
size: 20,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
14.height,
|
|
const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)),
|
|
14.height,
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
LocaleKeys.haveAQuestionAboutOffer.tr().toText13(
|
|
color: const Color(0xFF64748B),
|
|
),
|
|
InkWell(
|
|
onTap: () {
|
|
// Get help link will be provided later
|
|
},
|
|
child: LocaleKeys.getHelp.tr().toText13(
|
|
color: const Color(0xFF0D9488),
|
|
isBold: true,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomActionBar(bool isAr) {
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: const Border(
|
|
top: BorderSide(color: Color(0xFFF1F5F9), width: 1),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xff000000).withOpacity(.06),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 52,
|
|
child: Material(
|
|
color:
|
|
_isRedeeming ? const Color(0xFF94A3B8) : const Color(0xFF169E8F),
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: InkWell(
|
|
onTap: _isRedeeming ? null : _onUseOffer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child:
|
|
_isRedeeming
|
|
? const Center(
|
|
child: SizedBox(
|
|
width: 22,
|
|
height: 22,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2.5,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLocationsSection(bool isAr) {
|
|
List<WalaPlusRetailerLocation> locations = _retailer?.locations ?? [];
|
|
if (locations.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
int totalCount = locations.length;
|
|
List<WalaPlusRetailerLocation> previewLocations =
|
|
totalCount <= 2 ? locations : locations.take(2).toList();
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
LocaleKeys.locations.tr().toText18(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
),
|
|
InkWell(
|
|
onTap: () => _showAllLocationsBottomSheet(locations, isAr),
|
|
borderRadius: BorderRadius.circular(6),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
LocaleKeys.viewAll.tr().toText14(
|
|
color: const Color(0xFF0D9488),
|
|
isBold: true,
|
|
),
|
|
4.width,
|
|
Icon(
|
|
isAr
|
|
? Icons.arrow_back_rounded
|
|
: Icons.arrow_forward_rounded,
|
|
size: 16,
|
|
color: const Color(0xFF0D9488),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
12.height,
|
|
ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
padding: EdgeInsets.zero,
|
|
itemCount: previewLocations.length,
|
|
separatorBuilder: (_, __) => 10.height,
|
|
itemBuilder: (BuildContext _, int index) {
|
|
return _buildLocationCard(previewLocations[index], isAr);
|
|
},
|
|
),
|
|
16.height,
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildLocationCard(WalaPlusRetailerLocation loc, bool isAr) {
|
|
String distanceText = loc.getDisplayDistance() ?? "";
|
|
String cityText = loc.getLocalizedCity(isAr);
|
|
if (cityText.isEmpty) {
|
|
cityText = loc.city ?? "";
|
|
}
|
|
String addressText = loc.address ?? "";
|
|
|
|
String primaryTitle = addressText.isNotEmpty ? addressText : cityText;
|
|
String? secondaryText =
|
|
distanceText.isNotEmpty
|
|
? distanceText
|
|
: (addressText.isNotEmpty ? cityText : null);
|
|
|
|
return InkWell(
|
|
onTap: () => _openMapForLocation(loc),
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xff000000).withOpacity(.03),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFF1F5F9),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.location_on_outlined,
|
|
size: 20,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
),
|
|
12.width,
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
primaryTitle.toText16(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
maxlines: 1,
|
|
),
|
|
if (secondaryText != null && secondaryText.isNotEmpty) ...[
|
|
3.height,
|
|
secondaryText.toText12(
|
|
color: const Color(0xFF0D9488),
|
|
weight: FontWeight.w600,
|
|
),
|
|
],
|
|
],
|
|
).expanded,
|
|
10.width,
|
|
const Icon(
|
|
Icons.open_in_new_rounded,
|
|
size: 20,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAllLocationsBottomSheet(
|
|
List<WalaPlusRetailerLocation> locations,
|
|
bool isAr,
|
|
) {
|
|
Map<String, int> cityCounts = {};
|
|
for (var loc in locations) {
|
|
String c = loc.getLocalizedCity(isAr);
|
|
if (c.isEmpty) c = loc.city ?? "";
|
|
if (c.trim().isNotEmpty) {
|
|
cityCounts[c.trim()] = (cityCounts[c.trim()] ?? 0) + 1;
|
|
}
|
|
}
|
|
List<String> sortedCities =
|
|
cityCounts.keys.toList()
|
|
..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0));
|
|
|
|
String selectedCity = "ALL";
|
|
String searchQuery = "";
|
|
TextEditingController searchController = TextEditingController();
|
|
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (BuildContext sheetContext) {
|
|
return StatefulBuilder(
|
|
builder: (BuildContext _, StateSetter setSheetState) {
|
|
List<WalaPlusRetailerLocation> filtered =
|
|
locations.where((loc) {
|
|
String c = loc.getLocalizedCity(isAr);
|
|
if (c.isEmpty) c = loc.city ?? "";
|
|
if (selectedCity != "ALL" && c.trim() != selectedCity) {
|
|
return false;
|
|
}
|
|
if (searchQuery.isNotEmpty) {
|
|
String q = searchQuery.toLowerCase();
|
|
String addr = (loc.address ?? "").toLowerCase();
|
|
String cityEn = (loc.cityNameEn ?? "").toLowerCase();
|
|
String cityAr = (loc.cityNameAr ?? "").toLowerCase();
|
|
String genericCity = (loc.city ?? "").toLowerCase();
|
|
if (!addr.contains(q) &&
|
|
!cityEn.contains(q) &&
|
|
!cityAr.contains(q) &&
|
|
!genericCity.contains(q)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}).toList();
|
|
|
|
return Container(
|
|
constraints: BoxConstraints(
|
|
maxHeight: MediaQuery.of(sheetContext).size.height * 0.85,
|
|
),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFF8F9FA),
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
12.height,
|
|
Center(
|
|
child: Container(
|
|
width: 44,
|
|
height: 4.5,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFCBD5E1),
|
|
borderRadius: BorderRadius.circular(2.5),
|
|
),
|
|
),
|
|
),
|
|
12.height,
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
LocaleKeys.locations.tr().toText18(
|
|
isBold: true,
|
|
color: const Color(0xFF1E293B),
|
|
),
|
|
3.height,
|
|
LocaleKeys.branchesCount
|
|
.tr(args: ['${locations.length}'])
|
|
.toText12(color: const Color(0xFF64748B)),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.close_rounded,
|
|
color: Color(0xFF64748B),
|
|
size: 24,
|
|
),
|
|
onPressed: () => Navigator.pop(sheetContext),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
12.height,
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Container(
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xff000000).withOpacity(.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
12.width,
|
|
const Icon(
|
|
Icons.search_rounded,
|
|
size: 20,
|
|
color: Color(0xFF94A3B8),
|
|
),
|
|
8.width,
|
|
Expanded(
|
|
child: TextField(
|
|
controller: searchController,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: LocaleKeys.searchBranches.tr(),
|
|
hintStyle: const TextStyle(
|
|
fontSize: 13,
|
|
color: Color(0xFF94A3B8),
|
|
),
|
|
border: InputBorder.none,
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
onChanged: (val) {
|
|
setSheetState(() {
|
|
searchQuery = val.trim();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
if (searchQuery.isNotEmpty)
|
|
GestureDetector(
|
|
onTap: () {
|
|
searchController.clear();
|
|
setSheetState(() {
|
|
searchQuery = "";
|
|
});
|
|
},
|
|
child: const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
child: Icon(
|
|
Icons.cancel,
|
|
size: 18,
|
|
color: Color(0xFF94A3B8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (sortedCities.length > 1) ...[
|
|
10.height,
|
|
SizedBox(
|
|
height: 38,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
itemCount: sortedCities.length + 1,
|
|
separatorBuilder: (_, __) => 8.width,
|
|
itemBuilder: (BuildContext _, int index) {
|
|
if (index == 0) {
|
|
bool isSelected = selectedCity == "ALL";
|
|
return GestureDetector(
|
|
onTap: () {
|
|
setSheetState(() {
|
|
selectedCity = "ALL";
|
|
});
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(6),
|
|
color:
|
|
isSelected
|
|
? MyColors.darkIconColor
|
|
: MyColors.lightGreyEAColor,
|
|
),
|
|
child:
|
|
'${LocaleKeys.allCities.tr()} (${locations.length})'
|
|
.toText12(
|
|
color:
|
|
isSelected
|
|
? MyColors.white
|
|
: MyColors.black,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
String cName = sortedCities[index - 1];
|
|
int cCount = cityCounts[cName] ?? 0;
|
|
bool isSelected = selectedCity == cName;
|
|
return GestureDetector(
|
|
onTap: () {
|
|
setSheetState(() {
|
|
selectedCity = cName;
|
|
});
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(6),
|
|
color:
|
|
isSelected
|
|
? MyColors.darkIconColor
|
|
: MyColors.lightGreyEAColor,
|
|
),
|
|
child: '$cName ($cCount)'.toText12(
|
|
color:
|
|
isSelected
|
|
? MyColors.white
|
|
: MyColors.black,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
12.height,
|
|
const Divider(height: 1, color: Color(0xFFF1F5F9)),
|
|
Flexible(
|
|
child:
|
|
filtered.isEmpty
|
|
? Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.location_off_outlined,
|
|
size: 44,
|
|
color: Color(0xFF94A3B8),
|
|
),
|
|
12.height,
|
|
LocaleKeys.noBranchesFound.tr().toText14(
|
|
isBold: true,
|
|
color: const Color(0xFF64748B),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: const EdgeInsets.fromLTRB(
|
|
20,
|
|
16,
|
|
20,
|
|
24,
|
|
),
|
|
itemCount: filtered.length,
|
|
separatorBuilder: (_, __) => 10.height,
|
|
itemBuilder: (BuildContext _, int index) {
|
|
return _buildLocationCard(
|
|
filtered[index],
|
|
isAr,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _openMapForLocation(WalaPlusRetailerLocation loc) async {
|
|
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}",
|
|
);
|
|
} 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)}",
|
|
);
|
|
}
|
|
|
|
try {
|
|
if (await canLaunchUrl(googleMapsUrl)) {
|
|
await launchUrl(googleMapsUrl, mode: LaunchMode.externalApplication);
|
|
} else {
|
|
await launchUrl(googleMapsUrl);
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Error launching maps: $e");
|
|
}
|
|
}
|
|
}
|