Compare commits
2 Commits
64eb04011e
...
3dd4665f6e
| Author | SHA1 | Date |
|---|---|---|
|
|
3dd4665f6e | 1 week ago |
|
|
6abcdf135e | 1 week ago |
@ -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,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