import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.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(transactionController.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: Column( crossAxisAlignment: CrossAxisAlignment.start, 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), 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((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), ], ), ), ], ), ), ], ), ], ), ), ), ), ), ), ), ), ], ); }, ), ); } }