Merge pull request 'haroon_dev' (#337) from haroon_dev into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/337haroon_dev
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 |
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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…
Reference in New Issue