updating sequence of flows

This commit is contained in:
2025-11-02 19:49:25 +02:00
parent 087a9aa4bc
commit 895925d32f
9 changed files with 149 additions and 179 deletions

View File

@@ -1,6 +1,6 @@
; BASE_URL=https://peakapi.qantra.co.zw/api BASE_URL=https://peakapi.qantra.co.zw/api
; BASE_URL=http://192.168.100.26:6950/api ; BASE_URL=http://192.168.100.26:6950/api
BASE_URL=http://10.69.5.204:6950/api ; BASE_URL=http://10.69.5.204:6950/api
; BASE_URL=http://192.168.1.164:6950/api ; BASE_URL=http://192.168.1.164:6950/api
; BASE_URL=http://192.168.120.160:6950/api ; BASE_URL=http://192.168.120.160:6950/api
; BASE_URL=http://10.10.2.92:6950/api ; BASE_URL=http://10.10.2.92:6950/api

View File

@@ -110,9 +110,7 @@ class _MyAppState extends State<MyApp> {
), ),
GoRoute( GoRoute(
path: '/gateway', path: '/gateway',
builder: builder: (context, state) => const GatewayScreen(),
(context, state) =>
GatewayScreen(url: state.extra as String),
), ),
GoRoute( GoRoute(
path: '/receipt', path: '/receipt',

View File

@@ -84,14 +84,11 @@ class ConfirmController extends ChangeNotifier {
if (transactionController.model.selectedPaymentProcessor.authType == if (transactionController.model.selectedPaymentProcessor.authType ==
"WEB") { "WEB") {
if (context.mounted) { if (context.mounted) {
context.push( context.push('/gateway');
'/gateway',
extra: transactionController.model.receiptData?['targetUrl'],
);
} }
} else { } else {
await pollTransaction(transactionController.model.receiptData?['id']); await pollTransaction(transactionController.model.receiptData?['id']);
context.push('/receipt'); context.push('/integration');
} }
} else { } else {
model.status = response['status']; model.status = response['status'];

View File

@@ -60,15 +60,10 @@ class GatewayController extends ChangeNotifier {
dynamic response = workflowResponse['body']; dynamic response = workflowResponse['body'];
model.status = response['status']; model.status = response['status'];
transactionController.model.paymentStatus = response['status']; transactionController.model.paymentStatus = response['paymentStatus'];
transactionController.updateReceiptData(response);
if(model.status == 'PENDING') { if(model.status == 'SUCCESS') {
_showErrorSnackBar('Transaction status still pending'); transactionController.updateReceiptData(response);
}
if(model.status != 'FAILED') {
_showErrorSnackBar(response['errorMessage'].isEmpty ? 'Transaction failed' : response['errorMessage']);
} }
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {

View File

@@ -7,8 +7,7 @@ import 'package:qpay/screens/gateway/gateway_controller.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
class GatewayScreen extends StatefulWidget { class GatewayScreen extends StatefulWidget {
final String url; const GatewayScreen({super.key});
const GatewayScreen({super.key, required this.url});
@override @override
State<GatewayScreen> createState() => _GatewayScreenState(); State<GatewayScreen> createState() => _GatewayScreenState();
@@ -17,20 +16,22 @@ class GatewayScreen extends StatefulWidget {
class _GatewayScreenState extends State<GatewayScreen> { class _GatewayScreenState extends State<GatewayScreen> {
late WebViewController _webViewController; late WebViewController _webViewController;
late GatewayController controller; late GatewayController controller;
late String url;
bool integrationStarted = false; bool integrationStarted = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initWebView(widget.url);
controller = GatewayController(context); controller = GatewayController(context);
url = controller.transactionController.model.receiptData?['targetUrl'];
_initWebView(url);
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!); bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
if(simulate) { if(simulate) {
sleep(Duration(seconds: 10)); sleep(Duration(seconds: 10));
String url = widget.url.replaceAll("/pay/", "/receipt/"); String targetUrl = url.replaceAll("/pay/", "/receipt/");
_webViewController.loadRequest(Uri.parse(url)); _webViewController.loadRequest(Uri.parse(targetUrl));
} }
} }
@@ -71,7 +72,6 @@ class _GatewayScreenState extends State<GatewayScreen> {
setState(() { setState(() {
integrationStarted = true; integrationStarted = true;
}); });
await controller.poll(controller.transactionController.model.receiptData?['id']);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/integration'); context.go('/integration');

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qpay/http/http.dart'; import 'package:qpay/http/http.dart';
import 'package:qpay/screens/gateway/gateway_controller.dart';
import 'package:qpay/screens/transaction_controller.dart'; import 'package:qpay/screens/transaction_controller.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -18,6 +19,7 @@ class IntegrationController extends ChangeNotifier {
final IntegrationModel model = IntegrationModel(); final IntegrationModel model = IntegrationModel();
final Http http = Http(); final Http http = Http();
late TransactionController transactionController; late TransactionController transactionController;
late GatewayController gatewayController;
late SharedPreferences prefs; late SharedPreferences prefs;
BuildContext context; BuildContext context;
@@ -27,6 +29,8 @@ class IntegrationController extends ChangeNotifier {
context, context,
listen: false, listen: false,
); );
gatewayController = GatewayController(context);
} }
void _showErrorSnackBar(String? message) { void _showErrorSnackBar(String? message) {
@@ -58,6 +62,8 @@ class IntegrationController extends ChangeNotifier {
if (response['status'] == 'SUCCESS') { if (response['status'] == 'SUCCESS') {
model.status = response['status']; model.status = response['status'];
transactionController.updateReceiptData(response); transactionController.updateReceiptData(response);
gatewayController.poll(transactionController.model.confirmationData['id']);
} else { } else {
model.status = response['status']; model.status = response['status'];
model.errorMessage = response['errorMessage']; model.errorMessage = response['errorMessage'];

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qpay/screens/gateway/gateway_controller.dart';
import 'package:qpay/screens/transaction_controller.dart'; import 'package:qpay/screens/transaction_controller.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
@@ -17,7 +16,6 @@ class IntegrationScreen extends StatefulWidget {
class _IntegrationScreenState extends State<IntegrationScreen> { class _IntegrationScreenState extends State<IntegrationScreen> {
late IntegrationController controller; late IntegrationController controller;
late TransactionController transactionController; late TransactionController transactionController;
late GatewayController gatewayController;
bool integrating = false; bool integrating = false;
@@ -25,27 +23,12 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
void initState() { void initState() {
super.initState(); super.initState();
controller = IntegrationController(context); controller = IntegrationController(context);
gatewayController = GatewayController(context);
transactionController = Provider.of<TransactionController>( transactionController = Provider.of<TransactionController>(
context, context,
listen: false, listen: false,
); );
} controller.doIntegration();
@override
void didChangeDependencies() async {
super.didChangeDependencies();
if(integrating) return;
String status = Provider.of<TransactionController>(context).model.paymentStatus;
if(status == 'SUCCESS') {
setState(() {
integrating = true;
});
await controller.doIntegration();
}
} }
void _showErrorSnackBar(String? message) { void _showErrorSnackBar(String? message) {
@@ -74,91 +57,64 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
title: const Text('Completing your transaction'), title: const Text('Completing your transaction'),
), ),
body: ListenableBuilder( body: ListenableBuilder(
listenable: gatewayController, listenable: controller,
builder: (context, child) { builder: (context, child) {
return ListenableBuilder( if (controller.model.status == 'SUCCESS') {
listenable: controller, WidgetsBinding.instance.addPostFrameCallback((_) {
builder: (context, child) { context.go('/receipt');
if (controller.model.status == 'SUCCESS') { });
WidgetsBinding.instance.addPostFrameCallback((_) { }
context.go('/receipt');
});
}
return Container( return Container(
padding: EdgeInsets.all(50), padding: EdgeInsets.all(50),
child: Center( child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 75),
Column(
children: [ children: [
SizedBox(height: 75), Text(
'We\'ve successfully collected your funds. We are now '
if(transactionController.model.paymentStatus != 'SUCCESS') 'updating your billing account. This won\'t take long',
style: TextStyle(fontSize: 20),
textAlign: TextAlign.center,
),
Image.asset('assets/peak-animation.gif', width: 150),
SizedBox(
width: 50,
child: LinearProgressIndicator(),
),
SizedBox(height: 50),
if(controller.model.status == 'FAILED')
Column( Column(
children: [ children: [
Text( Text(
'We failed to check your payment status', 'We failed to update your billing account',
style: TextStyle(fontSize: 20), style: TextStyle(fontSize: 20),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: 50,), SizedBox(height: 20,),
Skeletonizer( Skeletonizer(
enabled: gatewayController.model.isLoading, enabled: controller.model.isLoading,
child: OutlinedButton( child: OutlinedButton(
child: Text('Check latest status'), child: Text('Retry'),
onPressed: () { onPressed: () {
gatewayController.poll(transactionController.model.receiptData?['id']); controller.doIntegration();
}, },
), ),
), ),
], ],
), ),
if(transactionController.model.paymentStatus == 'SUCCESS')
Column(
children: [
Text(
'We\'ve successfully collected your funds. We are now '
'updating your billing account. This won\'t take long',
style: TextStyle(fontSize: 20),
textAlign: TextAlign.center,
),
Image.asset('assets/peak-animation.gif', width: 150),
SizedBox(
width: 50,
child: LinearProgressIndicator(),
),
SizedBox(height: 50),
if(controller.model.status == 'FAILED')
Column(
children: [
Text(
'We failed to update your billing account',
style: TextStyle(fontSize: 20),
textAlign: TextAlign.center,
),
SizedBox(height: 20,),
Skeletonizer(
enabled: controller.model.isLoading,
child: OutlinedButton(
child: Text('Retry'),
onPressed: () {
controller.doIntegration();
},
),
),
],
),
],
),
], ],
), ),
), ],
); ),
}, ),
); );
} },
), ),
); );
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qpay/screens/gateway/gateway_controller.dart'; import 'package:qpay/screens/gateway/gateway_controller.dart';
import 'package:qpay/screens/transaction_controller.dart'; import 'package:qpay/screens/transaction_controller.dart';
@@ -6,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'package:qpay/screens/receipt/receipt_controller.dart'; import 'package:qpay/screens/receipt/receipt_controller.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:widgets_to_image/widgets_to_image.dart'; import 'package:widgets_to_image/widgets_to_image.dart';
class ReceiptScreen extends StatefulWidget { class ReceiptScreen extends StatefulWidget {
@@ -96,6 +98,11 @@ class _ReceiptScreenState extends State<ReceiptScreen>
receiptController.setLoading(true); receiptController.setLoading(true);
await gatewayController.poll(receiptController.model.receiptData?["id"]); await gatewayController.poll(receiptController.model.receiptData?["id"]);
if(gatewayController.model.status == "SUCCESS"){ 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(); await _retry();
} }
receiptController.setLoading(false); receiptController.setLoading(false);
@@ -253,21 +260,22 @@ class _ReceiptScreenState extends State<ReceiptScreen>
CrossAxisAlignment.center, CrossAxisAlignment.center,
children: [ children: [
SizedBox(height: 5), SizedBox(height: 5),
// receiptController
// .model
// .receiptData?["providerImage"] !=
// null
// ? Image.asset(
// 'assets/${receiptController.model.receiptData?["providerImage"] ?? ''}',
// width: 50,
// )
// : SizedBox.shrink(),
Text( Text(
receiptController receiptController
.model .model
.receiptData?["billName"] ?? .receiptData?["billName"] ??
"", "",
style: TextStyle(fontSize: 14), 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),
), ),
], ],
), ),
@@ -602,43 +610,47 @@ class _ReceiptScreenState extends State<ReceiptScreen>
), ),
], ],
), ),
const SizedBox(height: 10),
if(receiptController.model.receiptData?["integrationStatus"] != "SUCCESS" && if(receiptController.model.receiptData?["integrationStatus"] != "SUCCESS" &&
receiptController.model.receiptData?["paymentStatus"] == "SUCCESS") receiptController.model.receiptData?["paymentStatus"] == "SUCCESS")
Skeletonizer( Column(
enabled: children: [
receiptController const SizedBox(height: 10),
.model Skeletonizer(
.isLoading, enabled:
child: Row( receiptController
mainAxisAlignment: .model
MainAxisAlignment .isLoading,
.spaceBetween, child: Row(
children: [ mainAxisAlignment:
Text( MainAxisAlignment
"Additional action", .spaceBetween,
style: TextStyle( children: [
fontSize: 16, Text(
fontWeight: "Additional action",
FontWeight.bold,
),
),
Expanded(
child: Text("Billing account update failed, please tap on retry to try again",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Theme.of( fontWeight:
context, FontWeight.bold,
).colorScheme.primary,
), ),
textAlign: TextAlign.end,
softWrap: true,
), ),
), 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,), SizedBox(height: 10,),
Skeletonizer( Skeletonizer(
@@ -767,39 +779,44 @@ class _ReceiptScreenState extends State<ReceiptScreen>
.model .model
.receiptData?["additionalData"] .receiptData?["additionalData"]
.map<Widget>((data) { .map<Widget>((data) {
return Column( return Skeletonizer(
children: [ enabled: receiptController
Row( .model
mainAxisAlignment: .isLoading,
MainAxisAlignment child: Column(
.spaceBetween, children: [
children: [ Row(
Text( mainAxisAlignment:
data["name"] ?? MainAxisAlignment
"", .spaceBetween,
style: TextStyle( children: [
fontSize: Text(
16, data["name"] ??
fontWeight: "",
FontWeight style: TextStyle(
.bold, fontSize:
16,
fontWeight:
FontWeight
.bold,
),
), ),
), Text(
Text( data["value"] ??
data["value"] ?? "",
"", style:
style: TextStyle(
TextStyle( fontSize:
fontSize: 16,
16, ),
), ),
), ],
], ),
), const SizedBox(
const SizedBox( height: 10,
height: 10, ),
), ],
], ),
); );
}) })
.toList(), .toList(),

View File

@@ -41,6 +41,7 @@ class TransactionModel {
providerLabel: '', providerLabel: '',
); );
String paymentStatus = 'PENDING'; String paymentStatus = 'PENDING';
String pollStatus = 'PENDING';
String? errorMessage; String? errorMessage;
} }