Merge branch 'master' into faiz_dev

pull/304/head
faizatflutter 7 hours ago
commit f3f47dfa4f

@ -175,11 +175,11 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:2.9.0") implementation("androidx.navigation:navigation-ui-ktx:2.9.0")
implementation("androidx.activity:activity-ktx:1.10.1") implementation("androidx.activity:activity-ktx:1.10.1")
// val room_version = "2.6.1" val room_version = "2.6.1"
// implementation("androidx.room:room-runtime:$room_version") implementation("androidx.room:room-runtime:$room_version")
// annotationProcessor("androidx.room:room-compiler:$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.ssp:ssp-android:1.1.0")
implementation("com.intuit.sdp:sdp-android:1.1.0") implementation("com.intuit.sdp:sdp-android:1.1.0")

@ -1841,5 +1841,6 @@
"thisAboveInfoPrescription": "تم تحليل الوصفة الطبية هذه بواسطة الذكاء الاصطناعي، وهي لا تُعدّ نصيحة طبية. استشر طبيبك المختص للتشخيص والعلاج.", "thisAboveInfoPrescription": "تم تحليل الوصفة الطبية هذه بواسطة الذكاء الاصطناعي، وهي لا تُعدّ نصيحة طبية. استشر طبيبك المختص للتشخيص والعلاج.",
"liveCareNotificationPermissionsMessage": "يتطلب لايف كير أذونات الإشعارات، يرجى السماح بهذه الأذونات للمتابعة.", "liveCareNotificationPermissionsMessage": "يتطلب لايف كير أذونات الإشعارات، يرجى السماح بهذه الأذونات للمتابعة.",
"weatherIndicators": "طقس", "weatherIndicators": "طقس",
"submitRating": "إرسال التقييم" "submitRating": "إرسال التقييم",
"completedPrescriptionOrder": "تمت الخدمة"
} }

@ -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.", "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.", "liveCareNotificationPermissionsMessage": "LiveCare requires Notifications permission, Please allow to proceed.",
"weatherIndicators": "Weather", "weatherIndicators": "Weather",
"submitRating": "Submit" "submitRating": "Submit",
"completedPrescriptionOrder": "Completed"
} }

