Files
velocity-pay-flutter/lib/screens/gateway/gateway_web_screen.dart
2025-11-25 21:26:15 +02:00

260 lines
8.8 KiB
Dart

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 + 300,
child: HtmlElementView.fromTagName(
tagName: 'div',
onElementCreated: (element) {
final divElement =
element as web.HTMLDivElement;
divElement.id = "embed-target";
setupAttachmentObserver(element);
},
),
),
],
),
),
),
),
);
},
);
},
),
);
}
}