completed first web iteration
This commit is contained in:
@@ -76,15 +76,22 @@ class GatewayController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> pollTransaction(String uid) async {
|
||||
// only poll for 5 minutes
|
||||
int max = 30;
|
||||
while (!model.isCancelled) {
|
||||
max++;
|
||||
// Check cancellation flag instead of true
|
||||
// Wait 5 seconds before the next poll
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
|
||||
// Check again after delay in case it was cancelled during the delay
|
||||
if (model.isCancelled) break;
|
||||
|
||||
await poll(uid);
|
||||
|
||||
if(max > 30) {
|
||||
model.isCancelled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -17,6 +15,7 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
late WebViewController _webViewController;
|
||||
late GatewayController controller;
|
||||
late String url;
|
||||
String? sessionID;
|
||||
|
||||
bool integrationStarted = false;
|
||||
|
||||
@@ -24,63 +23,71 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = GatewayController(context);
|
||||
// https://na.gateway.mastercard.com/checkout/pay/SESSION0002220827459I8059951J88?checkoutVersion=1.0.0
|
||||
|
||||
url = controller.transactionController.model.receiptData?['targetUrl'];
|
||||
_initWebView(url);
|
||||
|
||||
_initWebView();
|
||||
}
|
||||
|
||||
void _initWebView() async {
|
||||
_webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onProgress: (int progress) {
|
||||
// Update loading bar.
|
||||
},
|
||||
onPageStarted: (String url) {
|
||||
controller.startLoading();
|
||||
},
|
||||
onPageFinished: (String url) {
|
||||
controller.finishLoading();
|
||||
},
|
||||
onHttpError: (HttpResponseError error) {},
|
||||
onWebResourceError: (WebResourceError error) {},
|
||||
onUrlChange: (UrlChange url) async {
|
||||
print("URL Changed to ${url.url!}");
|
||||
|
||||
_navigateWhenDone(url.url!);
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
|
||||
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
||||
if(simulate) {
|
||||
sleep(Duration(seconds: 10));
|
||||
if (simulate) {
|
||||
await Future.delayed(Duration(seconds: 10));
|
||||
String targetUrl = url.replaceAll("/pay/", "/receipt/");
|
||||
_webViewController.loadRequest(Uri.parse(targetUrl));
|
||||
}
|
||||
}
|
||||
|
||||
void _initWebView(String url) {
|
||||
_webViewController =
|
||||
WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onProgress: (int progress) {
|
||||
// Update loading bar.
|
||||
},
|
||||
onPageStarted: (String url) {
|
||||
controller.startLoading();
|
||||
},
|
||||
onPageFinished: (String url) {
|
||||
controller.finishLoading();
|
||||
},
|
||||
onHttpError: (HttpResponseError error) {},
|
||||
onWebResourceError: (WebResourceError error) {},
|
||||
onUrlChange: (UrlChange url) async {
|
||||
print("URL Changed to ${url.url!}");
|
||||
void _navigateWhenDone(String url) {
|
||||
if (url.contains("/checkout/receipt/")) {
|
||||
if (integrationStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(url.url!.contains("/checkout/receipt/")){
|
||||
if(integrationStarted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Payment was successful, please wait while we process your order.",
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.black87,
|
||||
duration: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Payment was successful, please wait while we process your order."),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.black87,
|
||||
duration: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
integrationStarted = true;
|
||||
});
|
||||
|
||||
setState(() {
|
||||
integrationStarted = true;
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -94,15 +101,14 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Complete your payment'),
|
||||
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(),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: controller,
|
||||
@@ -112,9 +118,12 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
|
||||
return controller.model.isLoading
|
||||
? const Center(child: CircularProgressIndicator(strokeWidth: 5))
|
||||
: WebViewWidget(controller: _webViewController);
|
||||
: SizedBox.expand(
|
||||
child: WebViewWidget(controller: _webViewController),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
259
lib/screens/gateway/gateway_web_screen.dart
Normal file
259
lib/screens/gateway/gateway_web_screen.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/interop.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/gateway/gateway_controller.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
class GatewayWebScreen extends StatefulWidget {
|
||||
const GatewayWebScreen({super.key});
|
||||
|
||||
@override
|
||||
State<GatewayWebScreen> createState() => _GatewayWebScreenState();
|
||||
}
|
||||
|
||||
class _GatewayWebScreenState extends State<GatewayWebScreen> {
|
||||
late WebViewController _webViewController;
|
||||
late GatewayController controller;
|
||||
late String url;
|
||||
String? sessionID;
|
||||
|
||||
bool integrationStarted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = GatewayController(context);
|
||||
|
||||
url = controller.transactionController.model.receiptData?['targetUrl'];
|
||||
}
|
||||
|
||||
// A function to set up the ResizeObserver
|
||||
void setupAttachmentObserver(web.Element element) {
|
||||
final observer = web.ResizeObserver(
|
||||
(JSArray entries, JSAny observer) {
|
||||
// The element has been laid out (and thus attached to the DOM)
|
||||
onElementAttached(element);
|
||||
|
||||
// Disconnect the observer once the action is done
|
||||
(observer as web.ResizeObserver).disconnect();
|
||||
}.toJS,
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
}
|
||||
|
||||
// Called after `element` is attached to the DOM.
|
||||
void onElementAttached(web.Element element) {
|
||||
// Your code to execute after the element is in the DOM goes here.
|
||||
print('Element with ID ${element.id} is attached to the DOM.');
|
||||
// You can now safely query the DOM or call JavaScript functions that rely
|
||||
// on the element being present.
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
initIframeView();
|
||||
});
|
||||
}
|
||||
|
||||
void initIframeView() async {
|
||||
final uri = Uri.parse(url);
|
||||
// 2. The pathSegments property returns a list of the parts of the path.
|
||||
// For "checkout/pay/SESSION...", the segments are ["checkout", "pay", "SESSION..."].
|
||||
final pathSegments = uri.pathSegments;
|
||||
// 3. Check if there are any segments and return the last one,
|
||||
// which is the session ID in this URL structure.
|
||||
if (pathSegments.isNotEmpty) {
|
||||
sessionID = pathSegments.last;
|
||||
|
||||
configure(sessionID!);
|
||||
}
|
||||
showEmbeddedPage();
|
||||
|
||||
Future.delayed(const Duration(seconds: 10), () async {
|
||||
web.HTMLIFrameElement element = fetchIframeFromDom(
|
||||
"hc-comms-layer-iframe",
|
||||
)!;
|
||||
print(element);
|
||||
element.onload = (JSAny event) {
|
||||
// This is called every time a new page loads in the iframe
|
||||
print(event);
|
||||
|
||||
try {
|
||||
final newLocation = element.contentWindow?.location.href;
|
||||
print('Iframe navigated to: $newLocation');
|
||||
// Perform actions based on the new URL
|
||||
|
||||
_navigateWhenDone(newLocation!);
|
||||
} catch (e) {
|
||||
// Handle potential (though unlikely for same-origin) security errors
|
||||
print('Could not access iframe location: $e');
|
||||
}
|
||||
}.toJS;
|
||||
|
||||
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
||||
if (simulate) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
_navigateWhenDone("/checkout/receipt/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
web.HTMLIFrameElement? fetchIframeFromDom(String id) {
|
||||
final element = web.window.document.querySelector('#$id');
|
||||
if (element is web.HTMLIFrameElement) {
|
||||
print('Found iframe in DOM: ${element.id}');
|
||||
print(element.toString());
|
||||
return element;
|
||||
}
|
||||
|
||||
print('Could not find iframe with id: $id in the DOM.');
|
||||
return null;
|
||||
}
|
||||
|
||||
void _navigateWhenDone(String url) {
|
||||
if (url.contains("/checkout/receipt/")) {
|
||||
if (integrationStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Payment was successful, please wait while we process your order.",
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.black87,
|
||||
duration: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
integrationStarted = true;
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Complete your payment'),
|
||||
leading: context.canPop()
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, child) {
|
||||
if (controller.model.status == 'SUCCESS') {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: ResponsivePolicy.md.toDouble(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.black87.withAlpha(20),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Once done on this page, check transaction status',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.push('/poll');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Check status',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10,),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: constraints.maxHeight + 200,
|
||||
child: HtmlElementView.fromTagName(
|
||||
tagName: 'div',
|
||||
onElementCreated: (element) {
|
||||
final divElement =
|
||||
element as web.HTMLDivElement;
|
||||
divElement.id = "embed-target";
|
||||
setupAttachmentObserver(element);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user