Compare commits

...

2 Commits

@ -1958,5 +1958,7 @@
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:"
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
"livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟"
}

@ -1947,7 +1947,9 @@
"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?"
}

@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient {
body['TokenID'] = "@dm!n";
}
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 4772993;
body['TokenID'] = "@dm!n";
// body['PatientID'] = 255375;
// 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 '';

@ -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;

@ -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>(() => PayTabsViewModel(appState: getIt()));
getIt.registerLazySingleton<ClarityService>(
() => ClarityService(appState: getIt.get<AppState>()),
);
}
}

@ -25,6 +25,9 @@ abstract class ImmediateLiveCareRepo {
{Function(dynamic)? onSuccess, Function(String)? onError});
Future<Either<Failure, GenericApiModel<List<PatientLiveCareHistory>>>> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError});
Future<Either<Failure, GenericApiModel<dynamic>>> 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<Either<Failure, GenericApiModel>> addInsuranceUpdateRequestForLiveCare(String name, String mobileNumber, String fileNumber,
{Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async {
Map<String, dynamic> mapDevice = {
"PatientName": name,
"MobileNumber": mobileNumber,
"FileNumber": fileNumber,
};
try {
GenericApiModel<dynamic>? 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<dynamic>(
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()));
}
}
}

@ -245,6 +245,28 @@ class ImmediateLiveCareViewModel extends ChangeNotifier {
);
}
Future<void> 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<void> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
patientLiveCareHistoryList = [];
patientHasPendingLiveCareRequest = false;

@ -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<String, dynamic> 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<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
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;
}
}

@ -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<String, dynamic> 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<String, dynamic> toJson() {
final Map<String, dynamic> data = Map<String, dynamic>();
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;
}
}

@ -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<Either<Failure, GenericApiModel<GetTamaraInstallmentsDetailsResponseModel>>> getTamaraInstallmentsDetails();
Future<Either<Failure, GenericApiModel<dynamic>>> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel});
Future<Either<Failure, GenericApiModel<dynamic>>> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel});
}
class PayfortRepoImp implements PayfortRepo {
@ -291,4 +297,57 @@ class PayfortRepoImp implements PayfortRepo {
}
}
@override
Future<Either<Failure, GenericApiModel>> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel}) async {
try {
GenericApiModel<dynamic>? 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<dynamic>(
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<Either<Failure, GenericApiModel>> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel}) async {
try {
GenericApiModel<dynamic>? 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<dynamic>(
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()));
}
}
}

@ -8,11 +8,15 @@ 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';
@ -92,6 +96,44 @@ class PayfortViewModel extends ChangeNotifier {
);
}
Future<void> 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<void> 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<void> checkPaymentStatus({required String transactionID, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.checkPaymentStatus(transactionID: transactionID);
@ -291,13 +333,19 @@ class PayfortViewModel extends ChangeNotifier {
}
Future<void> 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 +373,38 @@ class PayfortViewModel extends ChangeNotifier {
"customer_name": "${getIt.get<AppState>().getAuthenticatedUser()!.firstName} ${getIt.get<AppState>().getAuthenticatedUser()!.lastName}",
};
PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel();
payfortRequestInsertModel.accessCode = payfortProjectDetailsRespModel!.accessCode;
payfortRequestInsertModel.commandType = "PURCHASE";
payfortRequestInsertModel.amount = num.parse(amount);
payfortRequestInsertModel.customerEmail = getIt.get<AppState>().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<AppState>().getAuthenticatedUser()!.patientId.toString();
payfortRequestInsertModel.customerName = "${getIt.get<AppState>().getAuthenticatedUser()!.firstName} ${getIt.get<AppState>().getAuthenticatedUser()!.lastName}";
payfortRequestInsertModel.currency = getIt.get<AppState>().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED";
payfortRequestInsertModel.liveServiceID = liveServiceID;
payfortRequestInsertModel.latitude = getIt.get<AppState>().userLat.toString();
payfortRequestInsertModel.longitude = getIt.get<AppState>().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<AppState>().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 +412,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));

@ -1940,5 +1940,7 @@ abstract class LocaleKeys {
static const refund = 'refund';
static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc';
static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent';
static const livecareUpdateInsurance = 'livecareUpdateInsurance';
}

@ -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';

@ -228,8 +228,15 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
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!,
widget.patientAppointmentHistoryResponseModel.doctorID!,
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<AppointmentPaymentPage> {
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!,
widget.patientAppointmentHistoryResponseModel.doctorID!,
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));

@ -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,

@ -184,9 +184,16 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"mada",
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
orderDescription: "LiveCare Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
@ -328,9 +335,16 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"visa",
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
orderDescription: "LiveCare Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));

@ -192,9 +192,16 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
DateTime.now().millisecondsSinceEpoch.toString(),
bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(),
orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader();
@ -336,9 +343,16 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
DateTime.now().millisecondsSinceEpoch.toString(),
bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(),
orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader();

@ -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<WalletPaymentConfirmPage> {
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<WalletPaymentConfirmPage> {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
// Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
@ -353,9 +361,16 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
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<WalletPaymentConfirmPage> {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
// Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
onError: (err) {

@ -171,9 +171,16 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
);
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<AncillaryOrderPaymentPage> {
);
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));

@ -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<String, String>? 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<String, String>? 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<String, String>? 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<String, String>? 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<String, String>? 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);
}
}

@ -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<SplashPage> {
);
Clarity.initialize(context, config);
// Set initial app context tags
try {
final appState = getIt.get<AppState>();
// 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 {

@ -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,
),
),
],
),
));
],
),
)),
);
}
}

@ -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<String, String>? 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<String, String>? 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<ClarityScrollTracker> createState() => _ClarityScrollTrackerState();
}
class _ClarityScrollTrackerState extends State<ClarityScrollTracker> {
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;
}
}
Loading…
Cancel
Save