import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/confirm/confirm_controller.dart'; import 'package:qpay/widgets/app_snack_bar.dart'; import 'package:skeletonizer/skeletonizer.dart'; class ConfirmScreen extends StatefulWidget { const ConfirmScreen({super.key}); @override State createState() => _ConfirmScreenState(); } class _ConfirmScreenState extends State with TickerProviderStateMixin { late ConfirmController controller; late Animation _fadeAnimation; late Animation _slideAnimation; late AnimationController _controller; final _formKey = GlobalKey(); @override void initState() { super.initState(); controller = ConfirmController(context); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.0, 0.5, curve: Curves.easeOut), ), ); _slideAnimation = Tween(begin: const Offset(0, 0.1), end: Offset.zero).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.0, 0.5, curve: Curves.easeOut), ), ); _controller.forward(); } @override void dispose() { _controller.dispose(); controller.dispose(); super.dispose(); } void _handlePayment() async { try { if (controller.model.isLoading) return; final response = await controller.doTransaction(); if (!response.isSuccess) { if (context.mounted) { AppSnackBar.showError( context, controller.model.errorMessage ?? response.error ?? "Transaction failed", ); } return; } if (!mounted) return; // Check if the selected payment processor is a wallet (e.g. Velocity) if (controller .transactionController .model .selectedPaymentProcessor .label == 'WALLET') { final otp = await _showOtpBottomSheet(); if (otp == null) return; // User cancelled or dismissed the bottom sheet if (!mounted) return; final transactionId = controller.transactionController.model.confirmationData['id']; final otpResponse = await controller.updateVelocityTransactionOtp( transactionId, {'verificationCode': otp}, ); if (!otpResponse.isSuccess) { if (context.mounted) { AppSnackBar.showError( context, controller.model.errorMessage ?? otpResponse.error ?? "OTP verification failed", ); } return; } if (!mounted) return; } // Proceed with normal flow after OTP step (or skip if not WALLET) if (controller .transactionController .model .selectedPaymentProcessor .authType == "WEB") { if (kIsWeb) { context.push('/gateway-web'); } else { context.push('/gateway'); } } else { if (!mounted) return; context.push('/poll'); } } catch (e, s) { print(e); print(s); } } Future _showOtpBottomSheet() async { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final otpController = TextEditingController(); final formKey = GlobalKey(); return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (sheetContext) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(sheetContext).viewInsets.bottom, ), child: Container( decoration: BoxDecoration( color: isDark ? const Color(0xFF1A1A2E) : Colors.white, borderRadius: const BorderRadius.vertical( top: Radius.circular(24), ), ), padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), child: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), ), Center( child: Container( width: 56, height: 56, decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( Icons.verified_user_rounded, size: 28, color: theme.colorScheme.primary, ), ), ), const SizedBox(height: 16), Text( 'OTP Verification', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: isDark ? Colors.white : Colors.black87, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'An OTP has been sent to your registered mobile number/email. Please enter it below to proceed.', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey.shade500, ), textAlign: TextAlign.center, ), const SizedBox(height: 20), TextFormField( controller: otpController, keyboardType: TextInputType.number, textAlign: TextAlign.center, maxLength: 6, style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, letterSpacing: 8, color: isDark ? Colors.white : Colors.black87, ), decoration: InputDecoration( counterText: '', hintText: '------', hintStyle: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, letterSpacing: 8, color: Colors.grey.shade400, ), filled: true, fillColor: isDark ? Colors.white.withValues(alpha: 0.06) : Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: BorderSide( color: theme.colorScheme.primary, width: 2, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: BorderSide(color: Colors.red.shade400), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Please enter the OTP'; } if (value.trim().length < 4) { return 'OTP must be at least 4 digits'; } return null; }, ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { if (formKey.currentState?.validate() ?? false) { Navigator.of( sheetContext, ).pop(otpController.text.trim()); } }, style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), elevation: 0, ), child: const Text( 'Submit', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, ), ), ), ), ], ), ), ), ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: context.canPop() ? IconButton( onPressed: () { context.pop(); }, icon: const Icon(Icons.arrow_back), ) : SizedBox.shrink(), title: const Text( 'Confirm Payment', style: TextStyle(color: Colors.black87), ), ), body: ListenableBuilder( listenable: controller, builder: (context, child) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(20.0), child: FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: Form( key: _formKey, child: Center( child: LayoutBuilder( builder: (context, constraints) { return SizedBox( width: double.parse( constraints.maxWidth > ResponsivePolicy.md ? ResponsivePolicy.md.toString() : constraints.maxWidth.toString(), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Provider Details Card _buildInfoCard( icon: Icons.business_outlined, title: "PROVIDER DETAILS", children: [ ...controller .transactionController .model .confirmationData["additionalData"] .map((data) { return _buildDetailRow( icon: Icons.info_outline, label: data["name"] ?? "", value: data["value"] ?? "", ); }) .toList(), ], ), const SizedBox(height: 20), // Transaction Details Card _buildInfoCard( icon: Icons.receipt_long_outlined, title: "TRANSACTION DETAILS", children: [ _buildDetailRow( icon: Icons.monetization_on_outlined, label: "Amount", value: controller .transactionController .model .confirmationData["amount"] .toString(), ), if (controller .transactionController .model .confirmationData["charge"] != 0) _buildDetailRow( icon: Icons.percent_outlined, label: "Our Charge", value: controller .transactionController .model .confirmationData["charge"] .toString(), ), if (controller .transactionController .model .confirmationData["gatewayCharge"] != 0) _buildDetailRow( icon: Icons.account_balance_outlined, label: "Gateway Charge", value: controller .transactionController .model .confirmationData["gatewayCharge"] .toString(), ), if (controller .transactionController .model .confirmationData["tax"] != 0) _buildDetailRow( icon: Icons.receipt_outlined, label: "Tax", value: controller .transactionController .model .confirmationData["tax"] .toString(), ), const Divider( height: 1, color: Colors.grey, ), const SizedBox(height: 12), _buildDetailRow( icon: Icons.summarize_outlined, label: "Total Amount", value: controller .transactionController .model .confirmationData["totalAmount"] .toString(), valueStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Theme.of( context, ).colorScheme.primary, ), ), ], ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.only(left: 4), child: Text( "NB: Your payment provider may charge additional fees.", style: TextStyle( fontSize: 11, color: Colors.red.shade600, ), ), ), const SizedBox(height: 20), _buildPaymentProcessorButton(), const SizedBox(height: 20), Center( child: OutlinedButton( child: Text('Cancel'), onPressed: () { context.go('/'); }, ), ), ], ), ); }, ), ), ), ), ), ), ); }, ), ); } Widget _buildInfoCard({ required IconData icon, required String title, required List children, }) { return Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200, width: 1), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( icon, size: 18, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Theme.of(context).colorScheme.primary, letterSpacing: 1.0, ), ), ], ), const SizedBox(height: 16), Divider(height: 1, color: Colors.grey.shade200), const SizedBox(height: 16), ...children, ], ), ), ); } Widget _buildDetailRow({ required IconData icon, required String label, required String value, TextStyle? valueStyle, }) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Icon(icon, size: 16, color: Colors.grey.shade500), const SizedBox(width: 10), Text( label, style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), const Spacer(), Text( value, style: valueStyle ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ), ); } Widget _buildPaymentProcessorButton() { return Skeletonizer( enabled: controller.model.isLoading, child: SlideTransition( position: _slideAnimation, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.black87.withAlpha(20)), borderRadius: BorderRadius.circular(12), gradient: LinearGradient( colors: [ Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), Theme.of(context).colorScheme.tertiary.withValues(alpha: 0.05), ], stops: const [0.3, 0.7], begin: Alignment.topRight, end: Alignment.bottomLeft, ), ), child: InkWell( customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), onTap: () async { _handlePayment(); }, child: ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Image( image: AssetImage( "assets/${controller.transactionController.model.selectedPaymentProcessor.image}", ), ), ), title: Text( controller .transactionController .model .selectedPaymentProcessor .name, style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( controller .transactionController .model .selectedPaymentProcessor .description, style: TextStyle(fontWeight: FontWeight.normal, fontSize: 10), ), trailing: Icon( Icons.chevron_right, color: Theme.of( context, ).colorScheme.secondary.withValues(alpha: 0.7), ), ), ), ), ), ); } }