diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 09b6eec..41e7c99 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -169,12 +169,9 @@ extension FormatDate on String { String date = split("T")[0]; var dates = date.split("-"); - final isArabic = - Localizations.localeOf(context).languageCode == 'ar'; + final isArabic = Localizations.localeOf(context).languageCode == 'ar'; - final month = isArabic - ? getArabicMonth(int.parse(dates[1])) - : getMonth(int.parse(dates[1])); + final month = isArabic ? getArabicMonth(int.parse(dates[1])) : getMonth(int.parse(dates[1])); return "${dates[2]} $month, ${dates[0]}"; } diff --git a/lib/services/ad_deep_link_service.dart b/lib/services/ad_deep_link_service.dart index 7b89b10..a2fdf74 100644 --- a/lib/services/ad_deep_link_service.dart +++ b/lib/services/ad_deep_link_service.dart @@ -11,8 +11,10 @@ import 'package:mc_common_app/view_models/ad_view_model.dart'; import 'package:provider/provider.dart'; /// Handles opening the ad detail screen directly when the app is launched or -/// resumed via a shared ad link (e.g. https://ms.hmg.com/ad/1541), for both -/// Android App Links and iOS Universal Links. +/// resumed via a shared ad link, for Android App Links / iOS Universal Links +/// (https://ms.hmg.com/ad/1541) AND the custom-scheme fallback used by that +/// same web page when App Links verification isn't active +/// (mowatercustomer://ad/1541, mowaterprovider://ad/1541). class AdDeepLinkService { static final AppLinks _appLinks = AppLinks(); static StreamSubscription? _subscription; @@ -37,12 +39,20 @@ class AdDeepLinkService { static void _handleUri(Uri uri) { log("AdDeepLinkService: received $uri"); - final segments = uri.pathSegments; - if (segments.length < 2 || segments[0] != 'ad') return; - final int? adId = int.tryParse(segments[1]); - if (adId == null) return; + // https://ms.hmg.com/ad/1541 -> host="ms.hmg.com", pathSegments=["ad", "1541"] + // mowatercustomer://ad/1541 -> host="ad" (Dart treats it as the authority), pathSegments=["1541"] + int? adId; + if (uri.scheme == 'https') { + final segments = uri.pathSegments; + if (segments.length >= 2 && segments[0] == 'ad') { + adId = int.tryParse(segments[1]); + } + } else if (uri.host == 'ad' && uri.pathSegments.isNotEmpty) { + adId = int.tryParse(uri.pathSegments.first); + } + if (adId == null) return; _openAd(adId); } diff --git a/lib/utils/date_helper.dart b/lib/utils/date_helper.dart index e332e15..9717797 100644 --- a/lib/utils/date_helper.dart +++ b/lib/utils/date_helper.dart @@ -11,6 +11,16 @@ class DateHelper { return formatter.parse(dateString); } + /// True when [dateString] is at most [days] away from now, or already in the past + /// (e.g. an already-expired subscription still qualifies as "within 7 days"). + static bool isWithinDays(String? dateString, int days) { + if (dateString == null || dateString.isEmpty) return false; + final DateTime date = parseStringToDate(formatDateT(dateString)); + return date + .difference(DateTime.now()) + .inDays <= days; + } + static String formatAsShortDate(DateTime date) { final formatter = DateFormat.yMd(); return formatter.format(date); diff --git a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart index f4f226c..da086bc 100644 --- a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart @@ -745,9 +745,12 @@ class ShareAdButton extends StatelessWidget { } shareMessage += '\n'; - // Dynamic ad link - opens directly in the app if installed, otherwise - // falls back to the store via the hosted web page for this path. - shareMessage += 'https://ms.hmg.com/ad/${adDetails.id}\n'; + // Dynamic ad link - opens directly in the app if installed, otherwise falls + // back to the store via the hosted web page for this path. The "app" query + // param tells that page whether to target the customer or provider app, + // since both apps support ad sharing. + final String appParam = AppState().currentAppType == AppType.customer ? 'customer' : 'provider'; + shareMessage += 'https://ms.hmg.com/ad/${adDetails.id}?app=$appParam\n'; // Share using share_plus Share.share(shareMessage); diff --git a/lib/widgets/dropdown/dropdow_field.dart b/lib/widgets/dropdown/dropdow_field.dart index bba74b0..55c4c7d 100644 --- a/lib/widgets/dropdown/dropdow_field.dart +++ b/lib/widgets/dropdown/dropdow_field.dart @@ -14,17 +14,36 @@ class DropValue { String value; String subValue; bool? isEnabled; + String? valueN; - DropValue(this.id, this.value, this.subValue, {this.isEnabled = true}); + DropValue(this.id, this.value, this.subValue, {this.valueN, this.isEnabled = true}); bool operator ==(o) => o is DropValue && o.value == value && o.id == id; @override String toString() { - return 'DropValue{id: $id, value: $value, subValue: $subValue, isEnabled: $isEnabled}'; + return 'DropValue{id: $id, value: $value, subValue: $subValue, valueN: $valueN, isEnabled: $isEnabled}'; } } +/// Resolves which label to display for a [DropValue], matching the localization +/// rules: Arabic + useValueNForArabic -> valueN (falls back to value), Arabic + +/// useSubValueForArabic -> subValue, otherwise -> value. +String _dropdownDisplayLabel( + DropValue value, { + required bool isArabic, + required bool useSubValueForArabic, + required bool useValueNForArabic, +}) { + if (isArabic && useValueNForArabic) { + return value.valueN ?? value.value; + } + if (isArabic && useSubValueForArabic) { + return value.subValue; + } + return value.value; +} + class DropdownField extends StatefulWidget { final String? hint; final String errorValue; @@ -35,9 +54,23 @@ class DropdownField extends StatefulWidget { final bool showAppointmentPickerVariant; final TextStyle? textStyle; final bool isSelectAble; + final bool isArabic; + final bool useSubValueForArabic; + final bool useValueNForArabic; const DropdownField(this.onSelect, - {super.key, this.hint, this.list, this.dropdownValue, this.onTap, this.errorValue = "", this.showAppointmentPickerVariant = false, this.textStyle, this.isSelectAble = true}); + {super.key, + this.hint, + this.list, + this.dropdownValue, + this.onTap, + this.errorValue = "", + this.showAppointmentPickerVariant = false, + this.textStyle, + this.isSelectAble = true, + this.isArabic = false, + this.useSubValueForArabic = false, + this.useValueNForArabic = false}); @override State createState() => _DropdownFieldState(); @@ -83,7 +116,15 @@ class _DropdownFieldState extends State { decoration: Utils.containerColorRadiusBorderWidth(MyColors.greyShadowColor, 0, MyColors.greyACColor, 2), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), width: double.infinity, - child: (dropdownValue?.value ?? "").toText( + child: (dropdownValue == null + ? "" + : _dropdownDisplayLabel( + dropdownValue!, + isArabic: widget.isArabic, + useSubValueForArabic: widget.useSubValueForArabic, + useValueNForArabic: widget.useValueNForArabic, + )) + .toText( color: MyColors.darkTextColor, fontSize: 15, fontWeight: MyFonts.Medium, @@ -114,7 +155,15 @@ class _DropdownFieldState extends State { Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: (dropdownValue?.value ?? (hasNoData ? LocaleKeys.noDataAvailable.tr() : "")).toText( + child: (dropdownValue == null + ? (hasNoData ? LocaleKeys.noDataAvailable.tr() : "") + : _dropdownDisplayLabel( + dropdownValue!, + isArabic: widget.isArabic, + useSubValueForArabic: widget.useSubValueForArabic, + useValueNForArabic: widget.useValueNForArabic, + )) + .toText( fontSize: 15, color: hasNoData ? MyColors.greyACColor : MyColors.darkTextColor, fontWeight: MyFonts.Medium, @@ -152,6 +201,9 @@ class _DropdownFieldState extends State { return _SearchableDropdownSheet( items: items, selectedValue: dropdownValue, + isArabic: widget.isArabic, + useSubValueForArabic: widget.useSubValueForArabic, + useValueNForArabic: widget.useValueNForArabic, onSelected: (value) { setState(() { dropdownValue = value; @@ -168,8 +220,18 @@ class _SearchableDropdownSheet extends StatefulWidget { final List items; final DropValue? selectedValue; final Function(DropValue) onSelected; + final bool isArabic; + final bool useSubValueForArabic; + final bool useValueNForArabic; - const _SearchableDropdownSheet({required this.items, required this.selectedValue, required this.onSelected}); + const _SearchableDropdownSheet({ + required this.items, + required this.selectedValue, + required this.onSelected, + this.isArabic = false, + this.useSubValueForArabic = false, + this.useValueNForArabic = false, + }); @override State<_SearchableDropdownSheet> createState() => _SearchableDropdownSheetState(); @@ -185,9 +247,16 @@ class _SearchableDropdownSheetState extends State<_SearchableDropdownSheet> { super.dispose(); } + String _labelFor(DropValue value) => _dropdownDisplayLabel( + value, + isArabic: widget.isArabic, + useSubValueForArabic: widget.useSubValueForArabic, + useValueNForArabic: widget.useValueNForArabic, + ); + List get _filteredItems { if (_searchQuery.isEmpty) return widget.items; - return widget.items.where((element) => element.value.toLowerCase().contains(_searchQuery.toLowerCase())).toList(); + return widget.items.where((element) => _labelFor(element).toLowerCase().contains(_searchQuery.toLowerCase())).toList(); } Widget _buildSearchField() { @@ -249,55 +318,55 @@ class _SearchableDropdownSheetState extends State<_SearchableDropdownSheet> { child: Material( type: MaterialType.transparency, child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - 12.height, - Container( - width: 40, - height: 4, - decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), - ), - 14.height, - _buildSearchField(), - 10.height, - Flexible( - child: filtered.isEmpty - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 30), - child: LocaleKeys.noDataAvailable.tr().toText(fontSize: 15, color: MyColors.greyACColor), - ) - : ListView.separated( - shrinkWrap: true, - padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: filtered.length, - separatorBuilder: (context, index) => Divider(height: 1, thickness: 1, color: Colors.grey[300]), - itemBuilder: (context, index) { - DropValue value = filtered[index]; - bool isEnabled = value.isEnabled ?? true; - bool isSelected = widget.selectedValue?.id == value.id; - return ListTile( - enabled: isEnabled, - selected: isSelected, - dense: false, - selectedTileColor: MyColors.darkPrimaryColor.withValues(alpha: 0.08), - title: value.value.toText( - fontSize: 15, - color: !isEnabled ? MyColors.greyACColor : (isSelected ? MyColors.darkPrimaryColor : MyColors.darkTextColor), - fontWeight: isSelected ? MyFonts.Bold : MyFonts.Medium, - ), - trailing: isSelected ? const Icon(Icons.check, color: MyColors.darkPrimaryColor) : null, - onTap: !isEnabled - ? null - : () { - widget.onSelected(value); - Navigator.pop(context); - }, - ); - }, - ), - ), - 8.height, - ], + mainAxisSize: MainAxisSize.min, + children: [ + 12.height, + Container( + width: 40, + height: 4, + decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), + ), + 14.height, + _buildSearchField(), + 10.height, + Flexible( + child: filtered.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 30), + child: LocaleKeys.noDataAvailable.tr().toText(fontSize: 15, color: MyColors.greyACColor), + ) + : ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: filtered.length, + separatorBuilder: (context, index) => Divider(height: 1, thickness: 1, color: Colors.grey[300]), + itemBuilder: (context, index) { + DropValue value = filtered[index]; + bool isEnabled = value.isEnabled ?? true; + bool isSelected = widget.selectedValue?.id == value.id; + return ListTile( + enabled: isEnabled, + selected: isSelected, + dense: false, + selectedTileColor: MyColors.darkPrimaryColor.withValues(alpha: 0.08), + title: _labelFor(value).toText( + fontSize: 15, + color: !isEnabled ? MyColors.greyACColor : (isSelected ? MyColors.darkPrimaryColor : MyColors.darkTextColor), + fontWeight: isSelected ? MyFonts.Bold : MyFonts.Medium, + ), + trailing: isSelected ? const Icon(Icons.check, color: MyColors.darkPrimaryColor) : null, + onTap: !isEnabled + ? null + : () { + widget.onSelected(value); + Navigator.pop(context); + }, + ); + }, + ), + ), + 8.height, + ], ), ), ),