From cfd0c1673841046bd67d6c0ea78ff9eff7cc04bd Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 28 Jan 2026 14:03:20 +0300 Subject: [PATCH] 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 9249e54d..4c8bed13 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 ba8f627a..f87de290 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 f817a898..dc9213f7 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 e1748c3a..975308fc 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 c5139f17..b87a0a36 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 0c4aec14..8583d9bb 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 cf5eef34..fa239797 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 3e48f8bc..d522b454 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 9ae7ee30..73da69f5 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 3dec5aa8..4585b7d0 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 e0c15d0f..a8a64fa6 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 cf49a263..4685e487 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 00000000..a9a5d294 --- /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(); + } +}