Files
velocity-pay-flutter/lib/screens/confirm/confirm_screen.dart

440 lines
16 KiB
Dart

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:skeletonizer/skeletonizer.dart';
class ConfirmScreen extends StatefulWidget {
const ConfirmScreen({super.key});
@override
State<ConfirmScreen> createState() => _ConfirmScreenState();
}
class _ConfirmScreenState extends State<ConfirmScreen>
with TickerProviderStateMixin {
late ConfirmController controller;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late AnimationController _controller;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
controller = ConfirmController(context);
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
),
);
_slideAnimation =
Tween<Offset>(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) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
controller.model.errorMessage ??
response.error ??
"Transaction failed",
),
behavior: SnackBarBehavior.floating,
width: 600,
backgroundColor: Colors.deepOrange,
),
);
}
return;
}
if (!mounted) return;
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);
}
}
@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<Widget>((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<Widget> 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),
),
),
),
),
),
);
}
}