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 createState() => _GatewayWebScreenState(); } class _GatewayWebScreenState extends State { 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 { await ensureCheckoutScriptLoaded(); 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('/poll'); }); } } @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) { 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); }, ), ), ], ), ), ), ), ); }, ); }, ), ); } }