completed velocity integration

This commit is contained in:
2026-05-27 19:01:24 +02:00
parent fec46fb6e9
commit 6fdd3183fb
29 changed files with 2760 additions and 2293 deletions

View File

@@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:qpay/models/api_response.dart';
import 'package:qpay/screens/transaction_controller.dart';
import '../../http/http.dart';
@@ -53,7 +53,7 @@ class ConfirmController extends ChangeNotifier {
});
}
Future<Map<String, dynamic>?> doTransaction() async {
Future<ApiResponse<Map<String, dynamic>>> doTransaction() async {
try {
model.isLoading = true;
notifyListeners();
@@ -77,33 +77,30 @@ class ConfirmController extends ChangeNotifier {
dynamic response = workflowResponse['body'];
model.isLoading = false;
notifyListeners();
if (response['status'] != 'FAILED') {
model.status = response['status'];
transactionController.updateReceiptData(response);
model.isLoading = false;
notifyListeners();
return response;
return ApiResponse.success(Map<String, dynamic>.from(response));
} else {
model.status = response['status'];
model.errorMessage = response['errorMessage'];
_showErrorSnackBar(response['errorMessage']);
return ApiResponse.failure(response['errorMessage'] ?? "Transaction failed");
}
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
_showErrorSnackBar(
model.isLoading = false;
notifyListeners();
return ApiResponse.failure(
"Problem completing the transaction, please try again.",
);
}
model.isLoading = false;
notifyListeners();
return null;
}
Future<void> pollTransaction(String uid) async {
Future<ApiResponse<Map<String, dynamic>>> pollTransaction(String uid) async {
while (!model.isCancelled) {
// Check again after delay in case it was cancelled during the delay
if (model.isCancelled) break;
@@ -117,7 +114,7 @@ class ConfirmController extends ChangeNotifier {
transactionController.updateReceiptData(response);
model.isCancelled = true;
notifyListeners();
break;
return ApiResponse.success(Map<String, dynamic>.from(response));
}
} catch (e) {
logger.e(e);
@@ -130,6 +127,7 @@ class ConfirmController extends ChangeNotifier {
// Wait 5 seconds before the next poll
await Future.delayed(const Duration(seconds: 5));
}
return ApiResponse.failure("Polling cancelled");
}
@override

View File

@@ -40,15 +40,13 @@ class _ConfirmScreenState extends State<ConfirmScreen>
),
);
_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),
),
);
_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();
}
@@ -65,44 +63,59 @@ class _ConfirmScreenState extends State<ConfirmScreen>
if (controller.model.isLoading) return;
final response = await controller.doTransaction();
if (response!['status'] == 'FAILED') {
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){
// the one we use here depends on when mpgs embedded bug will be fixed
if (controller
.transactionController
.model
.selectedPaymentProcessor
.authType ==
"WEB") {
if (kIsWeb) {
context.push('/gateway-web');
// context.push('/gateway-redirect');
}else {
} else {
context.push('/gateway');
}
} else {
if (!mounted) return;
context.push('/poll');
}
}catch (e,s) {
} 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(),
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),
@@ -122,246 +135,124 @@ class _ConfirmScreenState extends State<ConfirmScreen>
key: _formKey,
child: Center(
child: LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
ResponsivePolicy.md.toString() :
constraints.maxWidth.toString()),
builder: (context, constraints) {
return SizedBox(
width: double.parse(
constraints.maxWidth > ResponsivePolicy.md
? ResponsivePolicy.md.toString()
: constraints.maxWidth.toString(),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.3),
),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.primary,
),
),
),
padding: EdgeInsets.only(
bottom: 4.0,
), // Adjust spacing
child: Text(
"PROVIDER DETAILS",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
...controller
// 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["additionalData"]
.map<Widget>((data) {
return Column(
children: [
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
data["name"] ?? "",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
data["value"] ?? "",
style: TextStyle(fontSize: 16),
),
],
),
],
);
})
.toList(),
const SizedBox(height: 30),
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.primary,
),
),
),
padding: EdgeInsets.only(
bottom: 4.0,
), // Adjust spacing
child: Text(
"TRANSACTIONS DETAILS",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
.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(),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
controller
.transactionController
.model
.confirmationData["amount"]
.toString(),
style: TextStyle(fontSize: 16),
),
],
if (controller
.transactionController
.model
.confirmationData["gatewayCharge"] !=
0)
_buildDetailRow(
icon: Icons.account_balance_outlined,
label: "Gateway Charge",
value: controller
.transactionController
.model
.confirmationData["gatewayCharge"]
.toString(),
),
const SizedBox(height: 10),
if (controller
.transactionController
.model
.confirmationData["charge"] !=
0)
Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Our Charge",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
controller
.transactionController
.model
.confirmationData["charge"]
.toString(),
style: TextStyle(fontSize: 16),
),
],
),
const SizedBox(height: 10),
],
),
if (controller
.transactionController
.model
.confirmationData["gatewayCharge"] !=
0)
Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Gateway Charge",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
controller
.transactionController
.model
.confirmationData["gatewayCharge"]
.toString(),
style: TextStyle(fontSize: 16),
),
],
),
const SizedBox(height: 10),
],
),
if (controller
.transactionController
.model
.confirmationData["tax"] !=
0)
Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Tax",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
controller
.transactionController
.model
.confirmationData["tax"]
.toString(),
style: TextStyle(fontSize: 16),
),
],
),
const SizedBox(height: 10),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Total Amount",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
controller
.transactionController
.model
.confirmationData["totalAmount"]
.toString(),
style: TextStyle(fontSize: 16),
),
],
if (controller
.transactionController
.model
.confirmationData["tax"] !=
0)
_buildDetailRow(
icon: Icons.receipt_outlined,
label: "Tax",
value: controller
.transactionController
.model
.confirmationData["tax"]
.toString(),
),
const SizedBox(height: 5),
Text(
"NB: Your payment provider may charge additional fees.",
style: TextStyle(
fontSize: 11,
color: Colors.red,
),
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),
@@ -378,7 +269,7 @@ class _ConfirmScreenState extends State<ConfirmScreen>
],
),
);
}
},
),
),
),
@@ -391,6 +282,91 @@ class _ConfirmScreenState extends State<ConfirmScreen>
);
}
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,