import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; /// Custom authentication button with loading states and variants class AuthButton extends StatefulWidget { final String text; final VoidCallback? onPressed; final bool isLoading; final bool isSuccess; final String? loadingText; final String? successText; final AuthButtonVariant variant; final bool fullWidth; final Widget? icon; final Duration successDuration; const AuthButton({ super.key, required this.text, this.onPressed, this.isLoading = false, this.isSuccess = false, this.loadingText, this.successText, this.variant = AuthButtonVariant.primary, this.fullWidth = true, this.icon, this.successDuration = const Duration(seconds: 2), }); @override State createState() => _AuthButtonState(); } class _AuthButtonState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; late Animation _opacityAnimation; bool _isPressed = false; DateTime? _lastPressedTime; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); _scaleAnimation = Tween( begin: 1.0, end: 0.95, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, )); _opacityAnimation = Tween( begin: 1.0, end: 0.7, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, )); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colors = _getColors(theme); final isDisabled = widget.isLoading || widget.onPressed == null; final displayText = _getDisplayText(); final displayIcon = _getDisplayIcon(); return AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Opacity( opacity: _opacityAnimation.value, child: Semantics( button: true, label: _getAccessibilityLabel(), hint: _getAccessibilityHint(), child: SizedBox( width: widget.fullWidth ? double.infinity : null, height: 48, child: ElevatedButton( onPressed: isDisabled ? null : _handlePressed, style: ElevatedButton.styleFrom( backgroundColor: _getBackgroundColor(colors), foregroundColor: _getForegroundColor(colors), disabledBackgroundColor: colors.disabledBackground, disabledForegroundColor: colors.disabledForeground, elevation: widget.variant == AuthButtonVariant.primary ? 2 : 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: widget.variant == AuthButtonVariant.outline ? BorderSide(color: colors.background) : BorderSide.none, ), ), child: _buildButtonContent(displayText, displayIcon, colors.foreground, theme), ), ), ), ), ); }, ); } Widget _buildButtonContent(String text, Widget? icon, Color foregroundColor, ThemeData theme) { if (widget.isLoading) { return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(foregroundColor), ), ), const SizedBox(width: 12), Text( widget.loadingText ?? 'Loading...', style: theme.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w600, color: foregroundColor, ), ), ], ); } if (widget.isSuccess) { return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.check_circle, color: foregroundColor, size: 20, ), const SizedBox(width: 8), Text( widget.successText ?? 'Success!', style: theme.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w600, color: foregroundColor, ), ), ], ); } return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) ...[ icon!, const SizedBox(width: 8), ], Text( text, style: theme.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w600, color: foregroundColor, ), ), ], ); } String _getDisplayText() { if (widget.isLoading) { return widget.loadingText ?? 'Loading...'; } if (widget.isSuccess) { return widget.successText ?? 'Success!'; } return widget.text; } Widget? _getDisplayIcon() { if (widget.isLoading || widget.isSuccess) { return null; // Loading spinner or checkmark will be shown } return widget.icon; } Color _getBackgroundColor(_ButtonColors colors) { if (widget.isSuccess) { return Theme.of(context).colorScheme.primary; } return colors.background; } Color _getForegroundColor(_ButtonColors colors) { if (widget.isSuccess) { return Theme.of(context).colorScheme.onPrimary; } return colors.foreground; } String _getAccessibilityLabel() { if (widget.isLoading) { return '${widget.loadingText ?? "Loading"} button'; } if (widget.isSuccess) { return '${widget.successText ?? "Success"} completed'; } return widget.text; } String _getAccessibilityHint() { if (widget.isLoading) { return 'Please wait while operation completes'; } if (widget.isSuccess) { return 'Operation completed successfully'; } return 'Double tap to ${widget.text.toLowerCase()}'; } void _handlePressed() async { // Prevent double-tap within 500ms final now = DateTime.now(); if (_lastPressedTime != null && now.difference(_lastPressedTime!).inMilliseconds < 500) { return; } _lastPressedTime = now; // Provide haptic feedback if available try { HapticFeedback.lightImpact(); } catch (e) { // Haptic feedback not available on all platforms } // Animate press _animationController.forward().then((_) { _animationController.reverse(); }); // Announce for accessibility _announceForAccessibility('${widget.text} activated'); // Execute callback await widget.onPressed?.call(); // Auto-reset success state after duration if (widget.isSuccess) { Future.delayed(widget.successDuration, () { if (mounted) { // Success state reset should be handled by parent _announceForAccessibility('Success state completed'); } }); } } void _announceForAccessibility(String message) { SemanticsService.announce(message, TextDirection.ltr); } _ButtonColors _getColors(ThemeData theme) { switch (variant) { case AuthButtonVariant.primary: return _ButtonColors( background: theme.colorScheme.primary, foreground: theme.colorScheme.onPrimary, disabledBackground: theme.colorScheme.primary.withOpacity(0.12), disabledForeground: theme.colorScheme.onPrimary.withOpacity(0.38), ); case AuthButtonVariant.secondary: return _ButtonColors( background: theme.colorScheme.secondary, foreground: theme.colorScheme.onSecondary, disabledBackground: theme.colorScheme.secondary.withOpacity(0.12), disabledForeground: theme.colorScheme.onSecondary.withOpacity(0.38), ); case AuthButtonVariant.outline: return _ButtonColors( background: Colors.transparent, foreground: theme.colorScheme.primary, disabledBackground: Colors.transparent, disabledForeground: theme.colorScheme.primary.withOpacity(0.38), ); } } } enum AuthButtonVariant { primary, secondary, outline, } _ButtonColors _getColors(ThemeData theme) { switch (widget.variant) { case AuthButtonVariant.primary: return _ButtonColors( background: theme.colorScheme.primary, foreground: theme.colorScheme.onPrimary, disabledBackground: theme.colorScheme.primary.withOpacity(0.12), disabledForeground: theme.colorScheme.onPrimary.withOpacity(0.38), ); case AuthButtonVariant.secondary: return _ButtonColors( background: theme.colorScheme.secondary, foreground: theme.colorScheme.onSecondary, disabledBackground: theme.colorScheme.secondary.withOpacity(0.12), disabledForeground: theme.colorScheme.onSecondary.withOpacity(0.38), ); case AuthButtonVariant.outline: return _ButtonColors( background: Colors.transparent, foreground: theme.colorScheme.primary, disabledBackground: Colors.transparent, disabledForeground: theme.colorScheme.primary.withOpacity(0.38), ); } } } class _ButtonColors { final Color background; final Color foreground; final Color disabledBackground; final Color disabledForeground; const _ButtonColors({ required this.background, required this.foreground, required this.disabledBackground, required this.disabledForeground, }); }