Flutter - platformViewRegistry для iFrame, создающий несколько представлений, и setState(), блокирующий выполнение <script> в Iframe
Для обработки платежей в нашем веб-приложении Flutter мы используем форму, размещенную в элементе IFrame, чтобы браузер клиента мог напрямую подключаться к платежному шлюзу. При отправке формы шлюз возвращает токен, который мы используем для настройки учетной записи в нашей серверной части без необходимости прямого доступа или хранения какой-либо информации о карте. Код JavaScript для формы, включая кнопку «Отправить», находится в srcdoc для IFrameElement.
Когда клиент получает токен, мы используем window.parent.postMessage (с надлежащими проверками безопасности), чтобы передать его из IFrame в код Flutter, где его прослушивает подписка на поток. Когда прослушиватель получает токен, он переводит клиента на следующий этап процесса регистрации.
Я пытаюсь сделать так, чтобы щелчок «отправить» запускал событие, которое изменяет состояние виджета для отображения круга загрузки, а затем триггер получения токена переключает состояние обратно в нормальное состояние и продолжает процесс регистрации. Однако, когда я обнаруживаю «отправить» и вызываю setState(), кажется, что остальная часть процесса блокируется, и я получаю вечный круг загрузки.
ОБНОВЛЯТЬ
Мне удалось создать желаемый эффект в Javascript iFrame, но я все равно хотел бы знать:
- Почему описанный вызов Flutter для setState() не работает должным образом,
- Почему этот код, кажется, создает несколько представлений (о чем свидетельствует журнал «viewNum = 2»; это число продолжает увеличиваться при тестировании, если я не обновляю браузер с помощью Shift) и
- Как отменить/удалить множественное представление и избежать их создания.
import 'dart:async';
import "package:flutter/material.dart";
import 'package:project/theme/branding.dart';
import 'package:project/widgets/account/version_select_form.dart';
import "dart:ui" as ui;
import "package:universal_html/html.dart";
class CreditCardForm extends StatefulWidget {
CreditCardForm(this._formKey,)
final _formKey;
@override
State<CreditCardForm> createState() => _CreditCardFormState();
}
class _CreditCardFormState extends State<CreditCardForm> {
_CreditCardFormState() {
_registerIFrame();
}
late String formCode = getFormCode();
String? token;
late StreamSubscription tokenSub;
late StreamSubscription submitSub;
bool _isProcessing = false;
_registerIFrame() {
IFrameElement _billingInfo = IFrameElement();
_billingInfo.srcdoc = formCode;
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
"_billingInfo",
(int viewId) => _billingInfo,
);
var submitListener = (event) {
if (event.type == "submit") {
debugPrint("heard ${event.type} in credit card form"); //logs: heard submit in credit card form
submitSub.cancel();
// setState(() { //this setState() seems to block the token process from proceeding
// _isProcessing = true;
// });
} else {
debugPrint("ignored ${event.type}");
}
};
var tokenListener = (event) async {
tokenSub.cancel();
// setState(() {
// _isProcessing = false;
// });
debugPrint("event.type for token message = ${event.type}"); //logs: event.type for token message = message
if (event.origin != window.location.origin)
debugPrint('nice try!');
} else {
token = event.data;
debugPrint(token); //logs token
tokenSub.cancel();
Navigator.pop(context);
showDialog(
context: context,
builder: (context) =>
Dialog(child: VersionSelectForm(token: token!)),
);
}
}
};
tokenSub = window.onMessage.listen(tokenListener);
submitSub = window.onSubmit.listen(submitListener);
Widget build(BuildContext context) {
return Column(
children: [
Form(
key: widget._formKey,
child: SizedBox(
width: 550,
height: 250,
child: HtmlElementView(
key: UniqueKey(),
viewType: "_billingInfo",
onPlatformViewCreated: (int viewNum) {
debugPrint('viewNum: $viewNum'); //logs: viewNum: 2 (I don't know why it's not 0)
},
),
),
),
_isProcessing
? Center(
child: CircularProgressIndicator(
color: BrandColors.orange,
),
)
: Container()
],
);
}
}
getFormCode() {
return """
<!DOCTYPE html>
<html>
<head>
<script src="https://<script src>"></script>
</head>
<style>
// styling info
</style>
<body>
<form class="host-form" id="billing-form">
//a bunch of html form elements
<div class="form-field submit-row">
<button id="submit-button" type="submit">CONTINUE</button>
</div>
</form>
</body>
<script>
var billingSite = new BillingSite();
billingSite.load({
selector: "billing-form",
publicKey: "**********",
type: "card",
serverHost: "https://our-store.billing-site.com",
});
//Without this vvv the submit listener above in _registerIFrame() does not respond,
//even though the next event listener is also for 'submit'
document.querySelector("#billing-form").addEventListener("submit", function(event) {
event.preventDefault();
console.log("event.type: "+event.type) //logs: event.type: submit
window.parent.dispatchEvent(new Event('submit'))
}
)
document.querySelector("#billing-form").addEventListener("submit", function(event) {
var form = this;
event.preventDefault();
billingSite.token(
form,
function success(token) {
window.parent.postMessage(token, window.parent.location.origin);
},
function error(err) {
console.log("token ERROR - err: ", err);
}
);
});
</script>
</html>
""";
}
1 ответ
Оказывается, если дать представлению другое имя, оно сможет отображать разные заголовки. То есть отправьте viewName в качестве параметра и используйте его для регистрации viewFactory и отображения viewType:
ui.platformViewRegistry.registerViewFactory(
viewName, //"newPaymentInfo" or "updatePaymentInfo" for example
...
Убедитесь, что HtmlElementView отражает имя в свойстве viewType:
return SizedBox(
height: widget.screenHeight,
width: screenWidth,
child: HtmlElementView(
key: ValueKey("paymentFormKey"),
viewType: viewName, //again, "newPaymentInfo" or "updatePaymentInfo" for example
onPlatformViewCreated: (int viewNum) {
debugPrint('viewNum: $viewNum'); //This usually logs '2' the first time and increments from there
},
));
}
Никаких других частей кода менять не пришлось (кроме принятия viewName в качестве параметра).