import 'package:flutter/material.dart'; import 'package:clarity_flutter/clarity_flutter.dart'; /// A wrapper widget that tracks user interactions with Clarity for UX frustration analysis. /// Helps identify rage clicks, dead clicks, and rapid scrolls. /// /// Usage: /// ```dart /// ClarityInteractionTracker( /// semanticLabel: 'Login Button', /// interactionType: ClarityInteractionType.button, /// onTap: () { /* your action */ }, /// child: YourWidget(), /// ) /// ``` class ClarityInteractionTracker extends StatelessWidget { final Widget child; final String semanticLabel; final VoidCallback? onTap; final ClarityInteractionType interactionType; final Map? customTags; final bool enabled; const ClarityInteractionTracker({ super.key, required this.child, required this.semanticLabel, this.onTap, this.interactionType = ClarityInteractionType.button, this.customTags, this.enabled = true, }); void _logInteraction(BuildContext context) { if (!enabled) return; try { // Set common tags Clarity.setCustomTag('interaction_type', interactionType.name); Clarity.setCustomTag('interaction_label', semanticLabel.length > 100 ? semanticLabel.substring(0, 100) : semanticLabel ); Clarity.setCustomTag('interaction_enabled', enabled.toString()); // Set route/screen name if available final route = ModalRoute.of(context); if (route != null && route.settings.name != null) { Clarity.setCustomTag('screen_route', route.settings.name!); } // Add custom tags if provided if (customTags != null) { customTags!.forEach((key, value) { // Ensure key and value fit Clarity's 255 char limit final truncatedValue = value.length > 250 ? value.substring(0, 250) : value; Clarity.setCustomTag(key, truncatedValue); }); } // Send event to Clarity Clarity.sendCustomEvent('user_interaction_${interactionType.name}'); } catch (e) { // Silence Clarity errors to not interrupt user flow debugPrint('Clarity interaction tracking error: $e'); } } @override Widget build(BuildContext context) { return Semantics( label: semanticLabel, button: interactionType == ClarityInteractionType.button, enabled: enabled, excludeSemantics: false, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: enabled && onTap != null ? () { _logInteraction(context); onTap?.call(); } : null, child: child, ), ); } } /// Types of interactions that can be tracked enum ClarityInteractionType { button, link, card, listItem, icon, image, input, custom, } /// Extension to easily wrap any widget with Clarity tracking extension ClarityTrackingExtension on Widget { /// Wraps this widget with Clarity interaction tracking Widget trackWithClarity({ required String label, VoidCallback? onTap, ClarityInteractionType type = ClarityInteractionType.custom, Map? customTags, bool enabled = true, }) { return ClarityInteractionTracker( semanticLabel: label, onTap: onTap, interactionType: type, customTags: customTags, enabled: enabled, child: this, ); } } /// Wrapper for ScrollView to track rapid scrolling behavior class ClarityScrollTracker extends StatefulWidget { final Widget child; final String scrollViewLabel; final ScrollController? controller; const ClarityScrollTracker({ super.key, required this.child, required this.scrollViewLabel, this.controller, }); @override State createState() => _ClarityScrollTrackerState(); } class _ClarityScrollTrackerState extends State { late ScrollController _controller; bool _isRapidScrolling = false; DateTime? _lastScrollTime; int _rapidScrollCount = 0; @override void initState() { super.initState(); _controller = widget.controller ?? ScrollController(); _controller.addListener(_onScroll); } void _onScroll() { final now = DateTime.now(); // Detect rapid scrolling (scroll events within 100ms) if (_lastScrollTime != null) { final timeDiff = now.difference(_lastScrollTime!).inMilliseconds; if (timeDiff < 100) { _rapidScrollCount++; if (!_isRapidScrolling && _rapidScrollCount > 5) { _isRapidScrolling = true; _logRapidScroll(); } } else if (timeDiff > 500) { // Reset if scrolling slows down _isRapidScrolling = false; _rapidScrollCount = 0; } } _lastScrollTime = now; } void _logRapidScroll() { try { Clarity.setCustomTag('scroll_type', 'rapid'); Clarity.setCustomTag('scroll_view', widget.scrollViewLabel); Clarity.setCustomTag('scroll_position', _controller.position.pixels.toString()); Clarity.sendCustomEvent('rapid_scroll_detected'); } catch (e) { debugPrint('Clarity scroll tracking error: $e'); } } @override void dispose() { if (widget.controller == null) { _controller.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return widget.child; } }