From 814b0ccd58c69d14eed415fa2a610808116fdcec Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 13 Aug 2026 10:45:33 +0300 Subject: [PATCH 1/5] Updates --- lib/core/utils/utils.dart | 39 +++++++++++++++++-- .../my_appointments_view_model.dart | 6 ++- .../medical_file/medical_file_page.dart | 2 +- lib/services/livecare_permission_service.dart | 6 +-- lib/splashPage.dart | 5 ++- pubspec.yaml | 7 ++-- 6 files changed, 52 insertions(+), 13 deletions(-) diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 320bd1cf..6d04fbc9 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -1057,9 +1057,42 @@ class Utils { return isHavePrivilege; } - static void openWebView({required String url}) { - Uri uri = Uri.parse(url); - launchUrl(uri, mode: LaunchMode.inAppBrowserView); + static Future openWebView({required String url}) async { + try { + Uri uri = Uri.parse(url); + + // Validate URL scheme for in-app browser + if (!uri.hasScheme || (!uri.scheme.startsWith('http'))) { + throw 'Invalid URL scheme. In-app browser only supports HTTP/HTTPS URLs'; + } + + // Check if URL can be launched + if (await canLaunchUrl(uri)) { + final launched = await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + + if (!launched) { + // Fallback to external browser + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } else { + // Fallback to external browser + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } catch (e) { + debugPrint('❌ Failed to open URL: $url - Error: $e'); + + // Try external browser as last resort + try { + final uri = Uri.parse(url); + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (e2) { + debugPrint('❌ Failed to open URL in external browser: $e2'); + // Optionally show user-friendly error message + } + } } static Color getCardBorderColor(int currentQueueStatus) { diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 832d94fe..b153c556 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -771,7 +771,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { patientMyDoctorsList = apiResponse.data!; isPatientMyDoctorsLoading = false; - isMyDoctorsDataToBeLoaded = false; + + if (!isTop8) { + isMyDoctorsDataToBeLoaded = false; + } + notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index cdee10db..cb3f7072 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -1063,7 +1063,7 @@ class _MedicalFilePageState extends State { ], ).onPress(() { // myAppointmentsViewModel.getPatientMyDoctors(); - myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true); + // myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true); Navigator.of(context).push( CustomPageRoute( page: MyDoctorsPage(), diff --git a/lib/services/livecare_permission_service.dart b/lib/services/livecare_permission_service.dart index bc5f2eef..52a4cc24 100644 --- a/lib/services/livecare_permission_service.dart +++ b/lib/services/livecare_permission_service.dart @@ -31,14 +31,14 @@ class LiveCarePermissionService { Permission.camera, Permission.microphone, Permission.notification, - if (Platform.isAndroid) Permission.systemAlertWindow, + // if (Platform.isAndroid) Permission.systemAlertWindow, ] : [ // Permission.camera, // Permission.microphone, Permission.notification, - if (Platform.isAndroid) Permission.systemAlertWindow, - ]; + // if (Platform.isAndroid) Permission.systemAlertWindow, + ]; try { final statuses = await permissions.request(); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index dc49428a..7407ffec 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/home/app_update_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; @@ -71,10 +72,10 @@ class _SplashScreenState extends State { } else { if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); - Navigator.of(context).pushReplacement(FadePage(page: OnboardingScreen())); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: OnboardingScreen())); } else { // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); } } }); diff --git a/pubspec.yaml b/pubspec.yaml index 9a05eb01..6ca96768 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,8 +2,8 @@ name: hmg_patient_app_new description: "New HMG Patient App" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 0.0.44+45 -#version: 0.0.14+1 +#version: 0.0.45+46 +version: 0.0.14+3 environment: sdk: ">=3.6.0 <4.0.0" @@ -63,7 +63,8 @@ dependencies: geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.13.1 - flutter_zoom_videosdk: 2.1.10 + # flutter_zoom_videosdk: 2.1.10 + flutter_zoom_videosdk: ^2.5.10 dart_jsonwebtoken: ^3.2.0 dartz: ^0.10.1 equatable: ^2.0.7 -- 2.30.2 From 636610accea6c8882772cf5565f4299cfbbb53da Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 13 Aug 2026 16:46:19 +0300 Subject: [PATCH 2/5] Updates for Zoom --- lib/core/api/api_client.dart | 2 +- lib/core/utils/zoom_config.dart | 6 + lib/presentation/home/landing_page.dart | 17 + .../tele_consultation/zoom/call_screen.dart | 40 +- .../date_range_calender.dart | 354 ++++++++++-------- .../viewmodel/date_range_calendar_model.dart | 14 +- pubspec.yaml | 2 +- 7 files changed, 258 insertions(+), 177 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index ebac476d..4d8d1650 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 3310954; + // body['PatientID'] = 945786; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/utils/zoom_config.dart b/lib/core/utils/zoom_config.dart index 09370c00..8262e83d 100644 --- a/lib/core/utils/zoom_config.dart +++ b/lib/core/utils/zoom_config.dart @@ -2,3 +2,9 @@ const Map configs = { 'ZOOM_SDK_KEY': 'b9T74nhfTg-ioP9urm970A', 'ZOOM_SDK_SECRET': 'KOzmjBNXQ1f4IPHpnngfL29uZvJMufSy2Fk8', }; + + +// const Map configs = { +// 'ZOOM_SDK_KEY': 'jYHoRpSUMTTefOwLq94Kyf1Kak513TUHpu78', +// 'ZOOM_SDK_SECRET': 'MOqi1zc18VFOMEaBDshFSHKLB0C2n9M0K48H', +// }; diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 3caed75a..db300d98 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -58,6 +58,7 @@ import 'package:hmg_patient_app_new/presentation/notifications/notifications_lis import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_page.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discounts.dart'; import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_doctor.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart'; @@ -291,6 +292,15 @@ class _LandingPageState extends State { page: FamilyMedicalScreen(), ), ); + + // Navigator.pushReplacementNamed( + // // context, + // GetIt.instance().navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments("yosemite-338", "123", "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); }, name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), imageWidget: Selector( @@ -314,6 +324,13 @@ class _LandingPageState extends State { onPressed: () async { await authVM.onLoginPressed(); + // Navigator.pushReplacementNamed( + // // context, + // GetIt.instance().navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // arguments: CallArguments("lake-tahoe-289", "123", "Patient", "40", "0", true, 1), + // ); + // Navigator.of(context).push( // CustomPageRoute( // // page: NotificationsListPage(), diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart index 64c6c471..1c9db67a 100644 --- a/lib/presentation/tele_consultation/zoom/call_screen.dart +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -84,7 +84,7 @@ class _CallScreenState extends State { //hide status bar SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack); - var circleButtonSize = 65.0; + var circleButtonSize = 65.h; Color backgroundColor = const Color(0xFF232323); Color buttonBackgroundColor = const Color.fromRGBO(0, 0, 0, 0.6); Color chatTextColor = const Color(0xFFAAAAAA); @@ -178,12 +178,12 @@ class _CallScreenState extends State { // "Join", // arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), // ); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); + // Navigator.pushAndRemoveUntil( + // context, + // CustomPageRoute( + // page: LandingNavigation(), + // ), + // (r) => false); }); final sessionNeedPasswordListener = eventListener.addListener(EventType.onSessionNeedPassword, (data) async { @@ -1773,16 +1773,17 @@ class _CallScreenState extends State { alignment: Alignment.centerRight, child: FractionallySizedBox( widthFactor: 0.2, - heightFactor: 0.6, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + heightFactor: 0.8, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: onPressAudio, - icon: isMuted.value ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/unmute@2x.png") : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/mute@2x.png"), - iconSize: circleButtonSize, - tooltip: isMuted.value == true ? "Unmute" : "Mute", - ), + icon: isMuted.value + ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/unmute@2x.png", width: circleButtonSize.h, height: circleButtonSize.h) + : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/mute@2x.png", width: circleButtonSize.h, height: circleButtonSize.h), + iconSize: circleButtonSize, + ), // IconButton( // onPressed: onPressShare, // icon: isSharing.value ? Image.asset("assets/images/png/zoom/share-off@2x.png") : Image.asset("assets/images/png/zoom/share-on@2x.png"), @@ -1791,9 +1792,11 @@ class _CallScreenState extends State { IconButton( onPressed: onPressVideo, iconSize: circleButtonSize, - icon: isVideoOn.value ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-off@2x.png") : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-on@2x.png"), - ), - Column( + icon: isVideoOn.value + ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-off@2x.png", width: circleButtonSize.h, height: circleButtonSize.h) + : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-on@2x.png", width: circleButtonSize.h, height: circleButtonSize.h), + ), + Column( children: [ IconButton( onPressed: () async { @@ -1808,7 +1811,8 @@ class _CallScreenState extends State { ), ], ), - )), + ), + ), // Container( // margin: const EdgeInsets.only(left: 16, right: 16, bottom: 40, top: 10), // alignment: Alignment.bottomCenter, diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index 463f516a..a544f010 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -56,8 +56,9 @@ class _DateRangeSelectorState extends State { late DateRangeSelectorRangeViewModel model; PickerViewMode _viewMode = PickerViewMode.date; - // Cache for Hijri conversions to avoid repeated calculations - final Map _hijriCache = {}; + // Prevent Syncfusion's initial onViewChanged callback from scheduling an + // unnecessary second build of the same month. + late int _displayedMonthKey; // Track the current Hijri month/year being displayed (for header display accuracy) HijriGregDate? _currentHijriDisplay; @@ -65,10 +66,14 @@ class _DateRangeSelectorState extends State { @override void initState() { _calendarController = DateRangePickerController(); + final today = DateTime.now(); + _displayedMonthKey = _monthKey(today); + if (widget.designType == CalendarDesignType.designV2) { + _calendarController.displayDate = today; + } scheduleMicrotask(() { if (widget.designType == CalendarDesignType.designV2) { // For V2, select today's date by default - final today = DateTime.now(); _calendarController.selectedDate = today; model.updateSelectedDate(today); } else { @@ -83,7 +88,9 @@ class _DateRangeSelectorState extends State { Widget build(BuildContext context) { model = Provider.of(context); - _calendarController.selectedRange = PickerDateRange(model.fromDate, model.toDate); + if (widget.designType != CalendarDesignType.designV2) { + _calendarController.selectedRange = PickerDateRange(model.fromDate, model.toDate); + } return widget.designType == CalendarDesignType.designV2 ? _buildDesignV2(widget.btnTitle) : _buildDefaultUI(); } @@ -273,18 +280,17 @@ class _DateRangeSelectorState extends State { tabs: [CustomTabBarModel(null, LocaleKeys.gregorianDate.tr()), CustomTabBarModel(null, LocaleKeys.hijriDate.tr())], onTabChange: (index) { final calendarModel = Provider.of(context, listen: false); + if (calendarModel.getSelectedTabIndex == index) return; + + // Update local fields before notifying the provider so + // the calendar changes with a single rebuild. + _viewMode = PickerViewMode.date; + _currentHijriDisplay = null; calendarModel.setTabIndex(index); // Notify parent widget about calendar type change (e.g., AuthenticationViewModel) final isGregorian = index == 0; widget.onCalendarTypeChanged?.call(isGregorian); - print('📅 Calendar type changed: ${isGregorian ? "Gregorian" : "Hijri"}'); - - // Reset view mode and Hijri display when switching calendar types - setState(() { - _viewMode = PickerViewMode.date; - _currentHijriDisplay = null; - }); }, ), ), @@ -306,12 +312,17 @@ class _DateRangeSelectorState extends State { child: Material( color: AppColors.whiteColor, // Rebuild when view mode or calendar type changes + // Syncfusion caches its rendered date cells. Include + // the calendar type in the key so Gregorian/Hijri day + // labels refresh immediately when the tab changes. + // Hijri conversion is now constant-time, so this + // targeted recreation remains responsive. key: ValueKey('${calendarModel.getSelectedTabIndex}-$_viewMode'), child: _viewMode == PickerViewMode.month ? _buildMonthPicker(isArabic, calendarModel) : _viewMode == PickerViewMode.year - ? _buildYearPicker(isArabic, calendarModel) - : (calendarModel.isHijri ? _buildHijriCalendar(isArabic) : _buildGregorianCalendar(isArabic)), + ? _buildYearPicker(isArabic, calendarModel) + : (calendarModel.isHijri ? _buildHijriCalendar(isArabic) : _buildGregorianCalendar(isArabic)), ), ), ], @@ -343,6 +354,8 @@ class _DateRangeSelectorState extends State { // Build Gregorian calendar (Design V2 - Single Date Selection) Widget _buildGregorianCalendar(bool isArabic) { + final today = DateTime.now(); + return SfDateRangePicker( controller: _calendarController, selectionMode: DateRangePickerSelectionMode.single, @@ -366,7 +379,7 @@ class _DateRangeSelectorState extends State { // Custom cell builder for square border selection cellBuilder: (BuildContext context, DateRangePickerCellDetails cellDetails) { if (cellDetails.date != DateTime(0)) { - final isToday = cellDetails.date.day == DateTime.now().day && cellDetails.date.month == DateTime.now().month && cellDetails.date.year == DateTime.now().year; + final isToday = cellDetails.date.day == today.day && cellDetails.date.month == today.month && cellDetails.date.year == today.year; final isSelected = _calendarController.selectedDate != null && cellDetails.date.day == _calendarController.selectedDate!.day && cellDetails.date.month == _calendarController.selectedDate!.month && @@ -414,12 +427,7 @@ class _DateRangeSelectorState extends State { ), ), onViewChanged: (DateRangePickerViewChangedArgs args) { - // Trigger rebuild when month changes - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() {}); - } - }); + _handleCalendarViewChanged(args); }, onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { if (args.value is DateTime) { @@ -433,105 +441,155 @@ class _DateRangeSelectorState extends State { ); } - // Build Hijri calendar (Design V2 - Single Date Selection) - // Note: SfDateRangePicker doesn't have built-in Hijri support - // Using Gregorian calendar with Hijri date conversion in the model + // Build one complete Hijri month instead of relabelling the dates in a + // Gregorian month. A Gregorian month overlaps two Hijri months, which would + // otherwise produce sequences such as 16...30, 1...17 in the same grid. Widget _buildHijriCalendar(bool isArabic) { final calendarModel = Provider.of(context, listen: false); + final today = DateTime.now(); + final displayedGregorian = _calendarController.displayDate ?? today; + final displayedHijri = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedGregorian); + final firstHijriDay = HijriGregDate(day: 1, month: displayedHijri.month, year: displayedHijri.year); + final firstGregorianDay = calendarModel.hijriToGregorian(firstHijriDay); + final leadingEmptyCells = firstGregorianDay.weekday % DateTime.daysPerWeek; + final daysInMonth = calendarModel.getDaysInMonth(displayedHijri.year, displayedHijri.month); + final selectedDate = _calendarController.selectedDate; + const weekdays = [ + DateTime.sunday, + DateTime.monday, + DateTime.tuesday, + DateTime.wednesday, + DateTime.thursday, + DateTime.friday, + DateTime.saturday, + ]; - return SfDateRangePicker( - controller: _calendarController, - selectionMode: DateRangePickerSelectionMode.single, - showNavigationArrow: false, - headerHeight: 0, - backgroundColor: AppColors.whiteColor, - monthViewSettings: DateRangePickerMonthViewSettings( - viewHeaderStyle: DateRangePickerViewHeaderStyle( - backgroundColor: AppColors.whiteColor, - textStyle: TextStyle( - fontSize: 12.f, - fontWeight: FontWeight.w600, - letterSpacing: -0.46, - color: AppColors.textColor, + return Column( + children: [ + SizedBox( + height: 32.h, + child: Row( + children: weekdays + .map( + (weekday) => Expanded( + child: Center( + child: Text( + calendarModel.getWeekdayNameLocalized(weekday, isArabic), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.textColor, + ), + ), + ), + ), + ) + .toList(), ), ), - showTrailingAndLeadingDates: false, - dayFormat: "EEE", - ), - cellBuilder: (BuildContext context, DateRangePickerCellDetails cellDetails) { - if (cellDetails.date != DateTime(0)) { - // Use cached Hijri conversion - final dateKey = '${cellDetails.date.year}-${cellDetails.date.month}-${cellDetails.date.day}'; - final hijriDate = _hijriCache.putIfAbsent(dateKey, () => calendarModel.gregorianToHijri(cellDetails.date)); + Expanded( + child: GridView.builder( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: DateTime.daysPerWeek, + mainAxisExtent: 36.h, + mainAxisSpacing: 4.h, + ), + itemCount: 42, + itemBuilder: (context, index) { + final hijriDay = index - leadingEmptyCells + 1; + if (hijriDay < 1 || hijriDay > daysInMonth) { + return const SizedBox.shrink(); + } - final isToday = cellDetails.date.day == DateTime.now().day && cellDetails.date.month == DateTime.now().month && cellDetails.date.year == DateTime.now().year; - final isSelected = _calendarController.selectedDate != null && - cellDetails.date.day == _calendarController.selectedDate!.day && - cellDetails.date.month == _calendarController.selectedDate!.month && - cellDetails.date.year == _calendarController.selectedDate!.year; + final hijriDate = HijriGregDate(day: hijriDay, month: displayedHijri.month, year: displayedHijri.year); + final gregorianDate = calendarModel.hijriToGregorian(hijriDate); + final isToday = _isSameDate(gregorianDate, today); + final isSelected = selectedDate != null && _isSameDate(gregorianDate, selectedDate); - return Container( - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.h), - border: isSelected ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, - color: Colors.transparent, - ), - child: Text( - hijriDate.day.toString(), - style: TextStyle( - fontFamily: "Poppins", - fontSize: 12.f, - color: AppColors.textColor, - fontWeight: isToday ? FontWeight.bold : (isSelected ? FontWeight.w600 : FontWeight.normal), - ), - ), - ); - } - return Container(); - }, - selectionShape: DateRangePickerSelectionShape.rectangle, - selectionRadius: 8.h, - selectionColor: Colors.transparent, - selectionTextStyle: TextStyle( - fontFamily: "Poppins", - color: AppColors.textColor, - fontWeight: FontWeight.w600, - ), - todayHighlightColor: Colors.transparent, - monthCellStyle: DateRangePickerMonthCellStyle( - textStyle: TextStyle( - fontFamily: "Poppins", - fontSize: 12.f, - color: AppColors.textColor, - ), - todayTextStyle: TextStyle( - fontFamily: "Poppins", - color: AppColors.textColor, - fontWeight: FontWeight.bold, + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + _calendarController.selectedDate = gregorianDate; + setState(() { + start = gregorianDate; + end = gregorianDate; + }); + model.updateSelectedDate(gregorianDate); + }, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.h), + border: isSelected ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, + color: Colors.transparent, + ), + child: Text( + hijriDay.toString(), + style: TextStyle( + fontFamily: "Poppins", + fontSize: 12.f, + color: AppColors.textColor, + fontWeight: isToday ? FontWeight.bold : (isSelected ? FontWeight.w600 : FontWeight.normal), + ), + ), + ), + ); + }, + ), ), - ), - onViewChanged: (DateRangePickerViewChangedArgs args) { - // Clear cache when month changes to avoid stale data - _hijriCache.clear(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() {}); - } - }); - }, - onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { - if (args.value is DateTime) { - setState(() { - start = args.value; - end = args.value; - }); - model.updateSelectedDate(args.value); - } - }, + ], ); } + bool _isSameDate(DateTime first, DateTime second) { + return first.year == second.year && first.month == second.month && first.day == second.day; + } + + int _monthKey(DateTime date) => (date.year * 100) + date.month; + + void _changeHijriMonth(DateRangCalenderModel calendarModel, int monthDelta) { + final displayedDate = _calendarController.displayDate ?? DateTime.now(); + final currentHijri = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedDate); + final zeroBasedMonth = (currentHijri.year * 12) + currentHijri.month - 1 + monthDelta; + final nextHijriDate = HijriGregDate( + day: 1, + month: (zeroBasedMonth % 12) + 1, + year: zeroBasedMonth ~/ 12, + ); + final nextGregorianDate = calendarModel.hijriToGregorian(nextHijriDate); + + setState(() { + _currentHijriDisplay = nextHijriDate; + _calendarController.displayDate = nextGregorianDate; + _displayedMonthKey = _monthKey(nextGregorianDate); + }); + } + + void _handleCalendarViewChanged(DateRangePickerViewChangedArgs args) { + final rangeStart = args.visibleDateRange.startDate; + final rangeEnd = args.visibleDateRange.endDate; + final displayedDate = _calendarController.displayDate ?? + (rangeEnd == null + ? rangeStart + : rangeStart?.add(Duration(days: rangeEnd.difference(rangeStart).inDays ~/ 2))); + + if (displayedDate == null) return; + + final newMonthKey = _monthKey(displayedDate); + if (newMonthKey == _displayedMonthKey) return; + + _displayedMonthKey = newMonthKey; + _currentHijriDisplay = null; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + } + // Build unified calendar header for both Gregorian and Hijri Widget _buildCalendarHeader(bool isArabic, DateRangCalenderModel calendarModel) { final displayedDate = _calendarController.displayDate ?? DateTime.now(); @@ -609,12 +667,10 @@ class _DateRangeSelectorState extends State { icon: Icon(Icons.chevron_left, color: AppColors.primaryRedColor), onPressed: () { if (_viewMode == PickerViewMode.date) { - _calendarController.backward!(); - // Update tracked Hijri display when navigating months if (calendarModel.isHijri) { - setState(() { - _currentHijriDisplay = null; // Will recalculate on next build - }); + _changeHijriMonth(calendarModel, -1); + } else { + _calendarController.backward!(); } } else if (_viewMode == PickerViewMode.year) { // Navigate years backward by 12 @@ -625,7 +681,7 @@ class _DateRangeSelectorState extends State { try { final newHijriDate = HijriGregDate(day: 1, month: hijriDate.month, year: hijriDate.year - 12); final newGregorianDate = calendarModel.hijriToGregorian(newHijriDate); - _calendarController.displayDate = DateTime(newGregorianDate.year, newGregorianDate.month, 1); + _calendarController.displayDate = newGregorianDate; _currentHijriDisplay = newHijriDate; } catch (e) { // Fallback @@ -645,12 +701,10 @@ class _DateRangeSelectorState extends State { icon: Icon(Icons.chevron_right, color: AppColors.primaryRedColor), onPressed: () { if (_viewMode == PickerViewMode.date) { - _calendarController.forward!(); - // Update tracked Hijri display when navigating months if (calendarModel.isHijri) { - setState(() { - _currentHijriDisplay = null; // Will recalculate on next build - }); + _changeHijriMonth(calendarModel, 1); + } else { + _calendarController.forward!(); } } else if (_viewMode == PickerViewMode.year) { // Navigate years forward by 12 @@ -661,7 +715,7 @@ class _DateRangeSelectorState extends State { try { final newHijriDate = HijriGregDate(day: 1, month: hijriDate.month, year: hijriDate.year + 12); final newGregorianDate = calendarModel.hijriToGregorian(newHijriDate); - _calendarController.displayDate = DateTime(newGregorianDate.year, newGregorianDate.month, 1); + _calendarController.displayDate = newGregorianDate; _currentHijriDisplay = newHijriDate; } catch (e) { // Fallback @@ -696,7 +750,7 @@ class _DateRangeSelectorState extends State { if (calendarModel.isHijri) { // For Hijri calendar final hijriCurrent = calendarModel.gregorianToHijri(currentDate); - final hijriDisplayed = calendarModel.gregorianToHijri(displayedDate); + final hijriDisplayed = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedDate); currentMonth = hijriCurrent.month; currentYear = hijriCurrent.year; displayedYear = hijriDisplayed.year; @@ -730,11 +784,9 @@ class _DateRangeSelectorState extends State { try { final hijriDate = HijriGregDate(day: 1, month: monthIndex, year: displayedYear); final gregorianDate = calendarModel.hijriToGregorian(hijriDate); - _calendarController.displayDate = DateTime(gregorianDate.year, gregorianDate.month, 1); + _calendarController.displayDate = gregorianDate; // Track the Hijri month/year for accurate header display _currentHijriDisplay = hijriDate; - // Clear cache since we changed the month - _hijriCache.clear(); } catch (e) { // Fallback if conversion fails _calendarController.displayDate = DateTime(displayedDate.year, monthIndex, 1); @@ -784,7 +836,7 @@ class _DateRangeSelectorState extends State { if (calendarModel.isHijri) { // For Hijri calendar - final hijriDisplayed = calendarModel.gregorianToHijri(displayedDate); + final hijriDisplayed = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedDate); final hijriCurrent = calendarModel.gregorianToHijri(DateTime.now()); displayedYear = hijriDisplayed.year; currentDisplayYear = hijriCurrent.year; @@ -818,7 +870,7 @@ class _DateRangeSelectorState extends State { final currentHijriMonth = _currentHijriDisplay?.month ?? calendarModel.gregorianToHijri(displayedDate).month; final hijriDate = HijriGregDate(day: 1, month: currentHijriMonth, year: year); final gregorianDate = calendarModel.hijriToGregorian(hijriDate); - _calendarController.displayDate = DateTime(gregorianDate.year, gregorianDate.month, 1); + _calendarController.displayDate = gregorianDate; // Track the Hijri month/year for accurate header display _currentHijriDisplay = hijriDate; } catch (e) { @@ -876,34 +928,34 @@ class _DateRangeSelectorState extends State { } displayDate(String label, String? date, bool isNotSelected) => Expanded( - child: Row( - spacing: 12.h, + child: Row( + spacing: 12.h, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.rangeCalendar, iconColor: isNotSelected ? AppColors.borderOnlyColor : AppColors.blackColor, height: 24, width: 24), + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rangeCalendar, iconColor: isNotSelected ? AppColors.borderOnlyColor : AppColors.blackColor, height: 24, width: 24), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: AppColors.inputLabelTextColor, - fontSize: 12.f, - fontWeight: FontWeight.w600, - ), - ), - Text( - date!, - style: TextStyle( - color: AppColors.textColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - ), - ) - ], + Text( + label, + style: TextStyle( + color: AppColors.inputLabelTextColor, + fontSize: 12.f, + fontWeight: FontWeight.w600, + ), + ), + Text( + date!, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), ) ], - ), - ); + ) + ], + ), + ); selectionChip(DateRangeSelectorRangeViewModel model) { return Row( diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart index 54c23a72..50b416cd 100644 --- a/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart @@ -122,12 +122,12 @@ class HijriGregConverter { static int _hijriYearStartJulian(int hijriYear) { if (hijriYear <= 1) return _hijriEpoch; - int totalDays = 0; - for (int year = 1; year < hijriYear; year++) { - totalDays += _hijriYearLength(year); - } - - return _hijriEpoch + totalDays; + // A Hijri year has 354 days, with 11 leap days in every 30-year + // cycle. Calculate the completed years directly instead of iterating + // from year 1 for every calendar cell. + final completedYears = hijriYear - 1; + final completedLeapDays = (3 + (11 * hijriYear)) ~/ 30; + return _hijriEpoch + (completedYears * 354) + completedLeapDays; } static int _hijriYearLength(int year) { @@ -358,6 +358,8 @@ class DateRangCalenderModel extends ChangeNotifier { } void setTabIndex(int index) { + if (_selectedTabIndex == index) return; + _selectedTabIndex = index; _calendarType = index == 0 ? CalendarType.gregorian : CalendarType.hijri; // Persist the selection for next time the widget opens diff --git a/pubspec.yaml b/pubspec.yaml index 6ca96768..4c6b92b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.13.1 - # flutter_zoom_videosdk: 2.1.10 +# flutter_zoom_videosdk: 2.1.10 flutter_zoom_videosdk: ^2.5.10 dart_jsonwebtoken: ^3.2.0 dartz: ^0.10.1 -- 2.30.2 From 22ab9b7d2dc158da01ea3ee9276621403a5d8d14 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 16 Aug 2026 15:47:39 +0300 Subject: [PATCH 3/5] Crashlytics fixes --- .../authentication_view_model.dart | 8 +++-- .../medical_file/medical_file_view_model.dart | 12 ++++++-- lib/features/payfort/payfort_repo.dart | 30 +++++++++++++++++++ lib/features/payfort/payfort_view_model.dart | 20 +++++++++++++ .../widgets/appointment_doctor_card.dart | 13 ++++---- .../book_appointment_page.dart | 3 +- lib/widgets/in_app_browser/InAppBrowser.dart | 3 ++ lib/widgets/input_widget.dart | 3 +- 8 files changed, 76 insertions(+), 16 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 7688cfa4..72ca42cb 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -824,7 +824,11 @@ class AuthenticationViewModel extends ChangeNotifier { onSuccess: (dynamic respData) async { try { if (respData != null) { - dynamic data = await SelectDeviceByImeiRespModelElement.fromJson(respData.toJson()); + SelectDeviceByImeiRespModelElement data = SelectDeviceByImeiRespModelElement.fromJson(respData.toJson()); + if (data.mobile == null || data.mobile == "" || data.identificationNo == null || data.identificationNo == "") { + return; + } + _appState.setSelectDeviceByImeiRespModelElement(data); LoaderBottomSheet.hideLoader(); @@ -856,8 +860,6 @@ class AuthenticationViewModel extends ChangeNotifier { } Future checkUserAuthentication({required OTPTypeEnum otpTypeEnum, Function(dynamic)? onSuccess, Function(String)? onError}) async { - // TODO: THIS SHOULD BE REMOVED LATER ON AND PASSED FROM APP STATE DIRECTLY INTO API CLIENT. BECAUSE THIS API ONLY NEEDS FEW PARAMS FROM USER - loginTypeEnum = otpTypeEnum == OTPTypeEnum.sms ? LoginTypeEnum.sms : LoginTypeEnum.whatsapp; // if (phoneNumberController.text.isEmpty) { diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 6fb8c8a2..a9bfb09c 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -45,7 +45,6 @@ class MedicalFileViewModel extends ChangeNotifier { List patientSickLeavesViewList = []; bool isSickLeavesSortByClinic = true; - bool isSickLeavesDataNeedsReloading = true; List patientAllergiesList = []; @@ -61,6 +60,7 @@ class MedicalFileViewModel extends ChangeNotifier { List patientMedicalReportsViewList = []; bool isMedicalReportsSortByClinic = true; + bool isMedicalReportsDataNeedsReloading = true; List patientMedicalReportAppointmentHistoryList = []; PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment; @@ -192,7 +192,7 @@ class MedicalFileViewModel extends ChangeNotifier { } setIsPatientMedicalReportsLoading(bool val) { - if (val) { + if (val && isMedicalReportsDataNeedsReloading) { onMedicalReportTabChange(0); patientMedicalReportList.clear(); patientMedicalReportsByClinic.clear(); @@ -200,8 +200,8 @@ class MedicalFileViewModel extends ChangeNotifier { patientMedicalReportsViewList.clear(); patientMedicalReportPDFBase64 = ""; isMedicalReportsSortByClinic = true; + isPatientMedicalReportsListLoading = val; } - isPatientMedicalReportsListLoading = val; notifyListeners(); } @@ -373,6 +373,10 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getPatientMedicalReportList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + if (!isMedicalReportsDataNeedsReloading) { + return; + } + patientMedicalReportList.clear(); patientMedicalReportRequestedList.clear(); patientMedicalReportReadyList.clear(); @@ -385,6 +389,7 @@ class MedicalFileViewModel extends ChangeNotifier { (failure) async => await errorHandlerService.handleError( failure: failure, onOkPressed: () { + isMedicalReportsDataNeedsReloading = true; onError!(failure.message); }, ), @@ -400,6 +405,7 @@ class MedicalFileViewModel extends ChangeNotifier { } onMedicalReportTabChange(0); isPatientMedicalReportsListLoading = false; + isMedicalReportsDataNeedsReloading = false; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/features/payfort/payfort_repo.dart b/lib/features/payfort/payfort_repo.dart index 3cd27a9d..9262d662 100644 --- a/lib/features/payfort/payfort_repo.dart +++ b/lib/features/payfort/payfort_repo.dart @@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/common_models/tamara_request_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; @@ -34,6 +35,8 @@ abstract class PayfortRepo { Future>> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel}); Future>> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel}); + + Future>> tamaraRequestInsert({required TamaraRequestModel tamaraRequestModel}); } class PayfortRepoImp implements PayfortRepo { @@ -350,4 +353,31 @@ class PayfortRepoImp implements PayfortRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> tamaraRequestInsert({required TamaraRequestModel tamaraRequestModel}) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(TAMARA_REQUEST_INSERT, body: tamaraRequestModel.toJson(), onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, isAllowAny: true, isPaymentServices: true); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/payfort/payfort_view_model.dart b/lib/features/payfort/payfort_view_model.dart index f567264f..031c3182 100644 --- a/lib/features/payfort/payfort_view_model.dart +++ b/lib/features/payfort/payfort_view_model.dart @@ -9,6 +9,7 @@ import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/tamara_request_model.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; @@ -116,6 +117,25 @@ class PayfortViewModel extends ChangeNotifier { ); } + Future tamaraRequestInsert({required TamaraRequestModel tamaraRequestModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.tamaraRequestInsert(tamaraRequestModel: tamaraRequestModel); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + // payfortProjectDetailsRespModel = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + Future payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await payfortRepo.payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel); diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 6e3f77d1..8738a449 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -152,16 +152,13 @@ class AppointmentDoctorCard extends StatelessWidget { ), AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), - icon: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! - ? AppAssets.walkin_appointment_icon + icon: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon, - iconColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : Colors.white, - labelText: patientAppointmentHistoryResponseModel.isLiveCareAppointment! - ? LocaleKeys.livecare.tr(context: context) + iconColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, + labelText: (patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context), - backgroundColor: - !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.greyColor : AppColors.successColor, - textColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : Colors.white, + backgroundColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.greyColor : AppColors.successColor, + textColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, ), ], ), diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index cb93d9e0..be91951a 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -389,7 +389,8 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Utils.buildImgWithNetwork( - url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!, + url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl ?? "https://eservices-test-bucket.s3.me-central-1.amazonaws.com/unkown_male.png", + // url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!, iconColor: AppColors.transparent, width: 64.h, height: 64.h, diff --git a/lib/widgets/in_app_browser/InAppBrowser.dart b/lib/widgets/in_app_browser/InAppBrowser.dart index e8370e5b..7f4836f6 100644 --- a/lib/widgets/in_app_browser/InAppBrowser.dart +++ b/lib/widgets/in_app_browser/InAppBrowser.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; enum _PAYMENT_TYPE { PACKAGES, PHARMACY, PATIENT } @@ -217,6 +218,8 @@ class MyInAppBrowser extends InAppBrowser { tamaraRequestModel.appointmentDate = (appoDate != null && appoDate != "") ? appoDate : null; tamaraRequestModel.isSchedule = ((appoNo != null && appoNo != "") && (appoDate != null && appoDate != "")) ? true : false; + getIt.get().tamaraRequestInsert(tamaraRequestModel: tamaraRequestModel); + // service.tamaraInsertRequest(tamaraRequestModel, context).then((res) { // // if (context != null) GifLoaderDialogUtils.hideDialog(context); generateTamaraURL(amount, orderDesc, transactionID, projId, emailId, paymentMethod, patientType, patientName, patientID, authenticatedUser, isLiveCareAppo, servID, LiveServID, appoDate, appoNo, diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index b7fe686f..b1214b87 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -198,7 +198,8 @@ class TextInputWidget extends StatelessWidget { ], ), ), - (suffix != null) ? suffix! : SizedBox.shrink() + // (suffix != null) ? suffix : SizedBox.shrink() + suffix ?? SizedBox.shrink() ], ), ), -- 2.30.2 From e93771df33a081eb98811ebdfa442d0afd7de3aa Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 16 Aug 2026 17:39:29 +0300 Subject: [PATCH 4/5] Pedia Dental issue fixes --- .../book_appointments_repo.dart | 5 ++ .../appointment_via_region_viewmodel.dart | 14 ++++ .../search_doctor_by_name.dart | 66 ++++++++++++------- .../book_appointment/select_clinic_page.dart | 21 ++++-- .../book_appointment/select_doctor_page.dart | 6 +- .../widgets/appointment_calendar.dart | 21 ++++-- .../book_appointment/widgets/doctor_card.dart | 29 +++++--- 7 files changed, 122 insertions(+), 40 deletions(-) diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index e5200f67..1e0b8739 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -2,7 +2,9 @@ import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart'; @@ -181,6 +183,9 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { "IsSearchAppointmnetByClinicID": isContinueDentalPlan ? false : true, "isDentalAllowedBackend": clinicID == 17 ? true : isContinueDentalPlan, "IsGetNearAppointment": isNearest, + "gender": getIt.get().isAuthenticated ? getIt.get().getAuthenticatedUser()!.gender! : 0, + "age": getIt.get().isAuthenticated ? getIt.get().getAuthenticatedUser()!.age! : 0, + "DateofBirth": getIt.get().isAuthenticated ? getIt.get().getAuthenticatedUser()!.dateofBirth! : null, if (isNearest) "SelectedDate": DateUtil.convertDateToString(DateTime.now()), "License": true }; diff --git a/lib/features/my_appointments/appointment_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index 1144be82..bb4179e1 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -161,6 +161,20 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { page: DentalChiefComplaintsPage(), ), ); + } else { + if (appState.getAuthenticatedUser()!.age! > 12) { + navigationService.push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } else { + navigationService.push( + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } } } if (clinicId == 253) { diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 165acfbf..4e9f1435 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -3,15 +3,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/doctors_filter.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -214,28 +218,46 @@ class _SearchDoctorByNameState extends State { bookAppointmentsViewModel: bookAppointmentsViewModel, isDoctorNameSearch: true, ).paddingSymmetrical(16.h, 0.h).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.getDoctorProfile( - onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(isDoctorAllowedToBook: true), - ), - ); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - ); + if (bookAppointmentsVM.filteredDoctorList[index].clinicID == 17 && getIt.get().getAuthenticatedUser()!.age! < 12) { + bookAppointmentsViewModel.setProjectID(bookAppointmentsVM.filteredDoctorList[index].projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(GetClinicsListResponseModel( + clinicID: bookAppointmentsVM.filteredDoctorList[index].clinicID, + clinicDescription: bookAppointmentsVM.filteredDoctorList[index].clinicName, + isLiveCareClinicAndOnline: false, + liveCareServiceID: 0, + liveCareClinicID: 0)); + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + Navigator.push( + context, + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } else { + bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile( + onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(isDoctorAllowedToBook: true), + ), + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + } + // Column( // children: bookAppointmentsVM.doctorsList[index].map((entry) { // final doctorIndex = entry.key; diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index bf4b5d58..785ebb62 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -1045,8 +1045,17 @@ class _SelectClinicPageState extends State { //Dental Clinic Flow if (clinic.clinicID == 17) { if (appState.isAuthenticated) { - initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); - return; + if (appState.getAuthenticatedUser()!.age! > 12) { + initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); + return; + } else { + Navigator.push( + context, + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); Navigator.of(context).push( @@ -1174,8 +1183,12 @@ class _SelectClinicPageState extends State { if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { bookAppointmentsViewModel.setProjectID(id); if (appState.isAuthenticated) { - initDentalAppointment(); - return SizedBox.shrink(); + if (appState.getAuthenticatedUser()!.age! > 12) { + initDentalAppointment(); + return SizedBox.shrink(); + } else { + return SizedBox.shrink(); + } } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index cad4fe0e..661c9a30 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -53,7 +53,11 @@ class _SelectDoctorPageState extends State { bookAppointmentsViewModel.getLiveCareDoctorsList(); } else { if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { - bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); + if (appState.getAuthenticatedUser()!.age! > 12) { + bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); + } else { + bookAppointmentsViewModel.getDoctorsList(isNearest: false); + } } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index bc6e214c..a95989a6 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -278,11 +278,22 @@ class _AppointmentCalendarState extends State { bookAppointmentsViewModel.setProjectID(bookAppointmentsViewModel.selectedDoctor.projectID.toString()); bookAppointmentsViewModel.setSelectedClinic(selectedClinic); bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); - Navigator.of(context).push( - CustomPageRoute( - page: DentalChiefComplaintsPage(), - ), - ); + if(appState.getAuthenticatedUser()!.age! > 12) { + Navigator.of(context).push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } else { + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!); + bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime, selectedDateDisplay); + Navigator.of(context).pop(); + Navigator.of(context).push( + CustomPageRoute( + page: ReviewAppointmentPage(), + ), + ); + } } else { bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!); bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime, selectedDateDisplay); diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index bb5a599d..8a48465a 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/laser/laser_appointment.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -185,14 +186,26 @@ class DoctorCard extends StatelessWidget { if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 17) { GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); - bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); - bookAppointmentsViewModel.setSelectedClinic(selectedClinic); - bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); - Navigator.of(context).push( - CustomPageRoute( - page: DentalChiefComplaintsPage(), - ), - ); + if (getIt.get().getAuthenticatedUser()!.age! > 12) { + bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); + bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } else { + bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + Navigator.push( + context, + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } } else if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 253) { GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); -- 2.30.2 From 83b17532d04acb085740935e985f3cd8272d4332 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 17 Aug 2026 16:19:01 +0300 Subject: [PATCH 5/5] Security changes implemented --- .../gradle/wrapper/gradle-wrapper.properties | 3 +- lib/core/app_state.dart | 9 +- lib/core/dependencies.dart | 5 + lib/core/talsec_config.dart | 15 ++ lib/main.dart | 42 ++++- lib/services/security_service.dart | 157 ++++++++++++++++++ lib/splashPage.dart | 79 ++++++--- lib/unsafe_device.dart | 117 +++++++++++++ pubspec.yaml | 2 + 9 files changed, 401 insertions(+), 28 deletions(-) create mode 100644 lib/core/talsec_config.dart create mode 100644 lib/services/security_service.dart create mode 100644 lib/unsafe_device.dart diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ac3b4792..2299155e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +#distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index a27b727e..0d015922 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -170,7 +170,6 @@ class AppState { set setIsAuthenticated(v) => isAuthenticated = v; - String deviceTypeID = ""; set setDeviceTypeID(v) => deviceTypeID = v; @@ -179,6 +178,14 @@ class AppState { String get getFamilyFileTokenID => _familyFileTokenID; + bool isSafeDevice = true; + + // set setIsSafeDevice(v) => isSafeDevice = v; + + set setIsSafeDevice(bool value) { + isSafeDevice = value; + } + set setFamilyFileTokenID(String value) { _familyFileTokenID = value; } diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index e4ca4c25..b6c7fb38 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -84,6 +84,7 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/services/permission_service.dart'; +import 'package:hmg_patient_app_new/services/security_service.dart'; import 'package:hmg_patient_app_new/core/services/turnstile_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; @@ -157,6 +158,10 @@ class AppDependencies { getIt.registerLazySingleton(() => PermissionService()); getIt.registerLazySingleton(() => TurnstileService(getIt())); + getIt.registerLazySingleton(() => SecurityServiceImpl( + appState: getIt(), + loggerService: getIt(), + )); // Repositories getIt.registerLazySingleton(() => CommonRepoImp(loggerService: getIt())); diff --git a/lib/core/talsec_config.dart b/lib/core/talsec_config.dart new file mode 100644 index 00000000..1603d419 --- /dev/null +++ b/lib/core/talsec_config.dart @@ -0,0 +1,15 @@ +import 'package:freerasp/freerasp.dart'; + +final talsecConfig = TalsecConfig( + androidConfig: AndroidConfig( + packageName: 'com.cloudsolutions.HMGPatientApp', + signingCertHashes: ['6tvWaoN5coG4SnfxGbdQlcLmM0J4ePQwDjrKIg+QkV0=', 'j6VEqVhrypHMIiXiFdRLDdGwjGaMGWY7KAdBJA+Z4Pc='], // Must be the release cert hash + supportedStores: ['com.android.vending'], // Google Play Store + ), + iosConfig: IOSConfig( + bundleIds: ['com.cloudsolutions.HMGPatientApp'], // iOS Bundle ID + teamId: '3A359E86ZF', // Found in Apple Developer portal + ), + watcherMail: '', // Required to receive security alerts + isProd: true, // Enforces strict checks for release builds +); \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 87746f14..b22bbb6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,11 +53,15 @@ import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/services/security_service.dart'; import 'package:hmg_patient_app_new/theme/app_theme.dart'; +import 'package:hmg_patient_app_new/unsafe_device.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; +import 'package:safe_device/safe_device.dart'; +import 'package:safe_device/safe_device_config.dart'; import 'core/utils/size_utils.dart'; import 'features/monthly_reports/terms_conditions_view_model.dart'; @@ -72,12 +76,12 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // flutter3_32 pub run easy_localization:generate -O ./lib/generated -f keys -o locale_keys.g.dart --source-dir ./assets/langs -class MyHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context) { - return super.createHttpClient(context)..badCertificateCallback = (X509Certificate cert, String host, int port) => true; - } -} +// class MyHttpOverrides extends HttpOverrides { +// @override +// HttpClient createHttpClient(SecurityContext? context) { +// return super.createHttpClient(context)..badCertificateCallback = (X509Certificate cert, String host, int port) => true; +// } +// } Future callAppStateInitializations() async { final String deviceTypeId = (Platform.isIOS @@ -110,6 +114,10 @@ Future callInitializations() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); + SafeDevice.init( + SafeDeviceConfig(mockLocationCheckEnabled: false), // disables mock location check on Android + ); + try { // Attempt to get the default app. If it exists, this avoids the error. await Firebase.app(); @@ -122,9 +130,29 @@ Future callInitializations() async { await AppDependencies.addDependencies(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - HttpOverrides.global = MyHttpOverrides(); + // HttpOverrides.global = MyHttpOverrides(); await callAppStateInitializations(); + // Initialize Security Service early to catch threats before app logic runs + if (kReleaseMode) { + await getIt.get().initialize(); + } + + // Set up critical threat callback to navigate to unsafe device page + getIt.get().setOnCriticalThreatCallback(() { + final navigationService = getIt.get(); + final context = navigationService.navigatorKey.currentContext; + + if (context != null) { + // Clear entire navigation stack and show unsafe device page + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const UnsafeDevice()), + (route) => false, // Remove all previous routes + ); + getIt.get().logError('🚨 Navigated to UnsafeDevice page - Critical threat detected'); + } + }); + // Initialize App Lifecycle Service to monitor background/foreground transitions getIt.get().initialize(); diff --git a/lib/services/security_service.dart b/lib/services/security_service.dart new file mode 100644 index 00000000..b72dffeb --- /dev/null +++ b/lib/services/security_service.dart @@ -0,0 +1,157 @@ +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/talsec_config.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Enum to categorize threat severity levels +enum ThreatSeverity { + critical, // Block app usage + warning, // Log only, don't block +} + +/// Model to track threat details +class ThreatEvent { + final String threatType; + final ThreatSeverity severity; + final DateTime timestamp; + final String? additionalInfo; + + ThreatEvent({ + required this.threatType, + required this.severity, + this.additionalInfo, + }) : timestamp = DateTime.now(); +} + +/// Callback type for critical threat detection +typedef OnCriticalThreatDetected = void Function(); + +/// Abstract class defining the security service interface +abstract class SecurityService { + /// Initialize and start the security monitoring + Future initialize(); + + /// Check if device is currently safe + bool get isSafeDevice; + + /// Get list of detected threats + List get detectedThreats; + + /// Set callback for when critical threat is detected + void setOnCriticalThreatCallback(OnCriticalThreatDetected callback); +} + +/// Implementation of SecurityService using Talsec (freeRASP) +class SecurityServiceImpl implements SecurityService { + final AppState appState; + final LoggerService loggerService; + + final List _detectedThreats = []; + bool _isInitialized = false; + OnCriticalThreatDetected? _onCriticalThreatCallback; + + SecurityServiceImpl({ + required this.appState, + required this.loggerService, + }); + + @override + bool get isSafeDevice => appState.isSafeDevice; + + @override + List get detectedThreats => List.unmodifiable(_detectedThreats); + + @override + Future initialize() async { + if (_isInitialized) { + loggerService.logInfo('SecurityService already initialized'); + return; + } + + try { + loggerService.logInfo('Initializing SecurityService with Talsec'); + + // Start the RASP engine + await Talsec.instance.start(talsecConfig); + + // Setup threat callbacks + final callback = ThreatCallback( + onAppIntegrity: () => _handleThreat('App Integrity', ThreatSeverity.critical), + onObfuscationIssues: () => _handleThreat('Obfuscation Issues', ThreatSeverity.warning), + onDebug: () => _handleThreat('Debug Mode', ThreatSeverity.critical), + onDeviceBinding: () => _handleThreat('Device Binding', ThreatSeverity.critical), + onDeviceID: () => _handleThreat('Device ID Mismatch', ThreatSeverity.critical), + onHooks: () => _handleThreat('Hooks Detected', ThreatSeverity.critical), + onPasscode: () => _handleThreat('Passcode Not Set', ThreatSeverity.warning), + onPrivilegedAccess: () => _handleThreat('Privileged Access (Root/Jailbreak)', ThreatSeverity.critical), + onSecureHardwareNotAvailable: () => _handleThreat('Secure Hardware Not Available', ThreatSeverity.warning), + onSimulator: () => _handleThreat('Simulator/Emulator Detected', ThreatSeverity.critical), + onSystemVPN: () => _handleThreat('System VPN Active', ThreatSeverity.warning), + onDevMode: () => _handleThreat('Developer Mode', ThreatSeverity.warning), + onADBEnabled: () => _handleThreat('USB Debugging Enabled', ThreatSeverity.warning), + onUnofficialStore: () => _handleThreat('Unofficial Store Installation', ThreatSeverity.critical), + onScreenshot: () => _handleThreat('Screenshot Detected', ThreatSeverity.warning), + onScreenRecording: () => _handleThreat('Screen Recording Active', ThreatSeverity.warning), + onMultiInstance: () => _handleThreat('Multiple Instances', ThreatSeverity.warning), + onLocationSpoofing: () => _handleThreat('Location Spoofing', ThreatSeverity.warning), + onTimeSpoofing: () => _handleThreat('Time Spoofing', ThreatSeverity.warning), + onAutomation: () => _handleThreat('Automation Detected', ThreatSeverity.warning), + onBootloader: () => _handleThreat('Unlocked Bootloader', ThreatSeverity.critical), + onMalware: (suspiciousApps) => _handleThreat('Malware/Suspicious Apps', ThreatSeverity.critical, additionalInfo: suspiciousApps.toString()), + ); + + Talsec.instance.attachListener(callback); + + _isInitialized = true; + loggerService.logInfo('SecurityService initialized successfully'); + } catch (e) { + loggerService.logError('Failed to initialize SecurityService: $e'); + if (!kDebugMode) { + FirebaseCrashlytics.instance.recordError( + e, + StackTrace.current, + reason: 'SecurityService initialization failed', + fatal: false, + ); + } + rethrow; + } + } + + @override + void setOnCriticalThreatCallback(OnCriticalThreatDetected callback) { + _onCriticalThreatCallback = callback; + loggerService.logInfo('Critical threat callback registered'); + } + + /// Handle detected threats with appropriate severity + void _handleThreat(String threatType, ThreatSeverity severity, {String? additionalInfo}) { + final threat = ThreatEvent( + threatType: threatType, + severity: severity, + additionalInfo: additionalInfo, + ); + + _detectedThreats.add(threat); + + // Log to console + if (severity == ThreatSeverity.critical) { + loggerService.logError('🔴 CRITICAL THREAT: $threatType ${additionalInfo != null ? "- $additionalInfo" : ""}'); + } else { + loggerService.logInfo('⚠️ WARNING: $threatType ${additionalInfo != null ? "- $additionalInfo" : ""}'); + } + + // Block app if critical threat + if (severity == ThreatSeverity.critical) { + appState.setIsSafeDevice = false; + loggerService.logError('Device marked as UNSAFE due to: $threatType'); + + // Trigger callback to navigate to unsafe device page + if (_onCriticalThreatCallback != null) { + _onCriticalThreatCallback!(); + } + } + } +} diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 7407ffec..8b39bf28 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -26,8 +26,10 @@ import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/services/zoom_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/unsafe_device.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; +import 'package:safe_device/safe_device.dart'; import 'core/cache_consts.dart'; import 'core/utils/push_notification_handler.dart'; @@ -42,8 +44,15 @@ class SplashPage extends StatefulWidget { class _SplashScreenState extends State { late AuthenticationViewModel authVm; + bool isJailBroken = false; + bool isRealDevice = true; + bool isDevelopmentModeEnable = false; + Future initializeStuff() async { listenerEvent(); + if (kReleaseMode) { + checkDeviceSafety(); + } Timer( Duration(milliseconds: 500), () async { @@ -51,34 +60,43 @@ class _SplashScreenState extends State { PushNotificationHandler().init(context); // Asyncronously }, ); + await authVm.getServicePrivilege(); Timer(Duration(seconds: 2, milliseconds: 500), () async { - bool isAppOpenedFromCall = getIt.get().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; - - // Initialize NotificationService using dependency injection - final notificationService = getIt.get(); - await notificationService.initialize(onNotificationClick: (payload) { - // Handle notification click here - }); + if (isJailBroken || !isRealDevice || !getIt.get().isSafeDevice) { + // Critical threat detected - navigate to unsafe device page + Navigator.of(getIt.get().navigatorKey.currentContext!).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const UnsafeDevice()), + (route) => false, // Remove all previous routes + ); + } else { + bool isAppOpenedFromCall = getIt.get().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; + // Initialize NotificationService using dependency injection + final notificationService = getIt.get(); + await notificationService.initialize(onNotificationClick: (payload) { + // Handle notification click here + }); - ZoomService().initializeZoomSDK(); + ZoomService().initializeZoomSDK(); - if (!kDebugMode) { - _initializeClarity(); - } + if (!kDebugMode) { + _initializeClarity(); + } - if (isAppOpenedFromCall) { - navigateToTeleConsult(); - } else { - if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { - // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); - Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: OnboardingScreen())); + if (isAppOpenedFromCall) { + navigateToTeleConsult(); } else { - // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); + if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: OnboardingScreen())); + } else { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); + } } } }); + // var zoom = ZoomVideoSdk(); // InitConfig initConfig = InitConfig( // domain: "zoom.us", @@ -154,6 +172,29 @@ class _SplashScreenState extends State { ); } + void checkDeviceSafety() { + try { + SafeDevice.isJailBroken.then((bool value) { + isJailBroken = value; + }); + SafeDevice.isJailBrokenCustom.then((bool value) { + isJailBroken = value; + }); + SafeDevice.isRealDevice.then((value) { + isRealDevice = value; + }); + + if (Platform.isAndroid) { + // isOnExternalStorage = await SafeDevice.isOnExternalStorage; + // SafeDevice.isDevelopmentModeEnable.then((value) { + // isDevelopmentModeEnable = value; + // }); + } + } catch (error) { + print(error); + } + } + Future listenerEvent() async { print('Call Canceled : ------->'); diff --git a/lib/unsafe_device.dart b/lib/unsafe_device.dart new file mode 100644 index 00000000..4712d70a --- /dev/null +++ b/lib/unsafe_device.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/services/security_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class UnsafeDevice extends StatefulWidget { + const UnsafeDevice({super.key}); + + @override + State createState() => _UnsafeDeviceState(); +} + +class _UnsafeDeviceState extends State { + @override + void initState() { + Talsec.instance.detachListener(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.whiteColor, + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo + Utils.buildImgWithAssets(icon: AppAssets.hmgLogo, width: MediaQuery.of(context).size.width * 0.7, height: 90.h, fit: BoxFit.contain), + SizedBox(height: 32.h), + // Warning Icon + Icon( + Icons.security, + size: 80.h, + color: AppColors.primaryRedColor, + ), + SizedBox(height: 24.h), + // Title + Text( + 'Unsafe Device Detected', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.bold, + color: AppColors.primaryRedColor, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 16.h), + + // Description + Text( + 'For your security, this app cannot run on devices with security vulnerabilities.', + style: TextStyle( + fontSize: 16.f, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 24.h), + // if (getIt.get().detectedThreats.isNotEmpty) ...[ + // Text( + // 'Detected Issues:', + // style: TextStyle( + // fontSize: 14.f, + // fontWeight: FontWeight.bold, + // color: Colors.black87, + // ), + // ), + // SizedBox(height: 8.h), + // Container( + // padding: EdgeInsets.all(12.w), + // decoration: BoxDecoration( + // color: Colors.red.withOpacity(0.1), + // borderRadius: BorderRadius.circular(8.r), + // border: Border.all(color: Colors.red.withOpacity(0.3)), + // ), + // child: Column( + // children: getIt + // .get() + // .detectedThreats + // .where((t) => t.severity == ThreatSeverity.critical) + // .take(5) // Show max 5 threats + // .map((threat) => Padding( + // padding: EdgeInsets.symmetric(vertical: 4.h), + // child: Row( + // children: [ + // Icon(Icons.error_outline, size: 16.f, color: Colors.red), + // SizedBox(width: 8.w), + // Expanded( + // child: Text( + // threat.threatType, + // style: TextStyle(fontSize: 12.f), + // ), + // ), + // ], + // ), + // )) + // .toList(), + // ), + // ), + // SizedBox(height: 24.h), + // ], + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4c6b92b3..0c5d78dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -111,6 +111,8 @@ dependencies: screen_brightness: ^1.0.1 flutter_screenshot_blocker: ^1.0.4 cloudflare_turnstile: ^3.7.2 + freerasp: ^8.2.1 + safe_device: ^1.4.1 dev_dependencies: flutter_test: -- 2.30.2