import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/gateway/gateway_controller.dart'; import 'package:qpay/screens/transaction_controller.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/screens/receipt/receipt_controller.dart'; import 'package:share_plus/share_plus.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:widgets_to_image/widgets_to_image.dart'; class ReceiptScreen extends StatefulWidget { const ReceiptScreen({super.key}); @override State createState() => _ReceiptScreenState(); } class _ReceiptScreenState extends State with TickerProviderStateMixin { late TransactionController transactionController; late GatewayController gatewayController; late Animation _fadeAnimation; late Animation _slideAnimation; late AnimationController _controller; late final TabController _tabController; late ReceiptController receiptController; final _formKey = GlobalKey(); final controller = WidgetsToImageController(); @override void initState() { super.initState(); transactionController = Provider.of( context, listen: false, ); receiptController = ReceiptController(context); gatewayController = GatewayController(context); receiptController.getReceiptData( transactionController.model.receiptData?["id"], ); _tabController = TabController(length: 2, vsync: this); _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(); } Future _captureImage() async { final image = await controller.capture(); if (image != null) { final params = ShareParams( files: [ XFile.fromData(image.buffer.asUint8List(), mimeType: 'image/png'), ], fileNameOverrides: ['receipt.png'], ); SharePlus.instance.share(params); } } _retry() async { receiptController.setLoading(true); await receiptController.doIntegration( receiptController.model.receiptData?["id"], ); if (receiptController.model.status == "SUCCESS") { receiptController.getReceiptData( receiptController.model.receiptData?["id"], ); } receiptController.setLoading(false); } _check() async { receiptController.setLoading(true); await gatewayController.poll(receiptController.model.receiptData?["id"]); if (gatewayController.model.status == "SUCCESS") { receiptController.getReceiptData( transactionController.model.receiptData?["id"], ); if (transactionController.model.receiptData?["pollingStatus"]) { receiptController.setLoading(false); return; // no need to proceed if we've already polled successfully } await _retry(); } receiptController.setLoading(false); } @override void dispose() { // TODO: implement dispose super.dispose(); _controller.dispose(); _tabController.dispose(); receiptController.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: ListenableBuilder( listenable: receiptController, builder: (context, child) { return CustomScrollView( slivers: [ SliverAppBar( stretch: true, stretchTriggerOffset: 100.0, expandedHeight: 50.0, surfaceTintColor: Colors.transparent, floating: true, leading: context.canPop() ? IconButton( onPressed: () { context.pop(); }, icon: const Icon(Icons.arrow_back), ) : SizedBox.shrink(), title: Text( "Payment Result", style: TextStyle(color: Colors.black87), ), ), SliverToBoxAdapter( child: WidgetsToImage( controller: controller, child: Container( color: Colors.white, child: Padding( padding: const EdgeInsets.all(20.0), child: FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: Form( key: _formKey, child: LayoutBuilder( builder: (context, constraints) { return Center( child: SizedBox( width: constraints.maxWidth > ResponsivePolicy.md ? ResponsivePolicy.md.toDouble() : constraints.maxWidth, child: Column( children: [ Skeletonizer( enabled: receiptController.model.isLoading, child: Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Theme.of(context) .colorScheme .primary .withOpacity(0.1), Theme.of(context) .colorScheme .tertiary .withOpacity(0.05), Theme.of(context) .colorScheme .tertiary .withOpacity(0.15), ], stops: [0.0, 0.5, 1.0], ), border: Border.all( color: Theme.of(context) .colorScheme .primary .withOpacity(0.3), ), borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Theme.of(context) .colorScheme .primary .withOpacity(0.1), blurRadius: 10, spreadRadius: 1, offset: Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( transactionController .model .receiptData?["integrationStatus"] == 'SUCCESS' ? Icons.check_circle : transactionController .model .receiptData?["integrationStatus"] == 'PENDING' ? Icons.pending : Icons.cancel, size: 18, color: transactionController .model .receiptData?["integrationStatus"] == 'SUCCESS' ? Colors.green : transactionController .model .receiptData?["integrationStatus"] == 'PENDING' ? Colors.orange : Colors.red, ), SizedBox(width: 5), Text( receiptController .model .receiptData?["debitCurrency"] ?? "", style: TextStyle( fontSize: 26, fontWeight: FontWeight.bold, ), ), SizedBox(width: 5), Text( receiptController .model .receiptData?["amount"] .toStringAsFixed( 2, ) ?? "", style: TextStyle( fontSize: 26, ), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: 5), Text( receiptController .model .receiptData?["billName"] ?? "", style: TextStyle( fontSize: 16, ), ), Text( receiptController .model .receiptData?["createdAt"] != null ? DateFormat.yMMMd() .add_jm() .format( DateTime.parse( receiptController .model .receiptData?["createdAt"], ), ) : "", style: TextStyle( fontSize: 15, ), ), ], ), SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment .spaceEvenly, children: [ Column( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all( color: Theme.of( context, ) .colorScheme .primary .withOpacity( 0.3, ), width: 1, ), ), child: receiptController .model .isLoading ? Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation< Color >( Theme.of( context, ).colorScheme.primary, ), ), ), ) : IconButton( onPressed: () { receiptController .repeatTransaction( context, ); }, icon: Icon( Icons .repeat, color: Theme.of( context, ).colorScheme.primary, size: 24, ), ), ), SizedBox(height: 8), Text( "Repeat", style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), Column( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all( color: Theme.of( context, ) .colorScheme .primary .withOpacity( 0.3, ), width: 1, ), ), child: IconButton( onPressed: () { _captureImage(); }, icon: Icon( Icons.share, color: Theme.of( context, ) .colorScheme .primary, size: 24, ), ), ), SizedBox(height: 8), Text( "Share", style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), if (receiptController .model .receiptData?["integrationStatus"] != "SUCCESS" && receiptController .model .receiptData?["paymentStatus"] == "SUCCESS") Column( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( shape: BoxShape .circle, color: Colors.white, border: Border.all( color: Theme.of( context, ) .colorScheme .primary .withOpacity( 0.3, ), width: 1, ), ), child: IconButton( onPressed: () { _retry(); }, icon: Icon( Icons.redo, color: Theme.of( context, ).colorScheme.primary, size: 24, ), ), ), SizedBox(height: 8), Text( "Retry", style: TextStyle( fontSize: 14, fontWeight: FontWeight .w500, ), ), ], ), if (receiptController .model .receiptData?["pollingStatus"] != "SUCCESS") Column( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( shape: BoxShape .circle, color: Colors.white, border: Border.all( color: Theme.of( context, ) .colorScheme .primary .withOpacity( 0.3, ), width: 1, ), ), child: IconButton( onPressed: () { _check(); }, icon: Icon( Icons .price_check, color: Theme.of( context, ).colorScheme.primary, size: 24, ), ), ), SizedBox(height: 8), Text( "Check status", style: TextStyle( fontSize: 14, fontWeight: FontWeight .w500, ), ), ], ), ], ), ], ), ), ), SizedBox(height: 20), Column( children: [ Container( decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(12), ), child: TabBar( controller: _tabController, indicator: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Theme.of( context, ).colorScheme.primary, ), indicatorPadding: EdgeInsets.all(5), indicatorSize: TabBarIndicatorSize.tab, labelColor: Colors.white, unselectedLabelColor: Colors.grey[600], dividerColor: Colors.transparent, tabs: [ Tab( child: Text( "Provider Details", style: TextStyle( fontWeight: FontWeight.bold, ), ), ), Tab( child: Text( "Transaction Details", style: TextStyle( fontWeight: FontWeight.bold, ), ), ), ], ), ), SizedBox( height: MediaQuery.of( context, ).size.height * 0.5, child: TabBarView( controller: _tabController, children: [ Padding( padding: const EdgeInsets.all( 10.0, ), child: Column( children: [ Skeletonizer( enabled: receiptController .model .isLoading, child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Status", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( transactionController .model .receiptData?["integrationStatus"] ?? "", style: TextStyle( fontSize: 16, ), ), ], ), ), if (transactionController .model .receiptData?["errorMessage"] != null && transactionController .model .receiptData?["errorMessage"] != "") Column( children: [ const SizedBox( height: 10, ), Skeletonizer( enabled: receiptController .model .isLoading, child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Message", style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), Expanded( child: Text( transactionController.model.receiptData?["errorMessage"] ?? "", style: TextStyle( fontSize: 16, ), textAlign: TextAlign.end, softWrap: true, ), ), ], ), ), ], ), if (receiptController .model .receiptData?["integrationStatus"] != "SUCCESS" && receiptController .model .receiptData?["paymentStatus"] == "SUCCESS") Column( children: [ const SizedBox( height: 10, ), Skeletonizer( enabled: receiptController .model .isLoading, child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Additional action", style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), Expanded( child: Text( "Billing account update failed, please tap on Retry to try again", style: TextStyle( fontSize: 16, color: Theme.of( context, ).colorScheme.primary, ), textAlign: TextAlign.end, softWrap: true, ), ), ], ), ), ], ), SizedBox(height: 10), Skeletonizer( enabled: receiptController .model .isLoading, child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Debit Reference", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( transactionController .model .receiptData?["debitRef"] ?? "", style: TextStyle( fontSize: 16, ), ), ], ), ), const SizedBox( height: 10, ), Skeletonizer( enabled: receiptController .model .isLoading, child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "From", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( receiptController .model .receiptData?["debitPhone"] ?? "", style: TextStyle( fontSize: 16, ), ), ], ), ), const SizedBox( height: 10, ), Skeletonizer( enabled: receiptController .model .isLoading, child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "To", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( receiptController .model .receiptData?["creditAccount"] ?? "", style: TextStyle( fontSize: 16, ), ), ], ), ), const SizedBox( height: 10, ), Skeletonizer( enabled: receiptController .model .isLoading, child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Processor", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( receiptController .model .receiptData?["paymentProcessorName"] ?? "", style: TextStyle( fontSize: 16, ), ), ], ), ), const SizedBox( height: 10, ), Divider( color: Colors.grey[300], thickness: 1, ), const SizedBox( height: 10, ), if (receiptController .model .receiptData?["additionalData"] == null) Skeletonizer( enabled: receiptController .model .isLoading, child: Column( children: [ SizedBox( height: 20, ), Center( child: Text( "No details found", ), ), ], ), ), if (receiptController .model .receiptData?["additionalData"] != null) ...receiptController.model.receiptData?["additionalData"].map< Widget >((data) { return Skeletonizer( enabled: receiptController .model .isLoading, child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( data["name"] ?? "", style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( data["value"] ?? "", style: TextStyle( fontSize: 16, ), ), ], ), const SizedBox( height: 10, ), ], ), ); }).toList(), ], ), ), Padding( padding: const EdgeInsets.all( 10.0, ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Amount", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( transactionController .model .receiptData?["amount"] .toStringAsFixed( 2, ) ?? "0.00", style: TextStyle( fontSize: 16, ), ), ], ), const SizedBox( height: 10, ), if (transactionController .model .receiptData?["charge"] != 0) Column( children: [ Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Our Charge", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( transactionController .model .receiptData?["charge"] .toStringAsFixed( 2, ) ?? "0.00", style: TextStyle( fontSize: 16, ), ), ], ), const SizedBox( height: 10, ), ], ), if (transactionController .model .receiptData?["gatewayCharge"] != 0) Column( children: [ Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Gateway Charge", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( transactionController .model .receiptData?["gatewayCharge"] .toStringAsFixed( 2, ) ?? "0.00", style: TextStyle( fontSize: 16, ), ), ], ), const SizedBox( height: 10, ), ], ), if (transactionController .model .receiptData?["tax"] != 0) Column( children: [ Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Tax", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( transactionController .model .receiptData?["tax"] .toStringAsFixed( 2, ) ?? "0.00", style: TextStyle( fontSize: 16, ), ), ], ), const SizedBox( height: 10, ), ], ), Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Text( "Total Amount", style: TextStyle( fontSize: 16, fontWeight: FontWeight .bold, ), ), Text( transactionController .model .receiptData?["totalAmount"] .toStringAsFixed( 2, ) ?? "0.00", style: TextStyle( fontSize: 16, ), ), ], ), const SizedBox( height: 20, ), ], ), ), ], ), ), ], ), ], ), ), ); }, ), ), ), ), ), ), ), ), ], ); }, ), ); } }