To process payments in our Flutter web app we are using a form hosted in an IFrame element so the client's browser can connect directly with the payment gateway.
I want to vary the header display in the HtmlElementView according to a condition on the user's account.
When I load the app and display the form the first time, it displays the correct header. However, if I go back through the signup screens and try again with different account conditions, it will not re-render with the new header.
A possibly related issue is that the framework seems to be making a new HtmlElementView every time I go through the signup flow without restarting the app. I know this because I log the viewNum for the HtmlElementView, and it starts at 2 (for some reason) and increments from there. I suspect that the platform is displaying the initial view instead of the current one.
I would like to know:
- How to prevent the multiple views, and
- How to force the framework to refresh the HtmlElementView and display the new headerHtml. (As I mentioned, it logs correctly but will not display.)
Thank you!
import 'dart:async';
import "package:flutter/material.dart";
import 'package:fundy_designer_2/services/payment_service.dart';
import 'package:fundy_designer_2/services/user_service.dart';
import "package:fundy_designer_2/widgets/account/account_controller.dart";
import 'package:fundy_designer_2/widgets/account/subscription_preview.dart';
import "dart:ui" as ui;
import "package:universal_html/html.dart";
class CreditCardForm extends StatefulWidget {
CreditCardForm(
this.screenWidth, {
this.screenHeight = 500,
this.updating = false,
this.callback,
this.displayHeader = true,
this.headerHtml,
});
final screenWidth;
final screenHeight;
final updating;
final Function? callback;
final bool displayHeader;
final headerHtml;
@override
State<CreditCardForm> createState() =>
_CreditCardFormState(screenWidth, displayHeader, headerHtml);
}
class _CreditCardFormState extends State<CreditCardForm> {
_CreditCardFormState(this.screenWidth, this.displayHeader, this.headerHtml) {
ccStyle = screenWidth < 600
? "assets/credit_card_form/cc_style_narrow.css"
: "assets/credit_card_form/cc_style_wide.css";
displayHeader = displayHeader;
headerHtml = headerHtml;
debugPrint(headerHtml);
_registerIFrame();
}
late String formCode = getFormCode(
ccStyle,
displayHeader: displayHeader,
headerHtml: headerHtml,
);
String? token;
late final screenWidth;
late StreamSubscription tokenSub;
late String ccStyle;
late bool displayHeader;
late String headerHtml;
_registerIFrame() {
IFrameElement billingInfo = IFrameElement();
billingInfo.style.height = "100%";
billingInfo.style.width = "100%";
billingInfo.srcdoc = formCode;
billingInfo.style.border = "none";
billingInfo.srcdoc = formCode;
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
"billingInfo",
(int viewId) => billingInfo,
);
var tokenListener = (event) async {
if (event.origin != window.location.origin ||
event.data.substring(0, 4) != "***" ||
event.data.length != **) {
debugPrint('nice try!');
} else {
token = event.data;
AccountController.instance.token = token;
// debugPrint("token in tokenListener");
// debugPrint(token);
tokenSub.cancel();
if (widget.updating) {
String? oldLast4 = UserService.instance.last4;
widget.callback!(true, true, oldLast4);
await PmtService.instance.updateCard(token);
String? newLast4 = UserService.instance.last4;
widget.callback!(false, false, newLast4);
}
if (!widget.updating) {
await AccountController.instance.getSubscriptionPreview();
Navigator.pop(context);
showDialog(
context: context,
builder: (context) {
return Dialog(child: SubscriptionPreview());
});
}
}
};
tokenSub = window.onMessage.listen(tokenListener);
}
Widget build(BuildContext context) {
return SizedBox(
height: widget.screenHeight,
width: screenWidth,
child: HtmlElementView(
key: ValueKey("paymentFormKey"),
viewType: billingInfo,
onPlatformViewCreated: (int viewNum) {
debugPrint('viewNum: $viewNum'); //This usually logs '2' the first time and increments from there
},
));
}
}
getFormCode(styleUrl, {displayHeader, headerHtml}) {
String headerDisplay = displayHeader ? "block" : "none";
debugPrint(headerHtml); //This logs the correct html, even though the iframe doesn't display it
return """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel = "stylesheet" href = "$styleUrl">
<script src="https://scriptsource.com/latest/script.js"></script>
<body>
<form class="host-form" id="payment-form">
$headerHtml
<div class="form-field" id="card-info">
//A bunch of form fields
<div class="form-field submit-row">
<button id="submit-button" type="submit">CONTINUE</button>
<div class="loader" id="loading-circle"></div>
</div>
</form>
</body>
<script>
var paymentForm = new PaymentForm();
paymentForm.load({
selector: "payment-form",
publicKey: "****",
type: "card",
showLabels: false,
serverHost: "https://serverhost.com",
hideCardImage: true,
});
document.querySelector("#payment-form").addEventListener("submit", function(event) {
var form = this;
event.preventDefault();
document.getElementById("submit-button").style.display = "none";
document.getElementById("loading-circle").style.display = "block";
chargify.token(
form,
function success(token) {
console.log(token);
window.parent.postMessage(token, window.parent.location.origin);
},
function error(err) {
console.log("token ERROR - err: ", err);
}
);
});
</script>
</html>
""";
}
It turns out giving the view a different name allows it to display different headers. That is, send in a viewName as a parameter and use it to register the viewFactory and display the viewType:
Make sure the HtmlElementView reflects the name in the viewType property:
No other parts of the code had to change (except accepting a viewName as a parameter).