diff --git a/assets/images/svg/doctor_profile_schedule_icon.svg b/assets/images/svg/doctor_profile_schedule_icon.svg
new file mode 100644
index 00000000..20e36299
--- /dev/null
+++ b/assets/images/svg/doctor_profile_schedule_icon.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json
index d39e8bd7..b69827d4 100644
--- a/assets/langs/ar-SA.json
+++ b/assets/langs/ar-SA.json
@@ -1958,5 +1958,8 @@
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
- "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:"
+ "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
+ "livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
+ "livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟",
+ "schedule": "جدول"
}
diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json
index 61a6da03..bb5b5fe4 100644
--- a/assets/langs/en-US.json
+++ b/assets/langs/en-US.json
@@ -1947,7 +1947,10 @@
"amountOnly": "Amount",
"invoiceHistory": "Invoice History",
"parkingQR": "Parking QR Code",
- "parkingQRDesc": "Scan the below QR Code at the exit parking gate:"
+ "parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
+ "livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.",
+ "livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?",
+ "schedule": "Schedule"
}
diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart
index d2ea81f9..6a44cb0e 100644
--- a/lib/core/api/api_client.dart
+++ b/lib/core/api/api_client.dart
@@ -208,10 +208,10 @@ class ApiClientImp implements ApiClient {
if (url == 'https://uat.hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo') {
url = "https://hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo";
body['TokenID'] = "@dm!n";
- }
+ }
// body['TokenID'] = "@dm!n";
- // body['PatientID'] = 4772993;
+ // body['PatientID'] = 4769748;
// body['PatientID'] = 89754444;
// body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0;
@@ -266,29 +266,77 @@ class ApiClientImp implements ApiClient {
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "SocketException: $e");
+ // Enhanced Clarity logging for network errors
+ Clarity.setCustomTag('api_err_type', 'socket_exception');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
- // Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
- Clarity.sendCustomEvent('api_failure');
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'POST');
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_socket_exception');
return;
} on http.ClientException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "ClientException: $e");
+
+ // Enhanced Clarity logging
+ Clarity.setCustomTag('api_err_type', 'client_exception');
+ Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'POST');
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_client_exception');
+
return;
} on TimeoutException catch (_) {
final message = LocaleKeys.networkTimeout.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "TimeoutException");
+ // Enhanced Clarity logging for timeouts
+ Clarity.setCustomTag('api_err_type', 'timeout');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
- // Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
- Clarity.sendCustomEvent('api_timeout_failure');
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'POST');
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_timeout');
+
return;
} catch (e) {
final message = LocaleKeys.networkUnknownError.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "UnknownException: $e");
+
+ // Enhanced Clarity logging for unknown errors
+ Clarity.setCustomTag('api_err_type', 'unknown_exception');
+ Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'POST');
+
+ // Truncate exception message for Clarity
+ final exceptionMsg = e.toString();
+ final truncatedMsg = exceptionMsg.length > 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg;
+ Clarity.setCustomTag('api_err_exception', truncatedMsg);
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_unknown_exception');
+
return;
}
@@ -489,21 +537,74 @@ class ApiClientImp implements ApiClient {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "SocketException: $e");
+
+ // Clarity logging for GET requests
+ Clarity.setCustomTag('api_err_type', 'socket_exception');
+ Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'GET');
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_get_socket_exception');
+
return;
} on http.ClientException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "ClientException: $e");
+
+ Clarity.setCustomTag('api_err_type', 'client_exception');
+ Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'GET');
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_get_client_exception');
+
return;
} on TimeoutException catch (_) {
final message = LocaleKeys.networkTimeout.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "TimeoutException");
+
+ Clarity.setCustomTag('api_err_type', 'timeout');
+ Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'GET');
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_get_timeout');
+
return;
} catch (e) {
final message = LocaleKeys.networkUnknownError.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "UnknownException: $e");
+
+ Clarity.setCustomTag('api_err_type', 'unknown_exception');
+ Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'GET');
+
+ final exceptionMsg = e.toString();
+ final truncatedMsg = exceptionMsg.length > 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg;
+ Clarity.setCustomTag('api_err_exception', truncatedMsg);
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_get_unknown_exception');
+
return;
}
@@ -513,6 +614,19 @@ class ApiClientImp implements ApiClient {
if (statusCode < 200 || statusCode >= 400) {
onFailure(LocaleKeys.networkErrorWhileFetching.tr(), statusCode);
logApiEndpointError(endPoint, LocaleKeys.networkErrorWhileFetching.tr(), statusCode);
+
+ // Clarity logging for GET status code errors
+ Clarity.setCustomTag('api_err_type', 'status_code_error');
+ Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
+ Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
+ Clarity.setCustomTag('api_err_method', 'GET');
+ Clarity.setCustomTag('api_err_status', statusCode.toString());
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('api_get_status_error');
} else {
var parsed = json.decode(utf8.decode(response.bodyBytes));
onSuccess(parsed, statusCode);
@@ -695,6 +809,15 @@ class ApiClientImp implements ApiClient {
return '...${url.substring(url.length - (clarityLimit - 3))}';
}
+ String _getEndpointName(String endpoint) {
+ // Extract the last part of the endpoint path for cleaner tracking
+ const int clarityLimit = 100;
+ final cleanEndpoint = endpoint.split('/').last;
+
+ if (cleanEndpoint.length <= clarityLimit) return cleanEndpoint;
+ return '...${cleanEndpoint.substring(cleanEndpoint.length - (clarityLimit - 3))}';
+ }
+
String _sanitizeRequestBody(String body) {
if (body.isEmpty) return '';
diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart
index aa51a5da..664432fd 100644
--- a/lib/core/api_consts.dart
+++ b/lib/core/api_consts.dart
@@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
static const maxSmallScreen = 660;
- static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
+ static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT
@@ -622,6 +622,8 @@ var GET_ER_APPOINTMENT_TIME = 'Services/ER_VirtualCall.svc/REST/GetRestTime';
var ADD_NEW_CALL_FOR_PATIENT_ER = 'Services/DoctorApplication.svc/REST/NewCallForPatientER';
+var ADD_LIVECARE_INSURANCE_UPDATE_REQUEST = 'Services/ER_VirtualCall.svc/REST/ER_InsertRequests';
+
var GET_LIVECARE_HISTORY = 'Services/ER_VirtualCall.svc/REST/GetPatientErVirtualHistory';
var CANCEL_LIVECARE_REQUEST = 'Services/ER_VirtualCall.svc/REST/DeleteErRequest';
var SEND_LIVECARE_INVOICE_EMAIL = 'Services/Notifications.svc/REST/SendInvoiceForLiveCare';
@@ -986,7 +988,10 @@ var IS_DOCTOR_AVAILABLE_BY_CALENDAR_SCHEDULE = 'Services/OUTPs.svc/REST/HIS_IsDo
//PAYFORT
var getPayFortProjectDetails = "Services/PayFort_Serv.svc/REST/GetPayFortProjectDetails";
-var addPayFortApplePayResponse = "Services/PayFort_Serv.svc/REST/AddResponse";
+
+var ADD_PAYFORT_REQUEST = "Services/PayFort_Serv.svc/REST/AddRequest";
+var ADD_PAYFORT_RESPONSE = "Services/PayFort_Serv.svc/REST/AddResponse";
+
// var payFortEnvironment = FortEnvironment.production;
// var applePayMerchantId = "merchant.com.hmgwebservices";
// var payFortEnvironment = FortEnvironment.test;
diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart
index 2e27d633..aba22eb3 100644
--- a/lib/core/app_assets.dart
+++ b/lib/core/app_assets.dart
@@ -193,6 +193,7 @@ class AppAssets {
static const String ic_rrt_vehicle = '$svgBasePath/ic_rrt_vehicle.svg';
static const String doctor_profile_rating_icon = '$svgBasePath/doctor_profile_rating_icon.svg';
static const String doctor_profile_reviews_icon = '$svgBasePath/doctor_profile_reviews_icon.svg';
+ static const String doctor_profile_schedule_icon = '$svgBasePath/doctor_profile_schedule_icon.svg';
static const String bookmark_icon = '$svgBasePath/bookmark_icon.svg';
static const String bookmark_filled_icon = '$svgBasePath/bookmark_filled_icon.svg';
static const String waiting_appointment_icon = '$svgBasePath/waitingAppo.svg';
diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart
index 3c6e923e..c4f7402c 100644
--- a/lib/core/dependencies.dart
+++ b/lib/core/dependencies.dart
@@ -75,6 +75,7 @@ import 'package:hmg_patient_app_new/features/weather/weather_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/clarity_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/firebase_service.dart';
@@ -366,5 +367,9 @@ class AppDependencies {
));
getIt.registerLazySingleton(() => PayTabsViewModel(appState: getIt()));
+
+ getIt.registerLazySingleton(
+ () => ClarityService(appState: getIt.get()),
+ );
}
}
diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart
index 3bee29de..cc87bb7b 100644
--- a/lib/features/habib_wallet/habib_wallet_view_model.dart
+++ b/lib/features/habib_wallet/habib_wallet_view_model.dart
@@ -335,7 +335,6 @@ class HabibWalletViewModel extends ChangeNotifier {
walletRechargeAmount = 0;
selectedRechargeType = 1;
advancePaymentHospitals.clear();
- habibWalletBalanceList.clear();
selectedHospital = null;
fileNumber = '';
depositorName = '';
@@ -417,6 +416,7 @@ class HabibWalletViewModel extends ChangeNotifier {
if (!isWalletAmountToBeLoaded) {
return;
}
+ habibWalletBalanceList.clear();
isWalletAmountLoading = true;
notifyListeners();
diff --git a/lib/features/immediate_livecare/immediate_livecare_repo.dart b/lib/features/immediate_livecare/immediate_livecare_repo.dart
index 8775378b..6726a09b 100644
--- a/lib/features/immediate_livecare/immediate_livecare_repo.dart
+++ b/lib/features/immediate_livecare/immediate_livecare_repo.dart
@@ -25,6 +25,9 @@ abstract class ImmediateLiveCareRepo {
{Function(dynamic)? onSuccess, Function(String)? onError});
Future>>> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError});
+
+ Future>> addInsuranceUpdateRequestForLiveCare(String name, String mobileNumber, String fileNumber,
+ {Function(dynamic)? onSuccess, Function(String)? onError});
}
class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo {
@@ -250,4 +253,44 @@ class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo {
return Left(UnknownFailure(e.toString()));
}
}
+
+ @override
+ Future> addInsuranceUpdateRequestForLiveCare(String name, String mobileNumber, String fileNumber,
+ {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async {
+ Map mapDevice = {
+ "PatientName": name,
+ "MobileNumber": mobileNumber,
+ "FileNumber": fileNumber,
+ };
+
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ ADD_LIVECARE_INSURANCE_UPDATE_REQUEST,
+ body: mapDevice,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ // onError!(error);
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: response,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
}
diff --git a/lib/features/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart
index 68246f00..84a79c02 100644
--- a/lib/features/immediate_livecare/immediate_livecare_view_model.dart
+++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart
@@ -245,6 +245,28 @@ class ImmediateLiveCareViewModel extends ChangeNotifier {
);
}
+ Future addInsuranceUpdateRequestForLiveCare({Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ final result = await immediateLiveCareRepo.addInsuranceUpdateRequestForLiveCare("${_appState.getAuthenticatedUser()?.firstName ?? ''} ${_appState.getAuthenticatedUser()?.lastName ?? ''}",
+ _appState.getAuthenticatedUser()!.mobileNumber ?? "", _appState.getAuthenticatedUser()!.patientId.toString() ?? "");
+
+ result.fold(
+ (failure) async {
+ onError!(failure.message);
+ },
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ onError!(apiResponse.errorMessage ?? "Unknown error occurred");
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
Future getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
patientLiveCareHistoryList = [];
patientHasPendingLiveCareRequest = false;
diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart
index 0862c40d..9182f9e7 100644
--- a/lib/features/my_appointments/my_appointments_view_model.dart
+++ b/lib/features/my_appointments/my_appointments_view_model.dart
@@ -588,7 +588,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
final result = await myAppointmentsRepo.confirmAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel);
result.fold(
- (failure) async => await errorHandlerService.handleError(failure: failure),
+ // (failure) async => await errorHandlerService.handleError(failure: failure),
+ (failure) async {
+ if(onError != null) {
+ onError(failure.message);
+ }
+ },
(apiResponse) {
if (apiResponse.messageStatus == 2) {
onError!(apiResponse.errorMessage!);
diff --git a/lib/features/payfort/models/payfort_request_insert_model.dart b/lib/features/payfort/models/payfort_request_insert_model.dart
new file mode 100644
index 00000000..56f354f5
--- /dev/null
+++ b/lib/features/payfort/models/payfort_request_insert_model.dart
@@ -0,0 +1,128 @@
+class PayfortRequestInsertModel {
+ String? accessCode;
+ String? commandType;
+ num? amount;
+ String? customerEmail;
+ String? orderDescription;
+ String? serviceID;
+ String? returnURL;
+ String? paymentOption;
+ String? clientRequestID;
+ String? projectID;
+ String? fileNumber;
+ String? customerName;
+ String? currency;
+ String? liveServiceID;
+ String? longitude;
+ String? latitude;
+ String? merchantReference;
+ String? merchantIdentifier;
+ String? deviceToken;
+ String? sHARequestPhase;
+ String? sHAResponsePhase;
+ String? signature;
+ String? language;
+ String? integrationId;
+ String? iPAdress;
+ String? isSchedule;
+ String? appointmentDate;
+ String? appointmentNo;
+ String? doctorID;
+
+ PayfortRequestInsertModel(
+ {this.accessCode,
+ this.commandType,
+ this.amount,
+ this.customerEmail,
+ this.orderDescription,
+ this.serviceID,
+ this.returnURL,
+ this.paymentOption,
+ this.clientRequestID,
+ this.projectID,
+ this.fileNumber,
+ this.customerName,
+ this.currency,
+ this.liveServiceID,
+ this.longitude,
+ this.latitude,
+ this.merchantReference,
+ this.merchantIdentifier,
+ this.deviceToken,
+ this.sHARequestPhase,
+ this.sHAResponsePhase,
+ this.signature,
+ this.language,
+ this.integrationId,
+ this.iPAdress,
+ this.isSchedule,
+ this.appointmentDate,
+ this.appointmentNo,
+ this.doctorID});
+
+ PayfortRequestInsertModel.fromJson(Map json) {
+ accessCode = json['Access_code'];
+ commandType = json['CommandType'];
+ amount = json['Amount'];
+ customerEmail = json['CustomerEmail'];
+ orderDescription = json['OrderDescription'];
+ serviceID = json['Service_ID'];
+ returnURL = json['ReturnURL'];
+ paymentOption = json['PaymentOption'];
+ clientRequestID = json['ClientRequestID'];
+ projectID = json['ProjectID'];
+ fileNumber = json['FileNumber'];
+ customerName = json['CustomerName'];
+ currency = json['Currency'];
+ liveServiceID = json['Live_ServiceID'];
+ longitude = json['Longitude'];
+ latitude = json['Latitude'];
+ merchantReference = json['Merchant_Reference'];
+ merchantIdentifier = json['Merchant_Identifier'];
+ deviceToken = json['DeviceToken'];
+ sHARequestPhase = json['SHA_RequestPhase'];
+ sHAResponsePhase = json['SHA_ResponsePhase'];
+ signature = json['Signature'];
+ language = json['Language'];
+ integrationId = json['Integration_Id'];
+ iPAdress = json['IPAdress'];
+ isSchedule = json['IsSchedule'];
+ appointmentDate = json['AppointmentDate'];
+ appointmentNo = json['AppointmentNo'];
+ doctorID = json['DoctorID'];
+ }
+
+ Map toJson() {
+ final Map data = new Map();
+ data['Access_code'] = this.accessCode;
+ data['CommandType'] = this.commandType;
+ data['Amount'] = this.amount;
+ data['CustomerEmail'] = this.customerEmail;
+ data['OrderDescription'] = this.orderDescription;
+ data['Service_ID'] = this.serviceID;
+ data['ReturnURL'] = this.returnURL;
+ data['PaymentOption'] = this.paymentOption;
+ data['ClientRequestID'] = this.clientRequestID;
+ data['ProjectID'] = this.projectID;
+ data['FileNumber'] = this.fileNumber;
+ data['CustomerName'] = this.customerName;
+ data['Currency'] = this.currency;
+ data['Live_ServiceID'] = this.liveServiceID;
+ data['Longitude'] = this.longitude;
+ data['Latitude'] = this.latitude;
+ data['Merchant_Reference'] = this.merchantReference;
+ data['Merchant_Identifier'] = this.merchantIdentifier;
+ data['DeviceToken'] = this.deviceToken;
+ data['SHA_RequestPhase'] = this.sHARequestPhase;
+ data['SHA_ResponsePhase'] = this.sHAResponsePhase;
+ data['Signature'] = this.signature;
+ data['Language'] = this.language;
+ data['Integration_Id'] = this.integrationId;
+ data['IPAdress'] = this.iPAdress;
+ data['IsSchedule'] = this.isSchedule;
+ data['AppointmentDate'] = this.appointmentDate;
+ data['AppointmentNo'] = this.appointmentNo;
+ data['DoctorID'] = this.doctorID;
+ return data;
+ }
+}
diff --git a/lib/features/payfort/models/payfort_response_insert_model.dart b/lib/features/payfort/models/payfort_response_insert_model.dart
new file mode 100644
index 00000000..452ca2a5
--- /dev/null
+++ b/lib/features/payfort/models/payfort_response_insert_model.dart
@@ -0,0 +1,77 @@
+class PayfortResponseInsertModel {
+ String? fortId;
+ String? commandType;
+ num? amount;
+ String? sadadOLP;
+ String? paymentOption;
+ String? customerIP;
+ String? eCI;
+ String? responseMessage;
+ String? cardNumber;
+ String? cardBin;
+ int? installmentsNumber;
+ String? status;
+ String? merchantRef;
+ bool? isRefund;
+ bool? remmeberMe;
+ String? reconciliationReference;
+
+ PayfortResponseInsertModel({
+ this.fortId,
+ this.commandType,
+ this.amount,
+ this.sadadOLP,
+ this.paymentOption,
+ this.customerIP,
+ this.eCI,
+ this.responseMessage,
+ this.cardNumber,
+ this.cardBin,
+ this.installmentsNumber,
+ this.status,
+ this.merchantRef,
+ this.isRefund,
+ this.remmeberMe,
+ this.reconciliationReference,
+ });
+
+ PayfortResponseInsertModel.fromJson(Map json) {
+ fortId = json['Fort_id'];
+ commandType = json['CommandType'];
+ amount = json['Amount'];
+ sadadOLP = json['Sadad_OLP'];
+ paymentOption = json['Payment_Option'];
+ customerIP = json['Customer_IP'];
+ eCI = json['ECI'];
+ responseMessage = json['Response_Message'];
+ cardNumber = json['Card_Number'];
+ cardBin = json['Card_Bin'];
+ installmentsNumber = json['Installments_Number'];
+ status = json['Status'];
+ merchantRef = json['Merchant_Ref'];
+ isRefund = json['IsRefund'];
+ remmeberMe = json['RemmeberMe'];
+ reconciliationReference = json['Reconciliation_Reference'];
+ }
+
+ Map toJson() {
+ final Map data = Map();
+ data['Fort_id'] = this.fortId;
+ data['CommandType'] = this.commandType;
+ data['Amount'] = this.amount;
+ data['Sadad_OLP'] = this.sadadOLP;
+ data['Payment_Option'] = this.paymentOption;
+ data['Customer_IP'] = this.customerIP;
+ data['ECI'] = this.eCI;
+ data['Response_Message'] = this.responseMessage;
+ data['Card_Number'] = this.cardNumber;
+ data['Card_Bin'] = this.cardBin;
+ data['Installments_Number'] = this.installmentsNumber;
+ data['Status'] = this.status;
+ data['Merchant_Ref'] = this.merchantRef;
+ data['IsRefund'] = this.isRefund;
+ data['RemmeberMe'] = this.remmeberMe;
+ data['Reconciliation_Reference'] = this.reconciliationReference;
+ return data;
+ }
+}
diff --git a/lib/features/payfort/payfort_repo.dart b/lib/features/payfort/payfort_repo.dart
index c00b01c2..3cd27a9d 100644
--- a/lib/features/payfort/payfort_repo.dart
+++ b/lib/features/payfort/payfort_repo.dart
@@ -8,6 +8,8 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/
import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_check_payment_status_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_project_details_resp_model.dart';
+import 'package:hmg_patient_app_new/features/payfort/models/payfort_request_insert_model.dart';
+import 'package:hmg_patient_app_new/features/payfort/models/payfort_response_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/sdk_token_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@@ -28,6 +30,10 @@ abstract class PayfortRepo {
{required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID});
Future>> getTamaraInstallmentsDetails();
+
+ Future>> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel});
+
+ Future>> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel});
}
class PayfortRepoImp implements PayfortRepo {
@@ -291,4 +297,57 @@ class PayfortRepoImp implements PayfortRepo {
}
}
+ @override
+ Future> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel}) async {
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(ADD_PAYFORT_REQUEST, body: payfortRequestInsertModel.toJson(), onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: response,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ }, isAllowAny: true, isPaymentServices: true);
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
+
+ @override
+ Future> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel}) async {
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(ADD_PAYFORT_RESPONSE, body: payfortResponseInsertModel.toJson(), onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: null,
+ data: response,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ }, isAllowAny: true, isPaymentServices: true);
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
}
diff --git a/lib/features/payfort/payfort_view_model.dart b/lib/features/payfort/payfort_view_model.dart
index aebb8d5f..f567264f 100644
--- a/lib/features/payfort/payfort_view_model.dart
+++ b/lib/features/payfort/payfort_view_model.dart
@@ -8,11 +8,14 @@ import 'package:flutter/services.dart';
import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
+import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart';
-import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_check_payment_status_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_project_details_resp_model.dart';
+import 'package:hmg_patient_app_new/features/payfort/models/payfort_request_insert_model.dart';
+import 'package:hmg_patient_app_new/features/payfort/models/payfort_response_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/sdk_token_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@@ -20,6 +23,8 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:crypto/crypto.dart';
+import 'models/apple_pay_request_insert_model.dart';
+
class PayfortViewModel extends ChangeNotifier {
PayfortRepo payfortRepo;
ErrorHandlerService errorHandlerService;
@@ -92,6 +97,44 @@ class PayfortViewModel extends ChangeNotifier {
);
}
+ Future payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ final result = await payfortRepo.payfortRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel);
+
+ result.fold(
+ (failure) async => await errorHandlerService.handleError(failure: failure),
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ // payfortProjectDetailsRespModel = apiResponse.data!;
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
+ Future payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ final result = await payfortRepo.payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel);
+
+ result.fold(
+ (failure) async => await errorHandlerService.handleError(failure: failure),
+ (apiResponse) {
+ if (apiResponse.messageStatus == 2) {
+ // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
+ } else if (apiResponse.messageStatus == 1) {
+ // payfortProjectDetailsRespModel = apiResponse.data!;
+ notifyListeners();
+ if (onSuccess != null) {
+ onSuccess(apiResponse);
+ }
+ }
+ },
+ );
+ }
+
Future checkPaymentStatus({required String transactionID, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.checkPaymentStatus(transactionID: transactionID);
@@ -291,13 +334,19 @@ class PayfortViewModel extends ChangeNotifier {
}
Future initiatePaymentWithCard( // var request, {
- {
+ String paymentOption,
+ String liveServiceID,
+ String isScheduleLiveCare,
+ String appointmentDate,
+ String appointmentNo,
+ String doctorID, {
SucceededCallback? onSucceeded,
FailedCallback? onFailed,
String countryIsoCode = "SA",
String transactionID = "",
String amount = "0",
String fileNumber = "0",
+ String orderDescription = "",
}) async {
dynamic paymentResult;
@@ -325,6 +374,38 @@ class PayfortViewModel extends ChangeNotifier {
"customer_name": "${getIt.get().getAuthenticatedUser()!.firstName} ${getIt.get().getAuthenticatedUser()!.lastName}",
};
+ PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel();
+
+ payfortRequestInsertModel.accessCode = payfortProjectDetailsRespModel!.accessCode;
+ payfortRequestInsertModel.commandType = "PURCHASE";
+ payfortRequestInsertModel.amount = num.parse(amount);
+ payfortRequestInsertModel.customerEmail = getIt.get().getAuthenticatedUser()!.emailAddress;
+ payfortRequestInsertModel.orderDescription = orderDescription;
+ payfortRequestInsertModel.serviceID = payfortProjectDetailsRespModel!.servID.toString();
+ payfortRequestInsertModel.paymentOption = paymentOption;
+ payfortRequestInsertModel.clientRequestID = transactionID;
+ payfortRequestInsertModel.projectID = payfortProjectDetailsRespModel!.projectID.toString();
+ payfortRequestInsertModel.fileNumber = getIt.get().getAuthenticatedUser()!.patientId.toString();
+ payfortRequestInsertModel.customerName = "${getIt.get().getAuthenticatedUser()!.firstName} ${getIt.get().getAuthenticatedUser()!.lastName}";
+ payfortRequestInsertModel.currency = getIt.get().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED";
+ payfortRequestInsertModel.liveServiceID = liveServiceID;
+ payfortRequestInsertModel.latitude = getIt.get().userLat.toString();
+ payfortRequestInsertModel.longitude = getIt.get().userLong.toString();
+ payfortRequestInsertModel.merchantReference = transactionID;
+ payfortRequestInsertModel.merchantIdentifier = payfortProjectDetailsRespModel!.merchantIdentifier;
+ payfortRequestInsertModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken);
+ payfortRequestInsertModel.sHARequestPhase = payfortProjectDetailsRespModel!.shaRequest;
+ payfortRequestInsertModel.sHAResponsePhase = payfortProjectDetailsRespModel!.shaResponse;
+ payfortRequestInsertModel.signature = sdkTokenResponse!.signature;
+ payfortRequestInsertModel.language = getIt.get().isArabic() ? "ar" : "en";
+ payfortRequestInsertModel.integrationId = "1";
+ payfortRequestInsertModel.isSchedule = isScheduleLiveCare;
+ payfortRequestInsertModel.appointmentDate = appointmentDate;
+ payfortRequestInsertModel.appointmentNo = appointmentNo;
+ payfortRequestInsertModel.doctorID = doctorID;
+
+ await payfortRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel);
+
LoaderBottomSheet.hideLoader();
paymentResult = await FlutterAmazonpaymentservices.normalPay(request, ApiConsts.payFortEnvironmentType, isShowResponsePage: false);
@@ -332,6 +413,27 @@ class PayfortViewModel extends ChangeNotifier {
String responseCode = paymentMap['response_code'] ?? "";
print("Payment Success Data: ${paymentMap.toString()}");
+ PayfortResponseInsertModel payfortResponseInsertModel = PayfortResponseInsertModel();
+
+ payfortResponseInsertModel.fortId = paymentMap['fort_id'] ?? "";
+ payfortResponseInsertModel.commandType = paymentMap['command'] ?? "";
+ payfortResponseInsertModel.amount = num.tryParse(paymentMap['amount'] ?? "0") ?? 0;
+ payfortResponseInsertModel.sadadOLP = "";
+ payfortResponseInsertModel.paymentOption = paymentMap['payment_option'] ?? "";
+ payfortResponseInsertModel.customerIP = paymentMap['customer_ip'] ?? "";
+ payfortResponseInsertModel.eCI = paymentMap['eci'] ?? "";
+ payfortResponseInsertModel.responseMessage = paymentMap['response_message'] ?? "";
+ payfortResponseInsertModel.cardNumber = paymentMap['card_number'] ?? "";
+ payfortResponseInsertModel.cardBin = paymentMap['card_bin'] ?? "";
+ payfortResponseInsertModel.installmentsNumber = int.tryParse(paymentMap['installments_number'] ?? "0") ?? 0;
+ payfortResponseInsertModel.status = paymentMap['status'] ?? "";
+ payfortResponseInsertModel.merchantRef = paymentMap['merchant_reference'] ?? "";
+ payfortResponseInsertModel.isRefund = false;
+ payfortResponseInsertModel.remmeberMe = true;
+ payfortResponseInsertModel.reconciliationReference = "";
+
+ await payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel);
+
if (responseCode.startsWith("14") || responseCode == "20064") {
if (onSucceeded != null) {
onSucceeded(PayFortResult.fromMap(paymentMap));
diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart
index 8d29d818..c82e2d12 100644
--- a/lib/generated/locale_keys.g.dart
+++ b/lib/generated/locale_keys.g.dart
@@ -1940,5 +1940,8 @@ abstract class LocaleKeys {
static const refund = 'refund';
static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc';
+ static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent';
+ static const livecareUpdateInsurance = 'livecareUpdateInsurance';
+ static const schedule = 'schedule';
}
diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart
index daf0839a..4d181230 100644
--- a/lib/presentation/appointments/appointment_details_page.dart
+++ b/lib/presentation/appointments/appointment_details_page.dart
@@ -40,6 +40,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appoin
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/bell_switch.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart';
+import 'package:hmg_patient_app_new/services/clarity_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';
@@ -1215,6 +1216,16 @@ class _AppointmentDetailsPageState extends State {
isDismissible: false,
isFullScreen: false,
isSuccessDialog: true);
+ },
+ onError: (err) {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: err),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
});
// LoaderBottomSheet.hideLoader();
case 15:
diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart
index b3814f00..46130b12 100644
--- a/lib/presentation/appointments/appointment_payment_page.dart
+++ b/lib/presentation/appointments/appointment_payment_page.dart
@@ -228,8 +228,15 @@ class _AppointmentPaymentPageState extends State {
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
+ "mada",
+ "0",
+ widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false",
+ widget.patientAppointmentHistoryResponseModel.appointmentDate!,
+ widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
+ widget.patientAppointmentHistoryResponseModel.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
+ orderDescription: "Appointment Payment",
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
@@ -444,9 +451,16 @@ class _AppointmentPaymentPageState extends State {
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
+ "visa",
+ "0",
+ widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false",
+ widget.patientAppointmentHistoryResponseModel.appointmentDate!,
+ widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
+ widget.patientAppointmentHistoryResponseModel.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
+ orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
diff --git a/lib/presentation/blood_donation/widgets/select_gender_widget.dart b/lib/presentation/blood_donation/widgets/select_gender_widget.dart
index 1368c4e3..e51d3e78 100644
--- a/lib/presentation/blood_donation/widgets/select_gender_widget.dart
+++ b/lib/presentation/blood_donation/widgets/select_gender_widget.dart
@@ -57,9 +57,12 @@ class SelectGenderWidget extends StatelessWidget {
),
],
).paddingSymmetrical(16.h, 16.h).onPress(() {
- final genderValue = isArabic
- ? GenderTypeEnum.values[index].typeAr
- : GenderTypeEnum.values[index].type;
+ final genderValue =
+ // isArabic
+ // ? GenderTypeEnum.values[index].typeAr
+
+ // :
+ GenderTypeEnum.values[index].type;
bloodDonationViewModel.onGenderChange(genderValue);
Navigator.of(context).pop();
}));
diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart
index be097651..f54f95bf 100644
--- a/lib/presentation/book_appointment/doctor_profile_page.dart
+++ b/lib/presentation/book_appointment/doctor_profile_page.dart
@@ -192,6 +192,15 @@ class DoctorProfilePage extends StatelessWidget {
isCloseButtonVisible: true,
);
}),
+ SizedBox(width: 36.w),
+ Column(
+ children: [
+ Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_schedule_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false),
+ SizedBox(height: 16.h),
+ LocaleKeys.schedule.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor),
+ LocaleKeys.view.tr(context: context).toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"),
+ ],
+ ).onPress(() {})
],
),
SizedBox(height: 16.h),
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 8bbf23af..57fd5e94 100644
--- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart
+++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart
@@ -186,27 +186,64 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
CustomButton(
text: LocaleKeys.updateInsurance.tr(context: context),
onPressed: () {
- Navigator.of(context)
- .push(
- CustomPageRoute(
- page: InsuranceHomePage(),
- ),
- )
- .then((val) async {
- LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingFeesPleaseWait.tr());
- await immediateLiveCareVM.getLiveCareImmediateAppointmentFees(onSuccess: (val) {
- LoaderBottomSheet.hideLoader();
- }, onError: (err) {
- LoaderBottomSheet.hideLoader();
- showCommonBottomSheetWithoutHeight(
- context,
- child: Utils.getErrorWidget(loadingText: err),
- callBackFunc: () {},
- isFullScreen: false,
- isCloseButtonVisible: true,
- );
- });
- });
+ showCommonBottomSheetWithoutHeight(
+ title: LocaleKeys.notice.tr(context: context),
+ context,
+ child: Utils.getWarningWidget(
+ loadingText: LocaleKeys.livecareUpdateInsurance.tr(),
+ isShowActionButtons: true,
+ onCancelTap: () {
+ Navigator.pop(context);
+ },
+ onConfirmTap: () async {
+ Navigator.pop(context);
+ LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr());
+ await immediateLiveCareVM.addInsuranceUpdateRequestForLiveCare(onSuccess: (val) {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getSuccessWidget(loadingText: LocaleKeys.livecareUpdateInsuranceSent.tr()),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ }, onError: (err) {
+ LoaderBottomSheet.hideLoader();
+ showCommonBottomSheetWithoutHeight(
+ context,
+ child: Utils.getErrorWidget(loadingText: err),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ });
+ }),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+
+ // Navigator.of(context)
+ // .push(
+ // CustomPageRoute(
+ // page: InsuranceHomePage(),
+ // ),
+ // )
+ // .then((val) async {
+ // LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingFeesPleaseWait.tr());
+ // await immediateLiveCareVM.getLiveCareImmediateAppointmentFees(onSuccess: (val) {
+ // LoaderBottomSheet.hideLoader();
+ // }, onError: (err) {
+ // LoaderBottomSheet.hideLoader();
+ // showCommonBottomSheetWithoutHeight(
+ // context,
+ // child: Utils.getErrorWidget(loadingText: err),
+ // callBackFunc: () {},
+ // isFullScreen: false,
+ // isCloseButtonVisible: true,
+ // );
+ // });
+ // });
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.secondaryLightRedBorderColor,
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 04fd3073..972606a9 100644
--- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart
+++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart
@@ -184,9 +184,16 @@ class _ImmediateLiveCarePaymentPageState extends State {
Spacer(),
LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white),
SizedBox(height: 4.h),
- Consumer(builder: (context, habibWalletVM, child) {
- return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount,
- textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1)
- .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h);
- }),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Consumer(builder: (context, habibWalletVM, child) {
+ return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount,
+ textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1)
+ .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h);
+ }),
+ Transform.flip(
+ flipX: appState.isArabic(),
+ child: Utils.buildSvgWithAssets(
+ icon: AppAssets.arrow_forward,
+ height: 24.h,
+ width: 24.h,
+ iconColor: AppColors.whiteColor,
+ ),
+ ),
+ ],
+ ),
],
),
),
- ),
+ ).onPress(() {
+ showCommonBottomSheetWithoutHeight(
+ title: LocaleKeys.habibWallet.tr(context: context),
+ context,
+ // child: Utils.getSuccessWidget(loadingText: LocaleKeys.erAppointmentBookedSuccess.tr(context: context)),
+ child: HabibWalletBalanceList(),
+ callBackFunc: () {},
+ isFullScreen: false,
+ isCloseButtonVisible: true,
+ );
+ }),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart
index f07cbf63..f894c387 100644
--- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart
+++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart
@@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
+import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@@ -178,9 +179,16 @@ class _WalletPaymentConfirmPageState extends State {
serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
+ "mada",
+ "0",
+ "false",
+ DateUtil.convertDateToString(DateTime.now()),
+ "0",
+ "0",
transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(),
+ orderDescription: "Advance Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader();
@@ -204,7 +212,7 @@ class _WalletPaymentConfirmPageState extends State {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
- Navigator.of(getIt.get().navigatorKey.currentContext!).pop();
+ // Navigator.of(getIt.get().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
@@ -353,9 +361,16 @@ class _WalletPaymentConfirmPageState extends State {
serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
+ "visa",
+ "0",
+ "false",
+ DateUtil.convertDateToString(DateTime.now()),
+ "0",
+ "0",
transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(),
+ orderDescription: "Advance Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader();
@@ -380,7 +395,7 @@ class _WalletPaymentConfirmPageState extends State {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get().navigatorKey.currentContext!).pop();
- Navigator.of(getIt.get().navigatorKey.currentContext!).pop();
+ // Navigator.of(getIt.get().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
onError: (err) {
diff --git a/lib/presentation/habib_wallet/widgets/wallet_balance_list.dart b/lib/presentation/habib_wallet/widgets/wallet_balance_list.dart
new file mode 100644
index 00000000..62817e98
--- /dev/null
+++ b/lib/presentation/habib_wallet/widgets/wallet_balance_list.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
+import 'package:hmg_patient_app_new/core/utils/utils.dart';
+import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
+import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
+import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
+import 'package:hmg_patient_app_new/theme/colors.dart';
+import 'package:provider/provider.dart';
+
+class HabibWalletBalanceList extends StatelessWidget {
+ const HabibWalletBalanceList({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 24.r,
+ hasShadow: false,
+ ),
+ child: Consumer(builder: (context, habibWalletVM, child) {
+ return ListView.separated(
+ itemCount: habibWalletVM.habibWalletBalanceList.length,
+ physics: NeverScrollableScrollPhysics(),
+ padding: EdgeInsets.only(top: 8, bottom: 8),
+ shrinkWrap: true,
+ itemBuilder: (context, index) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(child: habibWalletVM.habibWalletBalanceList[index].projectDescription!.toText16(isBold: true, color: AppColors.textColor)),
+ Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value
+ textColor: AppColors.textColor,
+ iconColor: AppColors.textColor,
+ iconSize: 18.h,
+ isExpanded: false,
+ fontSize: 28.f,
+ fontWeight: FontWeight.w600,
+ letterSpacing: -1),
+ ],
+ ).paddingSymmetrical(0, 12.h);
+ },
+ separatorBuilder: (BuildContext context, int index) {
+ return Divider(height: 1, color: AppColors.textColor.withAlpha(20));
+ },
+ ).paddingSymmetrical(16.h, 24.h).toShimmer2(isShow: habibWalletVM.isWalletAmountLoading);
+ }),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart
index 531b310a..85f1e972 100644
--- a/lib/presentation/home/landing_page.dart
+++ b/lib/presentation/home/landing_page.dart
@@ -448,31 +448,31 @@ class _LandingPageState extends State {
: SizedBox.shrink(),
// Offers And Discounts Carousel - Auto-scrolling from right to left
- appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel
- ? Column(
- children: [
- SizedBox(height: 12.h),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true),
- Row(
- children: [
- LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true),
- SizedBox(width: 2.h),
- Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h),
- ],
- ),
- ],
- ).paddingSymmetrical(24.h, 0.h).onPress(() {
- Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsPage()));
- }),
- SizedBox(height: 16.h),
- OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h),
- SizedBox(height: 18.h),
- ],
- )
- : SizedBox.shrink(),
+ // appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel
+ // ? Column(
+ // children: [
+ // SizedBox(height: 12.h),
+ // Row(
+ // mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ // children: [
+ // ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true),
+ // Row(
+ // children: [
+ // LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true),
+ // SizedBox(width: 2.h),
+ // Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h),
+ // ],
+ // ),
+ // ],
+ // ).paddingSymmetrical(24.h, 0.h).onPress(() {
+ // Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsPage()));
+ // }),
+ // SizedBox(height: 16.h),
+ // OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h),
+ // SizedBox(height: 18.h),
+ // ],
+ // )
+ // : SizedBox.shrink(),
appState.isAuthenticated
? Column(
@@ -516,7 +516,7 @@ class _LandingPageState extends State {
),
).paddingSymmetrical(24.h, 16.h)
: myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty
- ? myAppointmentsVM.patientAppointmentsHistoryList.length == 1
+ ? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 && !immediateLiveCareVM.patientHasPendingLiveCareRequest
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
@@ -536,9 +536,11 @@ class _LandingPageState extends State {
height: isTablet ? 290.h : 255.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
- itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3
- ? myAppointmentsVM.patientAppointmentsHistoryList.length
- : 3,
+ itemCount: () {
+ final int baseCount =
+ myAppointmentsVM.patientAppointmentsHistoryList.length + (immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0);
+ return baseCount < 3 ? baseCount : 3;
+ }(),
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {
@@ -571,10 +573,14 @@ class _LandingPageState extends State {
height: 255.h + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space
child: Builder(
builder: (context) {
+ final int baseAppointmentCount = myAppointmentsVM.patientAppointmentsHistoryList.length;
+ final int liveCareCount = immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0;
+ final int totalCount = baseAppointmentCount + liveCareCount;
+
final int swiperItemCount = myAppointmentsVM.isMyAppointmentsLoading
? 3
- : myAppointmentsVM.patientAppointmentsHistoryList.length < 3
- ? myAppointmentsVM.patientAppointmentsHistoryList.length
+ : totalCount < 3
+ ? totalCount
: 3;
final bool isArabic = appState.isArabic();
return Swiper(
diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart
index 99ed0928..ae0a4c02 100644
--- a/lib/presentation/todo_section/ancillary_order_payment_page.dart
+++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart
@@ -171,9 +171,16 @@ class _AncillaryOrderPaymentPageState extends State {
);
payfortViewModel.initiatePaymentWithCard(
+ "mada",
+ "0",
+ "false",
+ DateUtil.convertDateToString(DateTime.now()),
+ "0",
+ "0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: widget.totalAmount.toString(),
+ orderDescription: "Ancillary Order Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
@@ -286,9 +293,16 @@ class _AncillaryOrderPaymentPageState extends State {
);
payfortViewModel.initiatePaymentWithCard(
+ "visa",
+ "0",
+ "false",
+ DateUtil.convertDateToString(DateTime.now()),
+ "0",
+ "0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: widget.totalAmount.toString(),
+ orderDescription: "Ancillary Order Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
diff --git a/lib/services/clarity_service.dart b/lib/services/clarity_service.dart
new file mode 100644
index 00000000..e8600821
--- /dev/null
+++ b/lib/services/clarity_service.dart
@@ -0,0 +1,291 @@
+import 'package:clarity_flutter/clarity_flutter.dart';
+import 'package:flutter/foundation.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
+
+/// Centralized service for Clarity tracking and analytics
+/// Provides methods for tracking user actions, errors, and app state
+class ClarityService {
+ final AppState _appState;
+
+ ClarityService({required AppState appState}) : _appState = appState;
+
+ /// Initialize Clarity with user context
+ void setUserContext() {
+ if (kDebugMode) return; // Skip in debug mode
+
+ try {
+ if (_appState.isAuthenticated) {
+ final user = _appState.getAuthenticatedUser();
+ if (user != null) {
+ // Set user ID
+ Clarity.setCustomUserId(user.patientId.toString());
+
+ // Set user segments for filtering
+ Clarity.setCustomTag('user_type', user.patientType?.toString() ?? 'unknown');
+ Clarity.setCustomTag('user_segment', user.outSa == 0 ? 'in_sa' : 'out_sa');
+ Clarity.setCustomTag('auth_status', 'authenticated');
+ }
+ } else {
+ Clarity.setCustomTag('auth_status', 'guest');
+ }
+ } catch (e) {
+ debugPrint('Failed to set Clarity user context: $e');
+ }
+ }
+
+ /// Track screen views / route changes
+ void trackScreenView(String screenName, {Map? properties}) {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('current_screen', _truncate(screenName, 100));
+
+ if (properties != null) {
+ properties.forEach((key, value) {
+ Clarity.setCustomTag(key, _truncate(value, 250));
+ });
+ }
+
+ Clarity.sendCustomEvent('screen_view_$screenName');
+ } catch (e) {
+ debugPrint('Failed to track screen view: $e');
+ }
+ }
+
+ /// Track user actions (button clicks, form submissions, etc.)
+ void trackAction(String actionName, {Map? properties}) {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('action_name', _truncate(actionName, 100));
+
+ if (properties != null) {
+ properties.forEach((key, value) {
+ Clarity.setCustomTag('action_$key', _truncate(value, 250));
+ });
+ }
+
+ Clarity.sendCustomEvent('user_action_$actionName');
+ } catch (e) {
+ debugPrint('Failed to track action: $e');
+ }
+ }
+
+ /// Track API calls with success/failure
+ void trackApiCall({
+ required String endpoint,
+ required String method,
+ required bool isSuccess,
+ int? statusCode,
+ String? errorType,
+ String? errorMessage,
+ }) {
+ if (kDebugMode) return;
+
+ try {
+ final eventType = isSuccess ? 'api_success' : 'api_failure';
+
+ Clarity.setCustomTag('api_endpoint', _getEndpointName(endpoint));
+ Clarity.setCustomTag('api_method', method);
+ Clarity.setCustomTag('api_status', isSuccess ? 'success' : 'failure');
+
+ if (statusCode != null) {
+ Clarity.setCustomTag('api_status_code', statusCode.toString());
+ }
+
+ if (errorType != null) {
+ Clarity.setCustomTag('api_error_type', _truncate(errorType, 100));
+ }
+
+ if (errorMessage != null) {
+ Clarity.setCustomTag('api_error_msg', _truncate(errorMessage, 250));
+ }
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('api_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent(eventType);
+ } catch (e) {
+ debugPrint('Failed to track API call: $e');
+ }
+ }
+
+ /// Track app errors and exceptions
+ void trackError({
+ required String errorType,
+ required String errorMessage,
+ String? stackTrace,
+ bool isFatal = false,
+ Map? additionalContext,
+ }) {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('error_type', _truncate(errorType, 100));
+ Clarity.setCustomTag('error_fatal', isFatal.toString());
+ Clarity.setCustomTag('error_message', _truncate(errorMessage, 250));
+
+ if (stackTrace != null) {
+ // Only send first 250 chars of stack trace
+ Clarity.setCustomTag('error_stack', _truncate(stackTrace, 250));
+ }
+
+ if (additionalContext != null) {
+ additionalContext.forEach((key, value) {
+ Clarity.setCustomTag('error_$key', _truncate(value, 250));
+ });
+ }
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('error_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent(isFatal ? 'app_fatal_error' : 'app_error');
+ } catch (e) {
+ debugPrint('Failed to track error: $e');
+ }
+ }
+
+ /// Track user frustration events (rage clicks, dead clicks)
+ void trackFrustration({
+ required String frustrationType,
+ required String elementLabel,
+ Map? context,
+ }) {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('frustration_type', frustrationType);
+ Clarity.setCustomTag('frustration_element', _truncate(elementLabel, 100));
+
+ if (context != null) {
+ context.forEach((key, value) {
+ Clarity.setCustomTag('frustration_$key', _truncate(value, 250));
+ });
+ }
+
+ Clarity.sendCustomEvent('user_frustration_$frustrationType');
+ } catch (e) {
+ debugPrint('Failed to track frustration: $e');
+ }
+ }
+
+ /// Track feature usage
+ void trackFeatureUsage(String featureName, {Map? metadata}) {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('feature_name', _truncate(featureName, 100));
+
+ if (metadata != null) {
+ metadata.forEach((key, value) {
+ Clarity.setCustomTag('feature_$key', _truncate(value, 250));
+ });
+ }
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('feature_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('feature_used_$featureName');
+ } catch (e) {
+ debugPrint('Failed to track feature usage: $e');
+ }
+ }
+
+ /// Track payment events
+ void trackPayment({
+ required String paymentStatus,
+ String? paymentMethod,
+ String? amount,
+ String? transactionId,
+ }) {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('payment_status', paymentStatus);
+
+ if (paymentMethod != null) {
+ Clarity.setCustomTag('payment_method', paymentMethod);
+ }
+
+ if (amount != null) {
+ Clarity.setCustomTag('payment_amount', amount);
+ }
+
+ if (transactionId != null) {
+ Clarity.setCustomTag('payment_tx_id', _truncate(transactionId, 100));
+ }
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('payment_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('payment_$paymentStatus');
+ } catch (e) {
+ debugPrint('Failed to track payment: $e');
+ }
+ }
+
+ /// Track appointment booking flow
+ void trackAppointmentEvent({
+ required String eventType,
+ String? doctorId,
+ String? clinicId,
+ String? appointmentDate,
+ }) {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('appointment_event', eventType);
+
+ if (doctorId != null) {
+ Clarity.setCustomTag('appointment_doctor', doctorId);
+ }
+
+ if (clinicId != null) {
+ Clarity.setCustomTag('appointment_clinic', clinicId);
+ }
+
+ if (appointmentDate != null) {
+ Clarity.setCustomTag('appointment_date', appointmentDate);
+ }
+
+ if (_appState.isAuthenticated) {
+ Clarity.setCustomTag('appointment_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
+ }
+
+ Clarity.sendCustomEvent('appointment_$eventType');
+ } catch (e) {
+ debugPrint('Failed to track appointment event: $e');
+ }
+ }
+
+ /// Clear user context on logout
+ void clearUserContext() {
+ if (kDebugMode) return;
+
+ try {
+ Clarity.setCustomTag('auth_status', 'guest');
+ Clarity.setCustomTag('user_action', 'logout');
+ Clarity.sendCustomEvent('user_logged_out');
+ } catch (e) {
+ debugPrint('Failed to clear Clarity user context: $e');
+ }
+ }
+
+ /// Helper: Truncate string to fit Clarity's limits
+ String _truncate(String text, int maxLength) {
+ if (text.length <= maxLength) return text;
+ return '...${text.substring(text.length - (maxLength - 3))}';
+ }
+
+ /// Helper: Extract endpoint name from full URL
+ String _getEndpointName(String endpoint) {
+ final cleanEndpoint = endpoint.split('/').last;
+ return _truncate(cleanEndpoint, 100);
+ }
+}
+
+
diff --git a/lib/splashPage.dart b/lib/splashPage.dart
index 2edf222e..dc49428a 100644
--- a/lib/splashPage.dart
+++ b/lib/splashPage.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:flutter/foundation.dart';
@@ -10,6 +11,7 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
+import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@@ -91,6 +93,45 @@ class _SplashScreenState extends State {
);
Clarity.initialize(context, config);
+
+ // Set initial app context tags
+ try {
+ final appState = getIt.get();
+
+ // Set device and app metadata
+ Clarity.setCustomTag('app_version', ApiConsts.appVersionID.toString());
+ Clarity.setCustomTag('device_type', appState.deviceTypeID ?? 'unknown');
+ Clarity.setCustomTag('platform', Platform.isIOS ? 'ios' : 'android');
+
+ // Set user context if authenticated
+ if (appState.isAuthenticated) {
+ final user = appState.getAuthenticatedUser();
+ if (user != null) {
+ // Set user ID for session tracking
+ Clarity.setCustomUserId(user.patientId.toString());
+
+ // Set custom tags for filtering sessions
+ Clarity.setCustomTag('user_type', user.patientType?.toString() ?? 'unknown');
+ Clarity.setCustomTag('user_segment', user.outSa == 0 ? 'in_sa' : 'out_sa');
+ Clarity.setCustomTag('auth_status', 'authenticated');
+
+ // Optional: Set other non-sensitive user properties
+ if (user.mobileNumber != null) {
+ // Mask phone number for privacy (show only last 4 digits)
+ final maskedPhone = '***${user.mobileNumber!.substring(user.mobileNumber!.length - 4)}';
+ Clarity.setCustomTag('user_phone_masked', maskedPhone);
+ }
+ }
+ } else {
+ Clarity.setCustomTag('auth_status', 'guest');
+ }
+
+ // Send initialization event
+ Clarity.sendCustomEvent('app_initialized');
+
+ } catch (e) {
+ debugPrint('Failed to set Clarity user context: $e');
+ }
}
navigateToTeleConsult() async {
diff --git a/lib/widgets/app_language_change.dart b/lib/widgets/app_language_change.dart
index ff8f9c59..c2ae0272 100644
--- a/lib/widgets/app_language_change.dart
+++ b/lib/widgets/app_language_change.dart
@@ -75,6 +75,7 @@ class _AppLanguageChangeState extends State {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
+ habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.getLakumAccountInformation();
diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart
index 3f4902e0..f3eb94fb 100644
--- a/lib/widgets/buttons/custom_button.dart
+++ b/lib/widgets/buttons/custom_button.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@@ -60,39 +61,73 @@ class CustomButton extends StatelessWidget {
final fontS = fontSize ?? (16.f);
final isButtonDisabled = isDisabled || onPressed == null;
- return GestureDetector(
- onTap: isButtonDisabled ? null : onPressed,
- child: Container(
- height: height == null
- ? 56.h
- : (isFoldable || isTablet)
- ? height! + 10.h
- : height,
- width: width,
- padding: padding,
- decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
- color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor,
- borderRadius: radius,
- customBorder: BorderRadius.circular(radius),
- side: borderSide ?? BorderSide(width: borderWidth.h, color: isButtonDisabled ? Colors.transparent : borderColor)),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- if (icon != null)
- Padding(
- padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
- child: Utils.buildSvgWithAssets(
- icon: icon!,
- iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor,
- isDisabled: isButtonDisabled,
- width: iconS,
- height: iconS,
- applyThemeColor: applyThemeColor),
- ),
- if (text.isNotEmpty && icon != null)
- Flexible(
- child: Text(
+ return Semantics(
+ button: true,
+ enabled: !isButtonDisabled,
+ label: text,
+ hint: isButtonDisabled ? 'Button is disabled' : 'Tap to $text',
+ child: GestureDetector(
+ onTap: isButtonDisabled
+ ? null
+ : () {
+ // Log button tap to Clarity before executing callback
+ try {
+ Clarity.setCustomTag('button_text', text.length > 100 ? text.substring(0, 100) : text);
+ Clarity.setCustomTag('button_action', 'tap');
+ Clarity.setCustomTag('button_disabled', 'false');
+ Clarity.sendCustomEvent('button_tapped');
+ } catch (e) {
+ // Silence Clarity errors to not interrupt user flow
+ }
+
+ // Execute the actual button callback
+ onPressed?.call();
+ },
+ child: Container(
+ height: height == null
+ ? 56.h
+ : (isFoldable || isTablet)
+ ? height! + 10.h
+ : height,
+ width: width,
+ padding: padding,
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor,
+ borderRadius: radius,
+ customBorder: BorderRadius.circular(radius),
+ side: borderSide ?? BorderSide(width: borderWidth.h, color: isButtonDisabled ? Colors.transparent : borderColor)),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (icon != null)
+ Padding(
+ padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
+ child: Utils.buildSvgWithAssets(
+ icon: icon!,
+ iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor,
+ isDisabled: isButtonDisabled,
+ width: iconS,
+ height: iconS,
+ applyThemeColor: applyThemeColor),
+ ),
+ if (text.isNotEmpty && icon != null)
+ Flexible(
+ child: Text(
+ text,
+ overflow: textOverflow ?? TextOverflow.ellipsis,
+ maxLines: 1,
+ textAlign: TextAlign.center,
+ style: context.dynamicTextStyle(
+ fontSize: fontS,
+ color: isButtonDisabled ? AppColors.greyTextColor : textColor,
+ letterSpacing: 0,
+ fontWeight: isBold ? FontWeight.w700 : fontWeight,
+ ),
+ ),
+ ),
+ if (text.isNotEmpty && icon == null)
+ Text(
text,
overflow: textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
@@ -104,22 +139,9 @@ class CustomButton extends StatelessWidget {
fontWeight: isBold ? FontWeight.w700 : fontWeight,
),
),
- ),
- if (text.isNotEmpty && icon == null)
- Text(
- text,
- overflow: textOverflow ?? TextOverflow.ellipsis,
- maxLines: 1,
- textAlign: TextAlign.center,
- style: context.dynamicTextStyle(
- fontSize: fontS,
- color: isButtonDisabled ? AppColors.greyTextColor : textColor,
- letterSpacing: 0,
- fontWeight: isBold ? FontWeight.w700 : fontWeight,
- ),
- ),
- ],
- ),
- ));
+ ],
+ ),
+ )),
+ );
}
}
diff --git a/lib/widgets/clarity_interaction_tracker.dart b/lib/widgets/clarity_interaction_tracker.dart
new file mode 100644
index 00000000..e27aec86
--- /dev/null
+++ b/lib/widgets/clarity_interaction_tracker.dart
@@ -0,0 +1,200 @@
+import 'package:flutter/material.dart';
+import 'package:clarity_flutter/clarity_flutter.dart';
+
+/// A wrapper widget that tracks user interactions with Clarity for UX frustration analysis.
+/// Helps identify rage clicks, dead clicks, and rapid scrolls.
+///
+/// Usage:
+/// ```dart
+/// ClarityInteractionTracker(
+/// semanticLabel: 'Login Button',
+/// interactionType: ClarityInteractionType.button,
+/// onTap: () { /* your action */ },
+/// child: YourWidget(),
+/// )
+/// ```
+class ClarityInteractionTracker extends StatelessWidget {
+ final Widget child;
+ final String semanticLabel;
+ final VoidCallback? onTap;
+ final ClarityInteractionType interactionType;
+ final Map? customTags;
+ final bool enabled;
+
+ const ClarityInteractionTracker({
+ super.key,
+ required this.child,
+ required this.semanticLabel,
+ this.onTap,
+ this.interactionType = ClarityInteractionType.button,
+ this.customTags,
+ this.enabled = true,
+ });
+
+ void _logInteraction(BuildContext context) {
+ if (!enabled) return;
+
+ try {
+ // Set common tags
+ Clarity.setCustomTag('interaction_type', interactionType.name);
+ Clarity.setCustomTag('interaction_label',
+ semanticLabel.length > 100 ? semanticLabel.substring(0, 100) : semanticLabel
+ );
+ Clarity.setCustomTag('interaction_enabled', enabled.toString());
+
+ // Set route/screen name if available
+ final route = ModalRoute.of(context);
+ if (route != null && route.settings.name != null) {
+ Clarity.setCustomTag('screen_route', route.settings.name!);
+ }
+
+ // Add custom tags if provided
+ if (customTags != null) {
+ customTags!.forEach((key, value) {
+ // Ensure key and value fit Clarity's 255 char limit
+ final truncatedValue = value.length > 250 ? value.substring(0, 250) : value;
+ Clarity.setCustomTag(key, truncatedValue);
+ });
+ }
+
+ // Send event to Clarity
+ Clarity.sendCustomEvent('user_interaction_${interactionType.name}');
+ } catch (e) {
+ // Silence Clarity errors to not interrupt user flow
+ debugPrint('Clarity interaction tracking error: $e');
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Semantics(
+ label: semanticLabel,
+ button: interactionType == ClarityInteractionType.button,
+ enabled: enabled,
+ excludeSemantics: false,
+ child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: enabled && onTap != null
+ ? () {
+ _logInteraction(context);
+ onTap?.call();
+ }
+ : null,
+ child: child,
+ ),
+ );
+ }
+}
+
+/// Types of interactions that can be tracked
+enum ClarityInteractionType {
+ button,
+ link,
+ card,
+ listItem,
+ icon,
+ image,
+ input,
+ custom,
+}
+
+/// Extension to easily wrap any widget with Clarity tracking
+extension ClarityTrackingExtension on Widget {
+ /// Wraps this widget with Clarity interaction tracking
+ Widget trackWithClarity({
+ required String label,
+ VoidCallback? onTap,
+ ClarityInteractionType type = ClarityInteractionType.custom,
+ Map? customTags,
+ bool enabled = true,
+ }) {
+ return ClarityInteractionTracker(
+ semanticLabel: label,
+ onTap: onTap,
+ interactionType: type,
+ customTags: customTags,
+ enabled: enabled,
+ child: this,
+ );
+ }
+}
+
+/// Wrapper for ScrollView to track rapid scrolling behavior
+class ClarityScrollTracker extends StatefulWidget {
+ final Widget child;
+ final String scrollViewLabel;
+ final ScrollController? controller;
+
+ const ClarityScrollTracker({
+ super.key,
+ required this.child,
+ required this.scrollViewLabel,
+ this.controller,
+ });
+
+ @override
+ State createState() => _ClarityScrollTrackerState();
+}
+
+class _ClarityScrollTrackerState extends State {
+ late ScrollController _controller;
+ bool _isRapidScrolling = false;
+ DateTime? _lastScrollTime;
+ int _rapidScrollCount = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = widget.controller ?? ScrollController();
+ _controller.addListener(_onScroll);
+ }
+
+ void _onScroll() {
+ final now = DateTime.now();
+
+ // Detect rapid scrolling (scroll events within 100ms)
+ if (_lastScrollTime != null) {
+ final timeDiff = now.difference(_lastScrollTime!).inMilliseconds;
+
+ if (timeDiff < 100) {
+ _rapidScrollCount++;
+
+ if (!_isRapidScrolling && _rapidScrollCount > 5) {
+ _isRapidScrolling = true;
+ _logRapidScroll();
+ }
+ } else if (timeDiff > 500) {
+ // Reset if scrolling slows down
+ _isRapidScrolling = false;
+ _rapidScrollCount = 0;
+ }
+ }
+
+ _lastScrollTime = now;
+ }
+
+ void _logRapidScroll() {
+ try {
+ Clarity.setCustomTag('scroll_type', 'rapid');
+ Clarity.setCustomTag('scroll_view', widget.scrollViewLabel);
+ Clarity.setCustomTag('scroll_position', _controller.position.pixels.toString());
+ Clarity.sendCustomEvent('rapid_scroll_detected');
+ } catch (e) {
+ debugPrint('Clarity scroll tracking error: $e');
+ }
+ }
+
+ @override
+ void dispose() {
+ if (widget.controller == null) {
+ _controller.dispose();
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return widget.child;
+ }
+}
+
diff --git a/pubspec.yaml b/pubspec.yaml
index 880ec18e..ba0c0ed5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -87,7 +87,7 @@ dependencies:
open_filex: ^4.7.0
flutter_swiper_view:
path: package/flutter_swiper_view
- flutter_callkit_incoming: ^3.0.0
+ flutter_callkit_incoming: 3.0.0
in_app_update: ^4.2.5
# device_preview: ^1.3.1
image_picker_android: ^0.8.13+16