completed velocity integration
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
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 {
|
||||
@@ -17,12 +13,11 @@ class GatewayWebScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GatewayWebScreenState extends State<GatewayWebScreen> {
|
||||
late WebViewController _webViewController;
|
||||
late GatewayController controller;
|
||||
late String url;
|
||||
String? sessionID;
|
||||
|
||||
bool integrationStarted = false;
|
||||
bool redirecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -32,115 +27,40 @@ class _GatewayWebScreenState extends State<GatewayWebScreen> {
|
||||
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);
|
||||
void _redirectToGateway() {
|
||||
if (redirecting) return;
|
||||
|
||||
// Disconnect the observer once the action is done
|
||||
(observer as web.ResizeObserver).disconnect();
|
||||
}.toJS,
|
||||
setState(() {
|
||||
redirecting = true;
|
||||
});
|
||||
|
||||
bool simulate = bool.parse(
|
||||
dotenv.env['SIMULATE_PAYMENT_SUCCESS'] ?? 'false',
|
||||
);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
if (simulate) {
|
||||
// In simulation mode, just go to polling
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/poll');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the transaction ID to localStorage so index.html can redirect
|
||||
// the user back to /poll/:id when the gateway returns to this app.
|
||||
final transactionId =
|
||||
controller.transactionController.model.confirmationData['id'];
|
||||
if (transactionId != null) {
|
||||
web.window.localStorage.setItem(
|
||||
'pendingTransactionId',
|
||||
transactionId.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate the current browser tab to the payment gateway URL.
|
||||
// After payment, the gateway will redirect back to this app's host.
|
||||
// index.html then reads the saved transaction ID and navigates to
|
||||
// /poll/:id so the app can immediately start polling for the result.
|
||||
web.window.location.href = url;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -161,91 +81,98 @@ class _GatewayWebScreenState extends State<GatewayWebScreen> {
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, child) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: ResponsivePolicy.md.toDouble(),
|
||||
return Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth.toDouble(),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Info icon
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Text(
|
||||
'You are about to leave this app to complete your payment on a secure payment gateway.',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
'After completing payment, you will be automatically redirected back to this app to see your transaction status.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Image.asset(
|
||||
'assets/velocity-animation.gif',
|
||||
width: 150,
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
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);
|
||||
},
|
||||
child: ElevatedButton(
|
||||
onPressed: redirecting
|
||||
? null
|
||||
: _redirectToGateway,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
redirecting
|
||||
? 'Redirecting...'
|
||||
: 'Proceed to Payment Gateway',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/poll');
|
||||
},
|
||||
child: const Text('Check transaction status'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user