import 'dart:math'; import 'package:flutter/material.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:skeletonizer/skeletonizer.dart'; class StepLoadingWidget extends StatefulWidget { final int currentStep; // 1 or 2 final String title; final String subtitle; final String status; // '', 'SUCCESS', 'FAILED' final bool isLoading; final String? errorMessage; final VoidCallback? onRetry; const StepLoadingWidget({ super.key, required this.currentStep, required this.title, required this.subtitle, required this.status, required this.isLoading, this.errorMessage, this.onRetry, }); @override State createState() => _StepLoadingWidgetState(); } class _StepLoadingWidgetState extends State with TickerProviderStateMixin { late AnimationController _pulseController; late Animation _pulseAnimation; late AnimationController _spinController; @override void initState() { super.initState(); // Pulse animation for the outer ring _pulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(reverse: true); _pulseAnimation = Tween(begin: 0.85, end: 1.15).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); // Spin animation for the arc _spinController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..repeat(); } @override void dispose() { _pulseController.dispose(); _spinController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final primaryColor = theme.colorScheme.primary; return Container( padding: const EdgeInsets.all(50), child: Center( child: LayoutBuilder( builder: (context, constraints) { final maxWidth = constraints.maxWidth > ResponsivePolicy.md ? ResponsivePolicy.md.toDouble() : constraints.maxWidth; return SizedBox( width: maxWidth, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 60), // Step indicator _StepIndicator( currentStep: widget.currentStep, status: widget.status, primaryColor: primaryColor, ), const SizedBox(height: 60), // Animated loader if (widget.status != 'FAILED' && widget.status != 'SUCCESS') AnimatedBuilder( animation: Listenable.merge([ _pulseController, _spinController, ]), builder: (context, child) { return SizedBox( width: 80, height: 80, child: CustomPaint( painter: _LoadingPainter( pulseValue: _pulseAnimation.value, spinValue: _spinController.value, color: primaryColor, ), ), ); }, ), if (widget.status == 'SUCCESS') _buildCheckIcon(primaryColor), if (widget.status == 'FAILED') _buildErrorIcon(theme), const SizedBox(height: 40), // Title Text( widget.title, style: TextStyle( fontSize: 22, fontWeight: FontWeight.w600, color: theme.colorScheme.tertiary, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), // Subtitle Text( widget.subtitle, style: TextStyle( fontSize: 16, color: Colors.grey.shade600, height: 1.4, ), textAlign: TextAlign.center, ), const SizedBox(height: 50), // Error state with retry if (widget.status == 'FAILED') Column( children: [ Text( widget.errorMessage ?? 'Something went wrong', style: TextStyle( fontSize: 16, color: theme.colorScheme.error, ), textAlign: TextAlign.center, ), const SizedBox(height: 24), Skeletonizer( enabled: widget.isLoading, child: OutlinedButton.icon( icon: const Icon(Icons.refresh, size: 20), label: const Text('Retry'), onPressed: widget.onRetry, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 14, ), side: BorderSide(color: primaryColor, width: 1.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ], ), ); }, ), ), ); } Widget _buildCheckIcon(Color primaryColor) { return Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, color: primaryColor.withValues(alpha: 0.1), ), child: Icon(Icons.check_circle_rounded, size: 80, color: primaryColor), ); } Widget _buildErrorIcon(ThemeData theme) { return Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, color: theme.colorScheme.error.withValues(alpha: 0.1), ), child: Icon( Icons.error_outline_rounded, size: 80, color: theme.colorScheme.error, ), ); } } class _StepIndicator extends StatelessWidget { final int currentStep; final String status; final Color primaryColor; const _StepIndicator({ required this.currentStep, required this.status, required this.primaryColor, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final isSuccess = status == 'SUCCESS'; return Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildStep( number: '1', label: 'Verify Payment', isActive: currentStep == 1, isCompleted: currentStep > 1 || (currentStep == 1 && isSuccess), theme: theme, ), Container( width: 60, height: 2, margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: currentStep >= 2 ? primaryColor : Colors.grey.shade300, borderRadius: BorderRadius.circular(1), ), ), _buildStep( number: '2', label: 'Update Account', isActive: currentStep == 2, isCompleted: currentStep == 2 && isSuccess, theme: theme, ), ], ), ); } Widget _buildStep({ required String number, required String label, required bool isActive, required bool isCompleted, required ThemeData theme, }) { final Color circleColor; final Widget circleChild; if (isCompleted) { circleColor = theme.colorScheme.primary; circleChild = const Icon(Icons.check, size: 16, color: Colors.white); } else if (isActive) { circleColor = theme.colorScheme.primary; circleChild = Text( number, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ); } else { circleColor = Colors.grey.shade300; circleChild = Text( number, style: TextStyle( color: Colors.grey.shade600, fontWeight: FontWeight.bold, fontSize: 14, ), ); } return Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 32, height: 32, decoration: BoxDecoration(shape: BoxShape.circle, color: circleColor), child: Center(child: circleChild), ), const SizedBox(height: 6), Text( label, style: TextStyle( fontSize: 12, fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, color: isActive ? theme.colorScheme.tertiary : Colors.grey.shade500, ), ), ], ); } } class _LoadingPainter extends CustomPainter { final double pulseValue; final double spinValue; final Color color; _LoadingPainter({ required this.pulseValue, required this.spinValue, required this.color, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final baseRadius = size.width / 2 - 8; // Draw pulsing outer ring final pulseRadius = baseRadius * pulseValue; final pulsePaint = Paint() ..color = color.withValues(alpha: 0.15) ..style = PaintingStyle.fill; canvas.drawCircle(center, pulseRadius, pulsePaint); // Draw ring outline final ringPaint = Paint() ..color = color.withValues(alpha: 0.25) ..style = PaintingStyle.stroke ..strokeWidth = 3; canvas.drawCircle(center, baseRadius, ringPaint); // Draw spinning arc final arcPaint = Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 3.5 ..strokeCap = StrokeCap.round; final arcStart = spinValue * 2 * pi; final arcSweep = pi * 1.2; canvas.drawArc( Rect.fromCircle(center: center, radius: baseRadius), arcStart, arcSweep, false, arcPaint, ); // Draw inner dot final dotPaint = Paint() ..color = color ..style = PaintingStyle.fill; canvas.drawCircle(center, 6, dotPaint); } @override bool shouldRepaint(_LoadingPainter oldDelegate) { return oldDelegate.pulseValue != pulseValue || oldDelegate.spinValue != spinValue; } }