Bug Fixes

faiz_development_common
FaizHashmi 3 weeks ago
parent 944a09be60
commit 37320efa6e

@ -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]}";
}

@ -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<Uri>? _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);
}

@ -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);

@ -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);

@ -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<DropdownField> createState() => _DropdownFieldState();
@ -83,7 +116,15 @@ class _DropdownFieldState extends State<DropdownField> {
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<DropdownField> {
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<DropdownField> {
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<DropValue> 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<DropValue> 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,
],
),
),
),

Loading…
Cancel
Save