Merge pull request 'haroon_dev' (#337) from haroon_dev into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/337
haroon_dev
Haroon6138 14 hours ago
commit 89645645ba

@ -0,0 +1,9 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="10" fill="#FEE9EA"/>
<path d="M20 24C19.4477 24 19 24.4477 19 25C19 25.5523 19.4477 26 20 26H20.009C20.5613 26 21.009 25.5523 21.009 25C21.009 24.4477 20.5613 24 20.009 24H20Z" fill="#ED1C2B"/>
<path d="M23.9955 24C23.4432 24 22.9955 24.4477 22.9955 25C22.9955 25.5523 23.4432 26 23.9955 26H24.0045C24.5568 26 25.0045 25.5523 25.0045 25C25.0045 24.4477 24.5568 24 24.0045 24H23.9955Z" fill="#ED1C2B"/>
<path d="M27.991 24C27.4387 24 26.991 24.4477 26.991 25C26.991 25.5523 27.4387 26 27.991 26H28C28.5523 26 29 25.5523 29 25C29 24.4477 28.5523 24 28 24H27.991Z" fill="#ED1C2B"/>
<path d="M20 28C19.4477 28 19 28.4477 19 29C19 29.5523 19.4477 30 20 30H20.009C20.5613 30 21.009 29.5523 21.009 29C21.009 28.4477 20.5613 28 20.009 28H20Z" fill="#ED1C2B"/>
<path d="M23.9955 28C23.4432 28 22.9955 28.4477 22.9955 29C22.9955 29.5523 23.4432 30 23.9955 30H24.0045C24.5568 30 25.0045 29.5523 25.0045 29C25.0045 28.4477 24.5568 28 24.0045 28H23.9955Z" fill="#ED1C2B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.75 14C18.75 13.5858 18.4142 13.25 18 13.25C17.5858 13.25 17.25 13.5858 17.25 14V14.6348C16.4452 14.8629 15.7665 15.2336 15.2015 15.8444C14.4236 16.6853 14.0778 17.7479 13.9122 19.0795C13.75 20.3843 13.75 22.0565 13.75 24.19V24.81C13.75 26.9435 13.75 28.6157 13.9122 29.9205C14.0778 31.2521 14.4236 32.3147 15.2015 33.1556C15.9871 34.0049 16.9928 34.3901 18.2513 34.573C19.4693 34.75 21.0255 34.75 22.9892 34.75H25.0108C26.9745 34.75 28.5307 34.75 29.7487 34.573C31.0072 34.3901 32.0129 34.0049 32.7985 33.1556C33.5764 32.3147 33.9222 31.2521 34.0878 29.9205C34.25 28.6157 34.25 26.9435 34.25 24.8099V24.1901C34.25 22.0565 34.25 20.3843 34.0878 19.0795C33.9222 17.7479 33.5764 16.6853 32.7985 15.8444C32.2335 15.2336 31.5548 14.8629 30.75 14.6348V14C30.75 13.5858 30.4142 13.25 30 13.25C29.5858 13.25 29.25 13.5858 29.25 14V14.3659C28.1139 14.25 26.716 14.25 25.0108 14.25L22.9892 14.25C21.284 14.25 19.8861 14.25 18.75 14.3659V14ZM17.2786 16.2058C17.3681 16.52 17.6572 16.75 18 16.75C18.4142 16.75 18.75 16.4142 18.75 16V15.8746C19.811 15.7516 21.1824 15.75 23.05 15.75L24.95 15.75C26.8176 15.75 28.189 15.7516 29.25 15.8746V16C29.25 16.4142 29.5858 16.75 30 16.75C30.3428 16.75 30.6319 16.52 30.7214 16.2058C31.1287 16.3679 31.438 16.5826 31.6973 16.8629C32.1704 17.3744 32.4505 18.0759 32.5982 19.2564C32.5661 19.2522 32.5333 19.25 32.5 19.25L15.5 19.25C15.4667 19.25 15.4339 19.2522 15.4018 19.2564C15.5495 18.0759 15.8296 17.3744 16.3027 16.8629C16.562 16.5826 16.8713 16.3679 17.2786 16.2058ZM15.2901 20.7202C15.2506 21.6832 15.25 22.8363 15.25 24.2432V24.7568C15.25 26.9551 15.2514 28.5339 15.4008 29.7354C15.5482 30.9208 15.8285 31.6244 16.3027 32.1371C16.7692 32.6414 17.3973 32.9331 18.467 33.0886C19.5648 33.2481 21.0119 33.25 23.05 33.25H24.95C26.9881 33.25 28.4352 33.2481 29.533 33.0886C30.6027 32.9331 31.2308 32.6414 31.6973 32.1371C32.1715 31.6244 32.4518 30.9208 32.5992 29.7354C32.7486 28.5339 32.75 26.9551 32.75 24.7568V24.2432C32.75 22.8363 32.7494 21.6832 32.7099 20.7202C32.6433 20.7396 32.5728 20.75 32.5 20.75L15.5 20.75C15.4272 20.75 15.3567 20.7396 15.2901 20.7202Z" fill="#ED1C2B"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

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

@ -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"
}

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

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

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

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

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

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

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

@ -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,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<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 +334,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 +374,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 +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));

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

@ -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<AppointmentDetailsPage> {
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:

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

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

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

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

@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode
import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/wallet_balance_list.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart';
import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.dart';
@ -97,15 +98,39 @@ class _HabibWalletState extends State<HabibWalletPage> {
Spacer(),
LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white),
SizedBox(height: 4.h),
Consumer<HabibWalletViewModel>(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<HabibWalletViewModel>(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,

@ -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) {

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

@ -448,31 +448,31 @@ class _LandingPageState extends State<LandingPage> {
: 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<LandingPage> {
),
).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<LandingPage> {
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<LandingPage> {
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(

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

@ -75,6 +75,7 @@ class _AppLanguageChangeState extends State<AppLanguageChange> {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.getLakumAccountInformation();

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

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

Loading…
Cancel
Save