From cfd0c1673841046bd67d6c0ea78ff9eff7cc04bd Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 28 Jan 2026 14:03:20 +0300 Subject: [PATCH 1/2] updates & fixes --- ios/Podfile | 2 + lib/core/api/api_client.dart | 4 +- .../appointment_payment_page.dart | 14 +- .../widgets/appointment_card.dart | 173 ++++++++++-------- .../appointment_checkin_bottom_sheet.dart | 1 + .../widgets/appointment_doctor_card.dart | 4 +- .../book_appointment_page.dart | 4 +- .../immediate_livecare_payment_details.dart | 160 +++++++++++----- .../immediate_livecare_payment_page.dart | 2 + ...r_online_checkin_payment_details_page.dart | 9 +- .../er_online_checkin_payment_page.dart | 5 +- .../wallet_payment_confirm_page.dart | 2 + lib/presentation/home/landing_page.dart | 2 +- .../home/widgets/small_service_card.dart | 4 +- lib/services/livecare_permission_service.dart | 92 ++++++++++ 15 files changed, 335 insertions(+), 143 deletions(-) create mode 100644 lib/services/livecare_permission_service.dart diff --git a/ios/Podfile b/ios/Podfile index 9249e54..4c8bed1 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -54,6 +54,8 @@ post_install do |installer| 'PERMISSION_EVENTS_FULL_ACCESS=1', ## dart: PermissionGroup.reminders 'PERMISSION_REMINDERS=1', + ## dart: PermissionGroup.notification + 'PERMISSION_NOTIFICATIONS=1', ] build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index ba8f627..f87de29 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -208,10 +208,10 @@ class ApiClientImp implements ApiClient { // Handle body encoding based on isBodyPlainText flag final dynamic requestBody = isBodyPlainText ? body : json.encode(body); + debugPrint("uri: ${Uri.parse(url.trim())}"); + debugPrint("body: ${json.encode(body)}", wrapWidth: 2048); final response = await http.post(Uri.parse(url.trim()), body: requestBody, headers: headers); final int statusCode = response.statusCode; - log("uri: ${Uri.parse(url.trim())}"); - log("body: ${json.encode(body)}"); // log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index f817a89..dc9213f 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -459,7 +459,6 @@ class _AppointmentPaymentPageState extends State { await payfortViewModel.checkPaymentStatus( transactionID: transID, onSuccess: (apiResponse) async { - print(apiResponse.data); if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { await myAppointmentsViewModel.createAdvancePayment( paymentMethodName: selectedPaymentMethod, @@ -517,6 +516,7 @@ class _AppointmentPaymentPageState extends State { }); }); } else { + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)), @@ -564,14 +564,7 @@ class _AppointmentPaymentPageState extends State { } startApplePay() async { - showCommonBottomSheet(context, - child: Utils.getLoadingWidget(), - callBackFunc: (str) {}, - title: "", - height: ResponsiveExtension.screenHeight * 0.3, - isCloseButtonVisible: false, - isDismissible: false, - isFullScreen: false); + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); transID = Utils.getAppointmentTransID( widget.patientAppointmentHistoryResponseModel.projectID, widget.patientAppointmentHistoryResponseModel.clinicID, @@ -642,7 +635,7 @@ class _AppointmentPaymentPageState extends State { currency: appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", onFailed: (failureResult) async { log("failureResult: ${failureResult.message.toString()}"); - Navigator.of(context).pop(); + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), @@ -652,6 +645,7 @@ class _AppointmentPaymentPageState extends State { ); }, onSucceeded: (successResult) async { + LoaderBottomSheet.hideLoader(); log("successResult: ${successResult.responseMessage.toString()}"); selectedPaymentMethod = successResult.paymentOption ?? "VISA"; checkPaymentStatus(); diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index e1748c3..975308f 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -224,83 +224,108 @@ class AppointmentCard extends StatelessWidget { } Widget _buildActionArea(BuildContext context, AppState appState) { - if (isFromMedicalReport) { - if (isForEyeMeasurements) { - return SizedBox.shrink(); - } else { - return CustomButton( - text: LocaleKeys.selectAppointment.tr(context: context), - onPressed: () { - if (isForFeedback) { - contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel); - } else { - medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); - } - Navigator.pop(context, false); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - // height: isTablet || isFoldable ? 46.h : 40.h, - height: 40.h, - icon: AppAssets.checkmark_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - ); + if ((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) && AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) { + return CustomButton( + text: LocaleKeys.viewDetails.tr(context: context), + onPressed: () { + Navigator.of(context) + .push( + CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + ), + ) + .then((_) { + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, + ); + } else { + if (isFromMedicalReport) { + if (isForEyeMeasurements) { + return SizedBox.shrink(); + } else { + return CustomButton( + text: LocaleKeys.selectAppointment.tr(context: context), + onPressed: () { + if (isForFeedback) { + contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel); + } else { + medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); + } + Navigator.pop(context, false); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + ); + } } - } - return Row( - children: [ - Expanded( - flex: 6, - child: (AppointmentType.isArrived(patientAppointmentHistoryResponseModel) - ? _getArrivedButton(context) - : CustomButton( - text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction), - onPressed: () => _goToDetails(context), - backgroundColor: - AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), - borderColor: - AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), - textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), - fontSize: 14.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: 40.h, - icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), - iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), - iconSize: 15.h, - )) - .toShimmer2(isShow: isLoading), - ), - SizedBox(width: 8.h), - Expanded( - flex: 1, - child: Container( - height: (isFoldable || isTablet) ? 50.h : 40.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.textColor, borderRadius: 10.h), - child: Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - iconColor: AppColors.whiteColor, - width: 24.w, - height: 24.h, - fit: BoxFit.contain, + return Row( + children: [ + Expanded( + flex: 6, + child: (AppointmentType.isArrived(patientAppointmentHistoryResponseModel) + ? _getArrivedButton(context) + : CustomButton( + text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction), + onPressed: () => _goToDetails(context), + backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), + borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), + iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + iconSize: 15.h, + )) + .toShimmer2(isShow: isLoading), + ), + SizedBox(width: 8.h), + Expanded( + flex: 1, + child: Container( + height: (isFoldable || isTablet) ? 50.h : 40.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.textColor, borderRadius: 10.h), + child: Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.whiteColor, + width: 24.w, + height: 24.h, + fit: BoxFit.contain, + ), ), - ), - ).toShimmer2(isShow: isLoading).onPress(() { - _goToDetails(context); - }), - ), - ], - ); + ).toShimmer2(isShow: isLoading).onPress(() { + _goToDetails(context); + }), + ), + ], + ); + } } Widget _getArrivedButton(BuildContext context) { diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index c5139f1..b87a0a3 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -36,6 +36,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { + locationUtils = getIt.get(); appState = getIt.get(); FlutterNfcKit.nfcAvailability.then((value) { _supportsNFC = (value == NFCAvailability.available); diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 0c4aec1..8583d9b 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -89,8 +89,8 @@ class AppointmentDoctorCard extends StatelessWidget { runSpacing: 6.h, children: [ AppCustomChipWidget( - labelText: (patientAppointmentHistoryResponseModel.clinicName!.length > 15 - ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 12)}...' + labelText: (patientAppointmentHistoryResponseModel.clinicName!.length > 20 + ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...' : patientAppointmentHistoryResponseModel.clinicName!), labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), ), diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index cf5eef3..fa23979 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -458,8 +458,8 @@ class _BookAppointmentPageState extends State { ), SizedBox(width: 16.w), CustomButton( - height: 40.h, - width: 40.w, + height: 42.h, + width: 42.w, text: "", onPressed: () => context.navigateWithName(AppRoutes.userInfoSelection), icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward, diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index 3e48f8b..d522b45 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -18,6 +18,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immed import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; +import 'package:hmg_patient_app_new/services/livecare_permission_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -208,7 +209,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { ? CustomButton( text: LocaleKeys.confirmLiveCare.tr(context: context), onPressed: () async { - await askVideoCallPermission().then((val) async { + await askVideoCallPermission(context).then((val) async { if (val) { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.confirmingLiveCareRequest.tr(context: context)); @@ -271,31 +272,32 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { : CustomButton( text: LocaleKeys.payNow.tr(context: context), onPressed: () async { - await askVideoCallPermission().then((val) { + await askVideoCallPermission(context).then((val) { if (val) { Navigator.of(context).push( CustomPageRoute( page: ImmediateLiveCarePaymentPage(), ), ); - } else { - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), - context, - child: Utils.getWarningWidget( - loadingText: LocaleKeys.liveCarePermissionsMessage.tr(context: context), - isShowActionButtons: true, - onCancelTap: () { - Navigator.pop(context); - }, - onConfirmTap: () async { - openAppSettings(); - }), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); } + // else { + // showCommonBottomSheetWithoutHeight( + // title: LocaleKeys.notice.tr(context: context), + // context, + // child: Utils.getWarningWidget( + // loadingText: LocaleKeys.liveCarePermissionsMessage.tr(context: context), + // isShowActionButtons: true, + // onCancelTap: () { + // Navigator.pop(context); + // }, + // onConfirmTap: () async { + // openAppSettings(); + // }), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + // } }); }, backgroundColor: AppColors.infoColor, @@ -318,35 +320,111 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { ); } - Future askVideoCallPermission() async { - bool hasPermission = false; - if (Platform.isIOS) { - Map statuses = await [Permission.camera, Permission.microphone].request(); + Future askVideoCallPermission(BuildContext context) async { + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + + // If service returned nothing (error), treat as not granted + if (statuses.isEmpty) return false; + + bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; + bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; + bool notifGranted = statuses[Permission.notification]?.isGranted ?? false; - if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted) { - // Camera permission granted - hasPermission = true; - } else { - hasPermission = false; - } - } else { - Map statuses = await [Permission.camera, Permission.microphone, Permission.notification].request(); + // If all required permissions are already granted + if (cameraGranted && micGranted && notifGranted) return true; - if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted && statuses[Permission.notification] == PermissionStatus.granted) { - // Camera permission granted - hasPermission = true; - } else { - hasPermission = false; - } + // Collect only the missing permissions + final missing = []; + if (!cameraGranted) missing.add(Permission.camera); + if (!micGranted) missing.add(Permission.microphone); + if (!notifGranted) missing.add(Permission.notification); + + // If any of the missing permissions are permanently denied/restricted -> open settings (single dialog) + final permanent = missing.where((p) => (statuses[p]?.isPermanentlyDenied ?? false) || (statuses[p]?.isRestricted ?? false)).toList(); + if (permanent.isNotEmpty) { + final names = permanent.map((p) => LiveCarePermissionService.instance.friendlyName(p)).join(' and '); + final message = permanent.length == 1 ? '$names permission is permanently denied. Open app settings to allow it.' : '$names permissions are permanently denied. Open app settings to allow them.'; + await LiveCarePermissionService.instance.showOpenSettingsDialog( + context, + title: "Permissions Required", + message: message, + ); + return false; } - return hasPermission; + // Show one combined retry dialog for all missing permissions + final missingNames = missing.map((p) => LiveCarePermissionService.instance.friendlyName(p)).join(' and '); + final retry = await showDialog( + context: context, + builder: (c) => AlertDialog( + title: const Text('Permissions required'), + content: Text('$missingNames permission${missing.length > 1 ? 's are' : ' is'} required to start the video call. Retry now?'), + actions: [ + TextButton(onPressed: () => Navigator.of(c).pop(false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.of(c).pop(true), child: const Text('Retry')), + ], + ), + ); + + if (retry != true) return false; - // if (!(await Permission.camera.request().isGranted) || !(await Permission.microphone.request().isGranted)) { - // return false; - // } + // Request only the missing permissions + final newStatuses = {}; + for (final p in missing) { + newStatuses[p] = await p.request(); + } + + // Update grant state (preserve previously granted ones) + cameraGranted = newStatuses[Permission.camera]?.isGranted ?? cameraGranted; + micGranted = newStatuses[Permission.microphone]?.isGranted ?? micGranted; + notifGranted = newStatuses[Permission.notification]?.isGranted ?? notifGranted; + + // If any requested permission is now permanently denied -> open settings + final newlyPermanent = missing.where((p) => (newStatuses[p]?.isPermanentlyDenied ?? false) || (newStatuses[p]?.isRestricted ?? false)).toList(); + if (newlyPermanent.isNotEmpty) { + final names = newlyPermanent.map((p) => LiveCarePermissionService.instance.friendlyName(p)).join(' and '); + final message = + newlyPermanent.length == 1 ? '$names permission is permanently denied. Open app settings to allow it.' : '$names permissions are permanently denied. Open app settings to allow them.'; + await LiveCarePermissionService.instance.showOpenSettingsDialog( + context, + title: "Permissions Required", + message: message, + ); + return false; + } + + return cameraGranted && micGranted && notifGranted; } + // Future askVideoCallPermission() async { + // bool hasPermission = false; + // if (Platform.isIOS) { + // Map statuses = await [Permission.camera, Permission.microphone].request(); + // + // if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted) { + // // Camera permission granted + // hasPermission = true; + // } else { + // hasPermission = false; + // } + // } else { + // Map statuses = await [Permission.camera, Permission.microphone, Permission.notification].request(); + // + // if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted && statuses[Permission.notification] == PermissionStatus.granted) { + // // Camera permission granted + // hasPermission = true; + // } else { + // hasPermission = false; + // } + // } + // + // return hasPermission; + // + // // if (!(await Permission.camera.request().isGranted) || !(await Permission.microphone.request().isGranted)) { + // // return false; + // // } + // } + String getLiveCareType(BuildContext context, int callType) { switch (callType) { case 1: diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index 9ae7ee3..73da69f 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -432,6 +432,7 @@ class _ImmediateLiveCarePaymentPageState extends State } startApplePay() async { - // showCommonBottomSheet(context, - // child: Utils.getLoadingWidget(), callBackFunc: (str) {}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isCloseButtonVisible: false, isDismissible: false, isFullScreen: false); LoaderBottomSheet.showLoader(); transID = Utils.getAdvancePaymentTransID( emergencyServicesViewModel.selectedHospital!.iD, @@ -405,8 +403,8 @@ class _ErOnlineCheckinPaymentPageState extends State applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel!.shaRequest, currency: appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", onFailed: (failureResult) async { + LoaderBottomSheet.hideLoader(); log("failureResult: ${failureResult.message.toString()}"); - Navigator.of(context).pop(); showCommonBottomSheetWithoutHeight( context, child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), @@ -416,6 +414,7 @@ class _ErOnlineCheckinPaymentPageState extends State ); }, onSucceeded: (successResult) async { + LoaderBottomSheet.hideLoader(); log("successResult: ${successResult.responseMessage.toString()}"); selectedPaymentMethod = successResult.paymentOption ?? "VISA"; checkPaymentStatus(); diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index 3dec5aa..4585b7d 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -316,6 +316,8 @@ class _WalletPaymentConfirmPageState extends State { context, child: Utils.getSuccessWidget(loadingText: "Payment Successful!"), callBackFunc: () { + habibWalletVM.initHabibWalletProvider(); + habibWalletVM.getPatientBalanceAmount(); Navigator.of(context).pop(); Navigator.of(context).pop(); }, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index e0c15d0..a8a64fa 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -652,7 +652,7 @@ class _LandingPageState extends State { ], ).paddingSymmetrical(24.w, 0.h), SizedBox( - height: 411.h, + height: 431.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: LandingPageData.getServiceCardsList.length, diff --git a/lib/presentation/home/widgets/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index cf49a26..4685e48 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -65,10 +65,10 @@ class SmallServiceCard extends StatelessWidget { Utils.buildSvgWithAssets(icon: icon, iconColor: iconColor, width: 32.h, height: 32.h), SizedBox(height: 6.h), if (isFoldable || isTablet) ...[ - title.tr(context: context).toText11(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2), + title.tr(context: context).toText11(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 2), subtitle.tr(context: context).toText11(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2), ] else ...[ - title.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2), + title.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 2), subtitle.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2), ] ], diff --git a/lib/services/livecare_permission_service.dart b/lib/services/livecare_permission_service.dart new file mode 100644 index 0000000..a9a5d29 --- /dev/null +++ b/lib/services/livecare_permission_service.dart @@ -0,0 +1,92 @@ +import 'dart:async'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class LiveCarePermissionService { + LiveCarePermissionService._(); + + static final LiveCarePermissionService instance = LiveCarePermissionService._(); + + // Guard to prevent concurrent permission requests + Completer>? _ongoingRequest; + + /// Requests camera, microphone and notification permissions together and + /// returns the final map of permission statuses. + /// This method does NOT show any dialogs — UI decisions must be done by caller. + Future> requestCameraMicAndNotification(BuildContext context) async { + if (_ongoingRequest != null) { + return _ongoingRequest!.future; + } + + _ongoingRequest = Completer>(); + final completer = _ongoingRequest!; + + final permissions = [ + Permission.camera, + Permission.microphone, + Permission.notification, + ]; + + try { + final statuses = await permissions.request(); + + // Ensure all requested keys exist in the returned map (some platforms may omit) + final result = {}; + for (final p in permissions) { + result[p] = statuses[p] ?? await p.status; + } + + if (!completer.isCompleted) completer.complete(result); + return completer.future; + } catch (e) { + if (!completer.isCompleted) completer.complete({}); + return completer.future; + } finally { + _ongoingRequest = null; + } + } + + /// Show an "open settings" dialog (kept as helper for the UI layer). + Future showOpenSettingsDialog(BuildContext context, {required String title, required String message}) async { + return showCommonBottomSheetWithoutHeight( + title: title, + context, + child: Utils.getWarningWidget( + loadingText: message, + isShowActionButtons: true, + onCancelTap: () { + Navigator.of(context).pop(); + }, + onConfirmTap: () async { + openAppSettings(); + Navigator.of(context).pop(); + }), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () {}, + ); + // return showDialog( + // context: context, + // builder: (c) => ConfirmDialog( + // title: '$title Permission', + // message: message, + // onTap: () { + // openAppSettings(); + // Navigator.of(c).pop(); + // }, + // okTitle: LocaleKeys.ok.tr(context: c), + // ), + // ); + } + + String friendlyName(Permission permission) { + if (permission == Permission.camera) return 'Camera'; + if (permission == Permission.microphone) return 'Microphone'; + if (permission == Permission.notification) return 'Notifications'; + return permission.toString(); + } +} From 70a6d90ce37f5b844990d24af4d5a275ba80b007 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 29 Jan 2026 14:14:52 +0300 Subject: [PATCH 2/2] updates & fixes --- assets/langs/ar-SA.json | 4 +-- assets/langs/en-US.json | 5 +--- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 2 +- lib/extensions/string_extensions.dart | 25 +++++++++++++++++++ lib/main.dart | 2 +- .../appointment_details_page.dart | 7 +++++- .../widgets/appointment_card.dart | 14 ++++++----- .../home/widgets/small_service_card.dart | 8 +++--- .../ancillary_procedures_details_page.dart | 3 ++- .../widgets/ancillary_orders_list.dart | 8 +++--- lib/splashPage.dart | 2 +- 12 files changed, 54 insertions(+), 28 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 178b4c0..d64b9b7 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1526,8 +1526,6 @@ "trackerAndOthers": "المتتبع وأخرى", "bmi": "مؤشر كتلة الجسم", "yesPleaseINeedHelp": "نعم، من فضلك أريد المساعدة", - "noThanksIKnowTheClinic": "لا شكراً، أعرف العيادة" + "noThanksIKnowTheClinic": "لا شكراً، أعرف العيادة", "unableToSendOTP": "تعذر إرسال رمز التحقق لمرة واحدة" - - } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index d6f5228..03b766d 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1517,9 +1517,6 @@ "trackerAndOthers": "Tracker & Others", "bmi": "BMI", "yesPleaseINeedHelp": "Yes, Please I need help", - "noThanksIKnowTheClinic": "No, Thanks. I Know the clinic" + "noThanksIKnowTheClinic": "No, Thanks. I Know the clinic", "unableToSendOTP": "Unable to send Verification OTP" - - - } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index f87de29..c063932 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -182,7 +182,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; + // body['PatientID'] = 4769038; // body['PatientTypeID'] = 1; // // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8319c9d..bec908b 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -680,7 +680,7 @@ const DASHBOARD = 'Services/Patients.svc/REST/PatientDashboard'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 1df051e..30b0f7e 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -62,6 +62,31 @@ extension EmailValidator on String { decorationColor: color ?? AppColors.blackColor), ); + Widget toText9( + {Color? color, + FontWeight? weight, + bool isBold = false, + bool isUnderLine = false, + bool isCenter = false, + int? maxlines, + FontStyle? fontStyle, + TextOverflow? textOverflow, + double letterSpacing = 0}) => + Text( + this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: maxlines, + overflow: textOverflow, + style: TextStyle( + fontSize: 9.f, + fontStyle: fontStyle ?? FontStyle.normal, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: letterSpacing, + decoration: isUnderLine ? TextDecoration.underline : null, + decorationColor: color ?? AppColors.blackColor), + ); + Widget toText11( {Color? color, FontWeight? weight, diff --git a/lib/main.dart b/lib/main.dart index 6010a05..1137261 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -224,7 +224,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Dr. AlHabib', builder: (context, mchild) { - return MediaQuery(data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0)), child: mchild!); + return MediaQuery(data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), child: mchild!); }, showSemanticsDebugger: false, debugShowCheckedModeBanner: false, diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 3f98e6d..46ed33e 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -59,6 +59,8 @@ class _AppointmentDetailsPageState extends State { late LabViewModel labViewModel; late RadiologyViewModel radiologyViewModel; + String mapURL = ""; + @override void initState() { scheduleMicrotask(() async { @@ -212,8 +214,11 @@ class _AppointmentDetailsPageState extends State { borderRadius: BorderRadius.circular(24.r), // Todo: what is this???? Api Key??? 😲 child: Image.network( - "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}", + "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=${(MediaQuery.of(context).size.width * 1.5).toInt()}x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}", fit: BoxFit.contain, + // errorBuilder: (cxt, child, tr) { + // return SizedBox.shrink(); + // }, ), ), Positioned( diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 975308f..aa82b51 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -151,7 +151,9 @@ class AppointmentCard extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), SizedBox(height: 2.h), - "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), + (isFoldable || isTablet) + ? "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor) + : "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), ], ), ).circle(100).toShimmer2(isShow: isLoading), @@ -242,7 +244,7 @@ class AppointmentCard extends StatelessWidget { backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 14.f, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), @@ -267,7 +269,7 @@ class AppointmentCard extends StatelessWidget { backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 14.f, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), @@ -292,7 +294,7 @@ class AppointmentCard extends StatelessWidget { backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), - fontSize: 14.f, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), @@ -365,7 +367,7 @@ class AppointmentCard extends StatelessWidget { backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 14.f, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), @@ -383,7 +385,7 @@ class AppointmentCard extends StatelessWidget { backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, textColor: AppColors.blackColor, - fontSize: 14.f, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), diff --git a/lib/presentation/home/widgets/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index 4685e48..84ef6fa 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -65,11 +65,11 @@ class SmallServiceCard extends StatelessWidget { Utils.buildSvgWithAssets(icon: icon, iconColor: iconColor, width: 32.h, height: 32.h), SizedBox(height: 6.h), if (isFoldable || isTablet) ...[ - title.tr(context: context).toText11(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 2), - subtitle.tr(context: context).toText11(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2), + title.tr(context: context).toText10(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxlines: 1), + subtitle.tr(context: context).toText10(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxlines: 1), ] else ...[ - title.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 2), - subtitle.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2), + title.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 1), + subtitle.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 1), ] ], ), diff --git a/lib/presentation/todo_section/ancillary_procedures_details_page.dart b/lib/presentation/todo_section/ancillary_procedures_details_page.dart index f604673..9e97509 100644 --- a/lib/presentation/todo_section/ancillary_procedures_details_page.dart +++ b/lib/presentation/todo_section/ancillary_procedures_details_page.dart @@ -153,7 +153,7 @@ class _AncillaryOrderDetailsListState extends State { ), ), ), - if (orderData != null) _buildStickyPaymentButton(orderData), + if (orderData != null && !viewModel.isAncillaryDetailsProceduresLoading) _buildStickyPaymentButton(orderData), ], ); }), @@ -164,6 +164,7 @@ class _AncillaryOrderDetailsListState extends State { return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, itemCount: 3, itemBuilder: (context, index) { return AncillaryOrderCard( diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart index 71d2cf0..05eb6c7 100644 --- a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart @@ -215,9 +215,7 @@ class AncillaryOrderCard extends StatelessWidget { ).toShimmer2(isShow: isLoading), ], ), - - SizedBox(height: 12.h), - + SizedBox(height: 6.h), // Action Buttons Row( children: [ @@ -236,7 +234,7 @@ class AncillaryOrderCard extends StatelessWidget { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 14.f, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), @@ -260,7 +258,7 @@ class AncillaryOrderCard extends StatelessWidget { backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), textColor: Color(0xffED1C2B), - fontSize: 14.f, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 33d3d23..49680a2 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -56,7 +56,7 @@ class _SplashScreenState extends State { await notificationService.initialize(onNotificationClick: (payload) { // Handle notification click here }); - await ZoomService().initializeZoomSDK(); + ZoomService().initializeZoomSDK(); if (isAppOpenedFromCall) { navigateToTeleConsult(); } else {