diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9dd449c6..c1ccc0b9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -175,11 +175,11 @@ dependencies { implementation("androidx.navigation:navigation-ui-ktx:2.9.0") implementation("androidx.activity:activity-ktx:1.10.1") -// val room_version = "2.6.1" -// implementation("androidx.room:room-runtime:$room_version") -// annotationProcessor("androidx.room:room-compiler:$room_version") + val room_version = "2.6.1" + implementation("androidx.room:room-runtime:$room_version") + annotationProcessor("androidx.room:room-compiler:$room_version") -// implementation("net.zetetic:android-database-sqlcipher:4.5.4") + implementation("net.zetetic:android-database-sqlcipher:4.5.4") implementation("com.intuit.ssp:ssp-android:1.1.0") implementation("com.intuit.sdp:sdp-android:1.1.0") diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index f0e830f7..0a8ec974 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1841,5 +1841,6 @@ "thisAboveInfoPrescription": "تم تحليل الوصفة الطبية هذه بواسطة الذكاء الاصطناعي، وهي لا تُعدّ نصيحة طبية. استشر طبيبك المختص للتشخيص والعلاج.", "liveCareNotificationPermissionsMessage": "يتطلب لايف كير أذونات الإشعارات، يرجى السماح بهذه الأذونات للمتابعة.", "weatherIndicators": "طقس", - "submitRating": "إرسال التقييم" + "submitRating": "إرسال التقييم", + "completedPrescriptionOrder": "تمت الخدمة" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index e17992f9..9b969f79 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1831,7 +1831,8 @@ "thisAboveInfoPrescription": "This prescription was analyzed by AI, and it is not medical advice. Consult your healthcare provider for diagnosis and treatment.", "liveCareNotificationPermissionsMessage": "LiveCare requires Notifications permission, Please allow to proceed.", "weatherIndicators": "Weather", - "submitRating": "Submit" + "submitRating": "Submit", + "completedPrescriptionOrder": "Completed" } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 308891a7..dc11bbed 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -16,11 +16,9 @@ import GoogleMaps return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func initializePlatformChannels(){ -// if let mainViewController = window?.rootViewController as? FlutterViewController{ // platform initialization suppose to be in foreground -// -//// HMGPenguinInPlatformBridge.initialize(flutterViewController: mainViewController) -// -// } + if let mainViewController = window?.rootViewController as? FlutterViewController{ // platform initialization suppose to be in foreground + HMGPenguinInPlatformBridge.initialize(flutterViewController: mainViewController) + } } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken:Data){ // Messaging.messaging().apnsToken = deviceToken diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 5172f77b..64074ff8 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api/http_client_manager.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/dependencies.dart'; @@ -13,6 +14,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/home/app_update_page.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; +import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:http/http.dart' as http; @@ -87,10 +89,14 @@ abstract class ApiClient { class ApiClientImp implements ApiClient { final _analytics = getIt(); final AppState _appState; + final HttpClientManager _httpClient; ApiClientImp({ required AppState appState, - }) : _appState = appState; + HttpClientManager? httpClient, + }) : _appState = appState, + _httpClient = httpClient ?? + HttpClientManager(lifecycleService: getIt()); @override post( @@ -242,8 +248,12 @@ class ApiClientImp implements ApiClient { http.Response response; try { - response = await http.post(Uri.parse(url.trim()), body: requestBody, headers: headers); - debugPrint("response: ${response.body}", wrapWidth: 2048); + response = await _httpClient.post( + uri: Uri.parse(url.trim()), + body: requestBody, + headers: headers, + ); + // debugPrint("response: ${response.body}", wrapWidth: 2048); } on SocketException catch (e) { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1, failureType: ConnectivityFailure(message)); @@ -445,8 +455,8 @@ class ApiClientImp implements ApiClient { if (await Utils.checkConnection(bypassConnectionCheck: true)) { http.Response response; try { - response = await http.get( - Uri.parse(url.trim()), + response = await _httpClient.get( + uri: Uri.parse(url.trim()), headers: apiHeaders ?? {'Content-Type': 'application/json', 'Accept': 'application/json'}, ); } on SocketException catch (e) { diff --git a/lib/core/api/http_client_manager.dart b/lib/core/api/http_client_manager.dart new file mode 100644 index 00000000..d264283d --- /dev/null +++ b/lib/core/api/http_client_manager.dart @@ -0,0 +1,224 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; +import 'package:http/http.dart' as http; + +/// HTTP Client Manager with automatic retry logic for transient network errors +/// Handles background/foreground transitions gracefully +class HttpClientManager { + final http.Client _client; + final AppLifecycleService _lifecycleService; + + static const Duration _defaultTimeout = Duration(seconds: 30); + static const int _maxRetries = 2; + + HttpClientManager({ + required AppLifecycleService lifecycleService, + http.Client? client, + }) : _lifecycleService = lifecycleService, + _client = client ?? http.Client(); + + /// Execute POST request with retry logic + Future post({ + required Uri uri, + Map? headers, + Object? body, + Duration? timeout, + int maxRetries = _maxRetries, + }) async { + return _executeWithRetry( + () => _client + .post(uri, headers: headers, body: body) + .timeout(timeout ?? _defaultTimeout), + endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(), + maxRetries: maxRetries, + ); + } + + /// Execute GET request with retry logic + Future get({ + required Uri uri, + Map? headers, + Duration? timeout, + int maxRetries = _maxRetries, + }) async { + return _executeWithRetry( + () => _client + .get(uri, headers: headers) + .timeout(timeout ?? _defaultTimeout), + endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(), + maxRetries: maxRetries, + ); + } + + /// Core retry logic for HTTP requests + Future _executeWithRetry( + Future Function() request, { + required String endpoint, + int maxRetries = _maxRetries, + }) async { + int attempt = 0; + + while (attempt <= maxRetries) { + try { + // Wait for app to be in foreground before attempting request + await _waitForAppForeground(); + + // Check if app just came from background + if (_lifecycleService.lastResumedTime != null && attempt == 0) { + final timeSinceResume = DateTime.now().difference(_lifecycleService.lastResumedTime!); + if (timeSinceResume.inSeconds < 5) { + debugPrint('🔄 App recently resumed (${timeSinceResume.inSeconds}s ago), adding small delay before request...'); + await Future.delayed(const Duration(milliseconds: 500)); + } + } + + return await request(); + + } on SocketException catch (e) { + if (attempt >= maxRetries) { + // If app is in background, wait for it to come back before throwing + if (_lifecycleService.isAppInBackground) { + debugPrint('⏸️ App in background, waiting to resume before final error for $endpoint'); + await _waitForAppForeground(); + } + rethrow; + } + await _handleRetry( + attempt: attempt++, + errorType: 'SocketException', + endpoint: endpoint, + errorDetails: e.message, + ); + } on http.ClientException catch (e) { + if (attempt >= maxRetries) { + // If app is in background, wait for it to come back before throwing + if (_lifecycleService.isAppInBackground) { + debugPrint('⏸️ App in background, waiting to resume before final error for $endpoint'); + await _waitForAppForeground(); + } + rethrow; + } + await _handleRetry( + attempt: attempt++, + errorType: 'ClientException', + endpoint: endpoint, + errorDetails: e.message, + ); + } on TimeoutException catch (e) { + // For timeout, only retry once + if (attempt >= 1) { + // If app is in background, wait for it to come back before throwing + if (_lifecycleService.isAppInBackground) { + debugPrint('⏸️ App in background, waiting to resume before final error for $endpoint'); + await _waitForAppForeground(); + } + rethrow; + } + await _handleRetry( + attempt: attempt++, + errorType: 'TimeoutException', + endpoint: endpoint, + errorDetails: e.message ?? 'Request timed out', + ); + } + } + + throw Exception('Max retries exceeded for $endpoint'); + } + + /// Wait for app to be in foreground before proceeding + Future _waitForAppForeground() async { + if (!_lifecycleService.isAppInBackground) { + return; // App is already in foreground + } + + debugPrint('⏸️ App is in background, pausing request until app resumes...'); + + // Wait for app to resume with a timeout + final completer = Completer(); + late StreamSubscription subscription; + + // Set up a listener for app state changes + subscription = _lifecycleService.appStateStream.listen((state) { + if (state == AppLifecycleState.resumed) { + if (!completer.isCompleted) { + debugPrint('✅ App resumed, continuing with request'); + completer.complete(); + } + } + }); + + // Also check current state in case it changed + if (_lifecycleService.currentState == AppLifecycleState.resumed) { + if (!completer.isCompleted) { + completer.complete(); + } + } + + try { + // Wait for app to resume with 60 second timeout + await completer.future.timeout( + const Duration(seconds: 60), + onTimeout: () { + debugPrint('⚠️ Timeout waiting for app to resume'); + }, + ); + } finally { + await subscription.cancel(); + } + + // Add small delay after resume to let things stabilize + await Future.delayed(const Duration(milliseconds: 300)); + } + + /// Handle retry delay with exponential backoff and validation + Future _handleRetry({ + required int attempt, + required String errorType, + required String endpoint, + required String errorDetails, + }) async { + // If app went to background during the error, wait for it to come back + if (_lifecycleService.isAppInBackground) { + debugPrint('⏸️ Error occurred while app in background, waiting for app to resume...'); + await _waitForAppForeground(); + } + + // Check network connectivity before retrying + final hasConnection = await Utils.checkConnection(bypassConnectionCheck: true); + if (!hasConnection) { + debugPrint('⚠️ No network connection available, waiting before retry...'); + // Wait a bit and check again instead of immediately throwing + await Future.delayed(const Duration(seconds: 2)); + final recheckConnection = await Utils.checkConnection(bypassConnectionCheck: true); + if (!recheckConnection) { + throw SocketException('No network connection available for retry'); + } + } + + // Exponential backoff: 300ms, 900ms, 2700ms + final delay = Duration(milliseconds: 300 * (1 << attempt)); + + debugPrint( + '🔄 Retry attempt ${attempt + 1}/$_maxRetries for $endpoint\n' + ' Error: $errorType - $errorDetails\n' + ' Waiting: ${delay.inMilliseconds}ms\n' + ' App State: ${_lifecycleService.currentState}' + ); + + await Future.delayed(delay); + } + + /// Close the HTTP client + void dispose() { + _client.close(); + } +} + + + + + diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index db4d6a8c..ab94fecb 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -282,7 +282,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 20.9; + static final double appVersionID = 21.0; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 3b9eefc0..1de302b7 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -68,6 +68,7 @@ import 'package:hmg_patient_app_new/features/weather/weather_repo.dart'; import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; +import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -140,6 +141,9 @@ class AppDependencies { loggerService: getIt(), )); + // App Lifecycle Service - must be registered before ApiClient + getIt.registerLazySingleton(() => AppLifecycleService()); + getIt.registerLazySingleton(() => ApiClientImp(appState: getIt())); getIt.registerLazySingleton( () => LocalAuthService(loggerService: getIt(), localAuth: getIt()), diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index c7f3fa06..7f4013ed 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -43,8 +43,13 @@ extension ResponsiveExtension on num { // Enhanced clamping for different device types double clamp; if (SizeUtils.deviceType == DeviceType.tablet || _isFoldable) { - // More conservative scaling for tablets and foldables - clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.6 : 1.4; + if (SizeUtils.deviceType != DeviceType.tablet && _isFoldable) { + // clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.4 : 1.4; + clamp = 1.1; + } else { + // More conservative scaling for tablets and foldables + clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.6 : 1.4; + } } else { // Original logic for phones clamp = (aspectRatio > 1.3 || aspectRatio < 0.77) ? 1.6 : 1.2; diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 7ac6ae45..1d15fdd2 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -421,14 +421,14 @@ class BookAppointmentsViewModel extends ChangeNotifier { }); } } else { - for (var group in doctorsListGrouped) { - group.sort((a, b) { - var aSlot = a.decimalDoctorRate; - var bSlot = b.decimalDoctorRate; - if (aSlot == null || bSlot == null) return 0; - return bSlot.compareTo(aSlot); - }); - } + // for (var group in doctorsListGrouped) { + // group.sort((a, b) { + // var aSlot = a.decimalDoctorRate; + // var bSlot = b.decimalDoctorRate; + // if (aSlot == null || bSlot == null) return 0; + // return bSlot.compareTo(aSlot); + // }); + // } // doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); } @@ -677,7 +677,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { doctorsList = apiResponse.data!; filteredDoctorList = doctorsList; isDoctorsListLoading = false; - doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); + // doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); initializeFilteredList(); clearSearchFilters(); getFiltersFromDoctorList(); diff --git a/lib/features/hmg_services/hmg_services_repo.dart b/lib/features/hmg_services/hmg_services_repo.dart index b70e024c..1484c91f 100644 --- a/lib/features/hmg_services/hmg_services_repo.dart +++ b/lib/features/hmg_services/hmg_services_repo.dart @@ -944,11 +944,11 @@ class HmgServicesRepoImp implements HmgServicesRepo { final vitalSign = VitalSignResModel.fromJson(vitalSignJson); // Debug logging for blood pressure - print('=== Repository Blood Pressure Check ==='); - print('bloodPressureHigher: ${vitalSign.bloodPressureHigher}'); - print('bloodPressureLower: ${vitalSign.bloodPressureLower}'); - print('weightKg: ${vitalSign.weightKg}'); - print('heightCm: ${vitalSign.heightCm}'); + // print('=== Repository Blood Pressure Check ==='); + // print('bloodPressureHigher: ${vitalSign.bloodPressureHigher}'); + // print('bloodPressureLower: ${vitalSign.bloodPressureLower}'); + // print('weightKg: ${vitalSign.weightKg}'); + // print('heightCm: ${vitalSign.heightCm}'); // Check if the record has at least one valid vital sign measurement final hasValidWeight = _isValidValue(vitalSign.weightKg); diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index d5410585..5ef765b6 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1834,5 +1834,6 @@ abstract class LocaleKeys { static const liveCareNotificationPermissionsMessage = 'liveCareNotificationPermissionsMessage'; static const weatherIndicators = 'weatherIndicators'; static const submitRating = 'submitRating'; + static const completedPrescriptionOrder = 'completedPrescriptionOrder'; } diff --git a/lib/main.dart b/lib/main.dart index 23f7c4d7..5999a04e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,6 +49,8 @@ import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_mo import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; +import 'package:hmg_patient_app_new/services/analytics/analytics_service.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/theme/app_theme.dart'; @@ -88,7 +90,9 @@ Future callAppStateInitializations() async { appState.setDeviceTypeID = deviceTypeId; // Pass all uncaught "fatal" errors from the framework to Crashlytics - FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; + if (!kDebugMode) { + FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; + } // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics PlatformDispatcher.instance.onError = (error, stack) { @@ -121,6 +125,9 @@ Future callInitializations() async { HttpOverrides.global = MyHttpOverrides(); await callAppStateInitializations(); + // Initialize App Lifecycle Service to monitor background/foreground transitions + getIt.get().initialize(); + // Restore persisted dark-mode preference before the first frame. getIt.get().loadDarkMode(); } diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 98bc48d6..9b01ce8b 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -251,7 +251,7 @@ class _AppointmentCardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow), + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 14.h, height: 14.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), (isFoldable || isTablet) ? "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate}" @@ -270,14 +270,13 @@ class _AppointmentCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ (widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), Expanded( - child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}").toText16( - isBold: true, - maxlines: 1, - textOverflow: TextOverflow.ellipsis, - isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), + child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}") + .toText16(isBold: true, maxlines: 2, + textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), ), SizedBox(width: 12.w), (widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && @@ -295,7 +294,7 @@ class _AppointmentCardState extends State { Wrap( direction: Axis.horizontal, spacing: 6.h, - runSpacing: 4.h, + runSpacing: 6.h, children: [ AppCustomChipWidget( labelText: widget.isLoading diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart index 1c4f63e8..b8e58fa6 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart @@ -230,7 +230,7 @@ class _ImmediateLiveCarePendingRequestPageState extends State().chatRequestID}"); - chatURL = "https://chat.hmg.com/Index.aspx?RequestedId=${getIt.get().chatRequestID}"; + chatURL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId=${getIt.get().chatRequestID}"; debugPrint("Chat URL: $chatURL"); Uri uri = Uri.parse(chatURL); launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); diff --git a/lib/presentation/emergency_services/history/widget/RequestStatus.dart b/lib/presentation/emergency_services/history/widget/RequestStatus.dart index 849565a7..21a4312b 100644 --- a/lib/presentation/emergency_services/history/widget/RequestStatus.dart +++ b/lib/presentation/emergency_services/history/widget/RequestStatus.dart @@ -24,7 +24,7 @@ class RequestStatus extends StatelessWidget { case 2: //processing return LocaleKeys.underProcessing.tr(); case 3: - return LocaleKeys.completed.tr(); + return LocaleKeys.completedPrescriptionOrder.tr(); case 4: //cancel case 6: case 7: diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index d75c83e7..a2c066f0 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -539,61 +539,76 @@ class _ServicesPageState extends State { Spacer(), getIt.get().isAuthenticated ? Consumer(builder: (context, habibWalletVM, child) { - return Utils.getPaymentAmountWithSymbol2( - num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), - isExpanded: false, - letterSpacing: -1) - .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); + return Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.saudi_riyal_icon, + iconColor: AppColors.inputLabelTextColor, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.h), + NumberFormat.decimalPattern() + .format(habibWalletVM.habibWalletAmount) + .toString() + .toText28(isBold: true, isEnglishOnly: true) + .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 40.h), + ], + ); + // Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), + // isExpanded: false, letterSpacing: -1) + // .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); }) - : LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2), - Spacer(), - getIt.get().isAuthenticated - ? CustomButton( - height: 40.h, - icon: AppAssets.recharge_icon, - iconSize: 24.w, - iconColor: AppColors.infoColor, - textColor: AppColors.infoColor, - text: LocaleKeys.recharge.tr(), - borderWidth: 0.w, - isBold: true, - borderColor: Colors.transparent, - backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 14.f, - onPressed: () { - Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); - }, - ) - : SizedBox.shrink(), - ], - ).onPress(() async { - if (getIt.get().isAuthenticated) { - Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); - } else { - await getIt.get().onLoginPressed(); - } - }), - ), + : LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2), + Spacer(), + getIt.get().isAuthenticated + ? CustomButton( + height: 40.h, + icon: AppAssets.recharge_icon, + iconSize: 24.w, + iconColor: AppColors.infoColor, + textColor: AppColors.infoColor, + text: LocaleKeys.recharge.tr(), + borderWidth: 0.w, + isBold: true, + borderColor: Colors.transparent, + backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 14.f, + onPressed: () { + Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); + }, + ) + : SizedBox.shrink(), + ], + ).onPress(() async { + if (getIt.get().isAuthenticated) { + Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); + } else { + await getIt.get().onLoginPressed(); + } + }), ), - SizedBox(width: 16.w), - Expanded( - child: Container( - height: 183.h, - width: 183.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + ), + SizedBox(width: 16.w), + Expanded( + child: Container( + height: 183.h, + width: 183.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ Utils.buildSvgWithAssets( icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), LocaleKeys.familyTitle.tr().toText16(isBold: true, maxlines: 2).expanded, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 07e60238..4346d8b5 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -448,7 +448,7 @@ class _LandingPageState extends State { ).paddingSymmetrical(24.h, 0.h) : isTablet ? SizedBox( - height: isFoldable ? 290.h : 255.h, + height: isTablet ? 290.h : 255.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 3, @@ -456,7 +456,7 @@ class _LandingPageState extends State { padding: EdgeInsets.only(left: 16.h, right: 16.h), itemBuilder: (context, index) { return SizedBox( - height: 255.h, + height: isTablet ? 290.h : 255.h, width: 250.w, child: getIndexSwiperCard(index), ); diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 6d8e2524..29663938 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -108,10 +108,10 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { richText: Row( mainAxisSize: MainAxisSize.min, children: [ - "${LocaleKeys.receiptOn.tr(context: context)} ".toText10(), + "${LocaleKeys.receiptOn.tr(context: context)} ".toText10(isBold: true), Directionality( textDirection: ui.TextDirection.ltr, - child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.receiptOn), false).toText10(isEnglishOnly: true)), + child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.receiptOn), false).toText10(isBold: true)), ], ), isEnglishOnly: true, @@ -121,15 +121,14 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { richText: Row( mainAxisSize: MainAxisSize.min, children: [ - "${LocaleKeys.expiryOn.tr(context: context)} ".toText10(), + "${LocaleKeys.expiryOn.tr(context: context)} ".toText10(isBold: true), Directionality( textDirection: ui.TextDirection.ltr, - child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.expiryDate), false).toText10(isEnglishOnly: true)), + child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.expiryDate), false).toText10(isBold: true)), ], ), isEnglishOnly: true, ), - ], ), ], diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 3734bd95..6fab990c 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -162,10 +162,11 @@ class PatientInsuranceCard extends StatelessWidget { AppCustomChipWidget( icon: AppAssets.doctor_calendar_icon, // labelText: "${LocaleKeys.expiryDate.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceCardDetailsModel.cardValidTo), false)}", - richText: "${LocaleKeys.expiryDate.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceCardDetailsModel.cardValidTo), false)}".toText10(isEnglishOnly: true), + richText: + "${LocaleKeys.expiryDate.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceCardDetailsModel.cardValidTo), false)}".toText10(isBold: true), labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: 8.h), ), - AppCustomChipWidget(richText: LocaleKeys.patientCardID.tr(namedArgs: {'id': insuranceCardDetailsModel.patientCardID ?? ''}, context: context).toText10(isEnglishOnly: true)), + AppCustomChipWidget(richText: LocaleKeys.patientCardID.tr(namedArgs: {'id': insuranceCardDetailsModel.patientCardID ?? ''}, context: context).toText10(isBold: true)), ], ), SizedBox(height: 10.h), diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 7bb0cf32..8aedd8a5 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -20,8 +20,9 @@ class LabResultList extends StatelessWidget { selector: (_, model) => model.mainLabResultsByHospitals, builder: (__, list, ___) { if (list.isEmpty && context.read().labSpecialResult.isEmpty) { - return Utils.getNoDataWidget(context, - noDataText: LocaleKeys.noLabResults.tr(context: context)); + // return Utils.getNoDataWidget(context, + // noDataText: LocaleKeys.noLabResults.tr(context: context)); + return Container(); } else { return ListView.builder( physics: NeverScrollableScrollPhysics(), diff --git a/lib/services/app_lifecycle_service.dart b/lib/services/app_lifecycle_service.dart new file mode 100644 index 00000000..4a0dd947 --- /dev/null +++ b/lib/services/app_lifecycle_service.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; + +/// Service to monitor and track app lifecycle state changes +/// This helps detect when app goes to background/foreground +class AppLifecycleService with WidgetsBindingObserver { + final _stateController = StreamController.broadcast(); + + /// Stream of app lifecycle state changes + Stream get appStateStream => _stateController.stream; + + AppLifecycleState _currentState = AppLifecycleState.resumed; + + /// Current app lifecycle state + AppLifecycleState get currentState => _currentState; + + /// Check if app is currently active/visible + bool get isAppActive => _currentState == AppLifecycleState.resumed; + + /// Check if app is in background or inactive + bool get isAppInBackground => + _currentState == AppLifecycleState.paused || + _currentState == AppLifecycleState.inactive || + _currentState == AppLifecycleState.detached; + + /// Last time app was resumed + DateTime? _lastResumedTime; + DateTime? get lastResumedTime => _lastResumedTime; + + /// Last time app went to background + DateTime? _lastPausedTime; + DateTime? get lastPausedTime => _lastPausedTime; + + /// Duration app has been in current state + Duration get timeInCurrentState { + final referenceTime = _currentState == AppLifecycleState.resumed + ? _lastResumedTime + : _lastPausedTime; + + if (referenceTime == null) return Duration.zero; + return DateTime.now().difference(referenceTime); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _currentState = state; + + // Track timing + if (state == AppLifecycleState.resumed) { + _lastResumedTime = DateTime.now(); + debugPrint('📱 App RESUMED'); + } else if (state == AppLifecycleState.paused) { + _lastPausedTime = DateTime.now(); + debugPrint('📱 App PAUSED/BACKGROUNDED'); + } else if (state == AppLifecycleState.inactive) { + debugPrint('📱 App INACTIVE'); + } else if (state == AppLifecycleState.detached) { + debugPrint('📱 App DETACHED'); + } + + _stateController.add(state); + } + + /// Initialize the lifecycle observer + void initialize() { + WidgetsBinding.instance.addObserver(this); + debugPrint('📱 AppLifecycleService initialized'); + } + + /// Clean up resources + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _stateController.close(); + debugPrint('📱 AppLifecycleService disposed'); + } +} + +