@ -16,11 +16,9 @@ import GoogleMaps
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func initializePlatformChannels(){ func initializePlatformChannels(){
// if let mainViewController = window?.rootViewController as? FlutterViewController{ // platform initialization suppose to be in foreground if let mainViewController = window?.rootViewController as? FlutterViewController{ // platform initialization suppose to be in foreground
// HMGPenguinInPlatformBridge.initialize(flutterViewController: mainViewController)
//// HMGPenguinInPlatformBridge.initialize(flutterViewController: mainViewController) }
//
// }
} }
override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken:Data){ override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken:Data){
// Messaging.messaging().apnsToken = deviceToken // Messaging.messaging().apnsToken = deviceToken

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/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/presentation/home/app_update_page.dart';
import 'package:hmg_patient_app_new/routes/app_routes.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/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/services/navigation_service.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -87,10 +89,14 @@ abstract class ApiClient {
class ApiClientImp implements ApiClient { class ApiClientImp implements ApiClient {
final _analytics = getIt<GAnalytics>(); final _analytics = getIt<GAnalytics>();
final AppState _appState; final AppState _appState;
final HttpClientManager _httpClient;
ApiClientImp({ ApiClientImp({
required AppState appState, required AppState appState,
}) : _appState = appState; HttpClientManager? httpClient,
}) : _appState = appState,
_httpClient = httpClient ??
HttpClientManager(lifecycleService: getIt<AppLifecycleService>());
@override @override
post( post(
@ -242,8 +248,12 @@ class ApiClientImp implements ApiClient {
http.Response response; http.Response response;
try { try {
response = await http.post(Uri.parse(url.trim()), body: requestBody, headers: headers); response = await _httpClient.post(
debugPrint("response: ${response.body}", wrapWidth: 2048); uri: Uri.parse(url.trim()),
body: requestBody,
headers: headers,
);
// debugPrint("response: ${response.body}", wrapWidth: 2048);
} on SocketException catch (e) { } on SocketException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message)); onFailure(message, -1, failureType: ConnectivityFailure(message));
@ -445,8 +455,8 @@ class ApiClientImp implements ApiClient {
if (await Utils.checkConnection(bypassConnectionCheck: true)) { if (await Utils.checkConnection(bypassConnectionCheck: true)) {
http.Response response; http.Response response;
try { try {
response = await http.get( response = await _httpClient.get(
Uri.parse(url.trim()), uri: Uri.parse(url.trim()),
headers: apiHeaders ?? {'Content-Type': 'application/json', 'Accept': 'application/json'}, headers: apiHeaders ?? {'Content-Type': 'application/json', 'Accept': 'application/json'},
); );
} on SocketException catch (e) { } on SocketException catch (e) {

@ -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<http.Response> post({
required Uri uri,
Map<String, String>? 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<http.Response> get({
required Uri uri,
Map<String, String>? 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<http.Response> _executeWithRetry(
Future<http.Response> 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<void> _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<void>();
late StreamSubscription<AppLifecycleState> 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<void> _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();
}
}

@ -283,7 +283,7 @@ class ApiConsts {
static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations";
// ************ static values for Api **************** // ************ static values for Api ****************
static final double appVersionID = 20.9; static final double appVersionID = 21.0;
// static final double appVersionID = 50.7; // static final double appVersionID = 50.7;
static final int appChannelId = 3; static final int appChannelId = 3;

@ -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/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/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/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/cache_service.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -140,6 +141,9 @@ class AppDependencies {
loggerService: getIt(), loggerService: getIt(),
)); ));
// App Lifecycle Service - must be registered before ApiClient
getIt.registerLazySingleton<AppLifecycleService>(() => AppLifecycleService());
getIt.registerLazySingleton<ApiClient>(() => ApiClientImp(appState: getIt())); getIt.registerLazySingleton<ApiClient>(() => ApiClientImp(appState: getIt()));
getIt.registerLazySingleton<LocalAuthService>( getIt.registerLazySingleton<LocalAuthService>(
() => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()), () => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()),

@ -43,8 +43,13 @@ extension ResponsiveExtension on num {
// Enhanced clamping for different device types // Enhanced clamping for different device types
double clamp; double clamp;
if (SizeUtils.deviceType == DeviceType.tablet || _isFoldable) { if (SizeUtils.deviceType == DeviceType.tablet || _isFoldable) {
// More conservative scaling for tablets and foldables if (SizeUtils.deviceType != DeviceType.tablet && _isFoldable) {
clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.6 : 1.4; // 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 { } else {
// Original logic for phones // Original logic for phones
clamp = (aspectRatio > 1.3 || aspectRatio < 0.77) ? 1.6 : 1.2; clamp = (aspectRatio > 1.3 || aspectRatio < 0.77) ? 1.6 : 1.2;

@ -421,14 +421,14 @@ class BookAppointmentsViewModel extends ChangeNotifier {
}); });
} }
} else { } else {
for (var group in doctorsListGrouped) { // for (var group in doctorsListGrouped) {
group.sort((a, b) { // group.sort((a, b) {
var aSlot = a.decimalDoctorRate; // var aSlot = a.decimalDoctorRate;
var bSlot = b.decimalDoctorRate; // var bSlot = b.decimalDoctorRate;
if (aSlot == null || bSlot == null) return 0; // if (aSlot == null || bSlot == null) return 0;
return bSlot.compareTo(aSlot); // return bSlot.compareTo(aSlot);
}); // });
} // }
// doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); // doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!));
} }
@ -677,7 +677,7 @@ class BookAppointmentsViewModel extends ChangeNotifier {
doctorsList = apiResponse.data!; doctorsList = apiResponse.data!;
filteredDoctorList = doctorsList; filteredDoctorList = doctorsList;
isDoctorsListLoading = false; isDoctorsListLoading = false;
doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); // doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!));
initializeFilteredList(); initializeFilteredList();
clearSearchFilters(); clearSearchFilters();
getFiltersFromDoctorList(); getFiltersFromDoctorList();

@ -944,11 +944,11 @@ class HmgServicesRepoImp implements HmgServicesRepo {
final vitalSign = VitalSignResModel.fromJson(vitalSignJson); final vitalSign = VitalSignResModel.fromJson(vitalSignJson);
// Debug logging for blood pressure // Debug logging for blood pressure
print('=== Repository Blood Pressure Check ==='); // print('=== Repository Blood Pressure Check ===');
print('bloodPressureHigher: ${vitalSign.bloodPressureHigher}'); // print('bloodPressureHigher: ${vitalSign.bloodPressureHigher}');
print('bloodPressureLower: ${vitalSign.bloodPressureLower}'); // print('bloodPressureLower: ${vitalSign.bloodPressureLower}');
print('weightKg: ${vitalSign.weightKg}'); // print('weightKg: ${vitalSign.weightKg}');
print('heightCm: ${vitalSign.heightCm}'); // print('heightCm: ${vitalSign.heightCm}');
// Check if the record has at least one valid vital sign measurement // Check if the record has at least one valid vital sign measurement
final hasValidWeight = _isValidValue(vitalSign.weightKg); final hasValidWeight = _isValidValue(vitalSign.weightKg);

@ -1834,5 +1834,6 @@ abstract class LocaleKeys {
static const liveCareNotificationPermissionsMessage = 'liveCareNotificationPermissionsMessage'; static const liveCareNotificationPermissionsMessage = 'liveCareNotificationPermissionsMessage';
static const weatherIndicators = 'weatherIndicators'; static const weatherIndicators = 'weatherIndicators';
static const submitRating = 'submitRating'; static const submitRating = 'submitRating';
static const completedPrescriptionOrder = 'completedPrescriptionOrder';
} }

@ -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/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/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/routes/app_routes.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/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/app_theme.dart'; import 'package:hmg_patient_app_new/theme/app_theme.dart';
@ -88,7 +90,9 @@ Future<void> callAppStateInitializations() async {
appState.setDeviceTypeID = deviceTypeId; appState.setDeviceTypeID = deviceTypeId;
// Pass all uncaught "fatal" errors from the framework to Crashlytics // 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 // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
PlatformDispatcher.instance.onError = (error, stack) { PlatformDispatcher.instance.onError = (error, stack) {
@ -121,6 +125,9 @@ Future<void> callInitializations() async {
HttpOverrides.global = MyHttpOverrides(); HttpOverrides.global = MyHttpOverrides();
await callAppStateInitializations(); await callAppStateInitializations();
// Initialize App Lifecycle Service to monitor background/foreground transitions
getIt.get<AppLifecycleService>().initialize();
// Restore persisted dark-mode preference before the first frame. // Restore persisted dark-mode preference before the first frame.
getIt.get<ProfileSettingsViewModel>().loadDarkMode(); getIt.get<ProfileSettingsViewModel>().loadDarkMode();
} }

@ -251,7 +251,7 @@ class _AppointmentCardState extends State<AppointmentCard> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), SizedBox(height: 2.h),
(isFoldable || isTablet) (isFoldable || isTablet)
? "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate}" ? "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate}"
@ -270,14 +270,13 @@ class _AppointmentCardState extends State<AppointmentCard> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
(widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), (widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1),
Expanded( Expanded(
child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}").toText16( child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}")
isBold: true, .toText16(isBold: true, maxlines: 2,
maxlines: 1, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")),
textOverflow: TextOverflow.ellipsis,
isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")),
), ),
SizedBox(width: 12.w), SizedBox(width: 12.w),
(widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && (widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null &&
@ -295,7 +294,7 @@ class _AppointmentCardState extends State<AppointmentCard> {
Wrap( Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
spacing: 6.h, spacing: 6.h,
runSpacing: 4.h, runSpacing: 6.h,
children: [ children: [
AppCustomChipWidget( AppCustomChipWidget(
labelText: widget.isLoading labelText: widget.isLoading

@ -230,7 +230,7 @@ class _ImmediateLiveCarePendingRequestPageState extends State<ImmediateLiveCareP
workGroup: "Live_Care_Chat", workGroup: "Live_Care_Chat",
onSuccess: (response) { onSuccess: (response) {
debugPrint("Chat Request ID received: ${getIt.get<ContactUsViewModel>().chatRequestID}"); debugPrint("Chat Request ID received: ${getIt.get<ContactUsViewModel>().chatRequestID}");
chatURL = "https://chat.hmg.com/Index.aspx?RequestedId=${getIt.get<ContactUsViewModel>().chatRequestID}"; chatURL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId=${getIt.get<ContactUsViewModel>().chatRequestID}";
debugPrint("Chat URL: $chatURL"); debugPrint("Chat URL: $chatURL");
Uri uri = Uri.parse(chatURL); Uri uri = Uri.parse(chatURL);
launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: "");

@ -24,7 +24,7 @@ class RequestStatus extends StatelessWidget {
case 2: //processing case 2: //processing
return LocaleKeys.underProcessing.tr(); return LocaleKeys.underProcessing.tr();
case 3: case 3:
return LocaleKeys.completed.tr(); return LocaleKeys.completedPrescriptionOrder.tr();
case 4: //cancel case 4: //cancel
case 6: case 6:
case 7: case 7:

@ -539,61 +539,76 @@ class _ServicesPageState extends State<ServicesPage> {
Spacer(), Spacer(),
getIt.get<AppState>().isAuthenticated getIt.get<AppState>().isAuthenticated
? Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) { ? Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Utils.getPaymentAmountWithSymbol2( return Row(
num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), children: [
isExpanded: false, Utils.buildSvgWithAssets(
letterSpacing: -1) icon: AppAssets.saudi_riyal_icon,
.toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); 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), : LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2),
Spacer(), Spacer(),
getIt.get<AppState>().isAuthenticated getIt.get<AppState>().isAuthenticated
? CustomButton( ? CustomButton(
height: 40.h, height: 40.h,
icon: AppAssets.recharge_icon, icon: AppAssets.recharge_icon,
iconSize: 24.w, iconSize: 24.w,
iconColor: AppColors.infoColor, iconColor: AppColors.infoColor,
textColor: AppColors.infoColor, textColor: AppColors.infoColor,
text: LocaleKeys.recharge.tr(), text: LocaleKeys.recharge.tr(),
borderWidth: 0.w, borderWidth: 0.w,
isBold: true, isBold: true,
borderColor: Colors.transparent, borderColor: Colors.transparent,
backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08),
padding: EdgeInsets.all(8.w), padding: EdgeInsets.all(8.w),
fontSize: 14.f, fontSize: 14.f,
onPressed: () { onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage()));
}, },
) )
: SizedBox.shrink(), : SizedBox.shrink(),
], ],
).onPress(() async { ).onPress(() async {
if (getIt.get<AppState>().isAuthenticated) { if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage()));
} else { } else {
await getIt.get<AuthenticationViewModel>().onLoginPressed(); await getIt.get<AuthenticationViewModel>().onLoginPressed();
} }
}), }),
),
), ),
SizedBox(width: 16.w), ),
Expanded( SizedBox(width: 16.w),
child: Container( Expanded(
height: 183.h, child: Container(
width: 183.h, height: 183.h,
padding: EdgeInsets.all(16.w), width: 183.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( padding: EdgeInsets.all(16.w),
color: AppColors.whiteColor, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
borderRadius: 20.r, color: AppColors.whiteColor,
hasShadow: false, borderRadius: 20.r,
), hasShadow: false,
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
spacing: 8.w, Row(
crossAxisAlignment: CrossAxisAlignment.center, spacing: 8.w,
children: [ crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildSvgWithAssets( Utils.buildSvgWithAssets(
icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false),
LocaleKeys.familyTitle.tr().toText16(isBold: true, maxlines: 2).expanded, LocaleKeys.familyTitle.tr().toText16(isBold: true, maxlines: 2).expanded,

@ -448,7 +448,7 @@ class _LandingPageState extends State<LandingPage> {
).paddingSymmetrical(24.h, 0.h) ).paddingSymmetrical(24.h, 0.h)
: isTablet : isTablet
? SizedBox( ? SizedBox(
height: isFoldable ? 290.h : 255.h, height: isTablet ? 290.h : 255.h,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: 3, itemCount: 3,
@ -456,7 +456,7 @@ class _LandingPageState extends State<LandingPage> {
padding: EdgeInsets.only(left: 16.h, right: 16.h), padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return SizedBox( return SizedBox(
height: 255.h, height: isTablet ? 290.h : 255.h,
width: 250.w, width: 250.w,
child: getIndexSwiperCard(index), child: getIndexSwiperCard(index),
); );

@ -108,10 +108,10 @@ class InsuranceApprovalDetailsPage extends StatelessWidget {
richText: Row( richText: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
"${LocaleKeys.receiptOn.tr(context: context)} ".toText10(), "${LocaleKeys.receiptOn.tr(context: context)} ".toText10(isBold: true),
Directionality( Directionality(
textDirection: ui.TextDirection.ltr, 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, isEnglishOnly: true,
@ -121,15 +121,14 @@ class InsuranceApprovalDetailsPage extends StatelessWidget {
richText: Row( richText: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
"${LocaleKeys.expiryOn.tr(context: context)} ".toText10(), "${LocaleKeys.expiryOn.tr(context: context)} ".toText10(isBold: true),
Directionality( Directionality(
textDirection: ui.TextDirection.ltr, 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, isEnglishOnly: true,
), ),
], ],
), ),
], ],

@ -162,10 +162,11 @@ class PatientInsuranceCard extends StatelessWidget {
AppCustomChipWidget( AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon, icon: AppAssets.doctor_calendar_icon,
// labelText: "${LocaleKeys.expiryDate.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceCardDetailsModel.cardValidTo), false)}", // 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), 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), SizedBox(height: 10.h),

@ -20,8 +20,9 @@ class LabResultList extends StatelessWidget {
selector: (_, model) => model.mainLabResultsByHospitals, selector: (_, model) => model.mainLabResultsByHospitals,
builder: (__, list, ___) { builder: (__, list, ___) {
if (list.isEmpty && context.read<LabViewModel>().labSpecialResult.isEmpty) { if (list.isEmpty && context.read<LabViewModel>().labSpecialResult.isEmpty) {
return Utils.getNoDataWidget(context, // return Utils.getNoDataWidget(context,
noDataText: LocaleKeys.noLabResults.tr(context: context)); // noDataText: LocaleKeys.noLabResults.tr(context: context));
return Container();
} else { } else {
return ListView.builder( return ListView.builder(
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),

@ -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<AppLifecycleState>.broadcast();
/// Stream of app lifecycle state changes
Stream<AppLifecycleState> 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');
}
}
Loading…
Cancel
Save