Skip to content

Commit 2762c54

Browse files
authored
[webview_flutter_wkwebview] Add javascript panel interface for wkwebview (#5795)
* There are cases where Web calls System Popup with javascript on webview_flutter * At this time, the message comes in the WKUIDelegate part in iOS. * https://developer.apple.com/documentation/webkit/wkuidelegate/1537406-webview * https://developer.apple.com/documentation/webkit/wkuidelegate/1536489-webview * Related issue: flutter/flutter#30358 (comment) * Related Interface PR: #5670 * The PR that contains all changes can be found at #4704
1 parent a5dbf45 commit 2762c54

20 files changed

+872
-21
lines changed

packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.11.0
2+
3+
* Adds support to show JavaScript dialog. See `PlatformWebViewController.setOnJavaScriptAlertDialog`, `PlatformWebViewController.setOnJavaScriptConfirmDialog` and `PlatformWebViewController.setOnJavaScriptTextInputDialog`.
4+
15
## 3.10.3
26

37
* Adds a check that throws an `ArgumentError` when `WebKitWebViewController.addJavaScriptChannel`

packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,98 @@ Future<void> main() async {
13031303
},
13041304
);
13051305

1306+
testWidgets('can receive JavaScript alert dialogs',
1307+
(WidgetTester tester) async {
1308+
final PlatformWebViewController controller = PlatformWebViewController(
1309+
const PlatformWebViewControllerCreationParams(),
1310+
);
1311+
1312+
final Completer<String> alertMessage = Completer<String>();
1313+
unawaited(controller.setOnJavaScriptAlertDialog(
1314+
(JavaScriptAlertDialogRequest request) async {
1315+
alertMessage.complete(request.message);
1316+
},
1317+
));
1318+
1319+
unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
1320+
unawaited(
1321+
controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
1322+
);
1323+
1324+
await tester.pumpWidget(Builder(
1325+
builder: (BuildContext context) {
1326+
return PlatformWebViewWidget(
1327+
PlatformWebViewWidgetCreationParams(controller: controller),
1328+
).build(context);
1329+
},
1330+
));
1331+
1332+
await controller.runJavaScript('alert("alert message")');
1333+
await expectLater(alertMessage.future, completion('alert message'));
1334+
});
1335+
1336+
testWidgets('can receive JavaScript confirm dialogs',
1337+
(WidgetTester tester) async {
1338+
final PlatformWebViewController controller = PlatformWebViewController(
1339+
const PlatformWebViewControllerCreationParams(),
1340+
);
1341+
1342+
final Completer<String> confirmMessage = Completer<String>();
1343+
unawaited(controller.setOnJavaScriptConfirmDialog(
1344+
(JavaScriptConfirmDialogRequest request) async {
1345+
confirmMessage.complete(request.message);
1346+
return true;
1347+
},
1348+
));
1349+
1350+
unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
1351+
unawaited(
1352+
controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
1353+
);
1354+
1355+
await tester.pumpWidget(Builder(
1356+
builder: (BuildContext context) {
1357+
return PlatformWebViewWidget(
1358+
PlatformWebViewWidgetCreationParams(controller: controller),
1359+
).build(context);
1360+
},
1361+
));
1362+
1363+
await controller.runJavaScript('confirm("confirm message")');
1364+
await expectLater(confirmMessage.future, completion('confirm message'));
1365+
});
1366+
1367+
testWidgets('can receive JavaScript prompt dialogs',
1368+
(WidgetTester tester) async {
1369+
final PlatformWebViewController controller = PlatformWebViewController(
1370+
const PlatformWebViewControllerCreationParams(),
1371+
);
1372+
1373+
unawaited(controller.setOnJavaScriptTextInputDialog(
1374+
(JavaScriptTextInputDialogRequest request) async {
1375+
return 'return message';
1376+
},
1377+
));
1378+
1379+
unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
1380+
unawaited(
1381+
controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
1382+
);
1383+
1384+
await tester.pumpWidget(Builder(
1385+
builder: (BuildContext context) {
1386+
return PlatformWebViewWidget(
1387+
PlatformWebViewWidgetCreationParams(controller: controller),
1388+
).build(context);
1389+
},
1390+
));
1391+
1392+
final Object promptResponse = await controller.runJavaScriptReturningResult(
1393+
'prompt("input message", "default text")',
1394+
);
1395+
expect(promptResponse, 'return message');
1396+
});
1397+
13061398
group('Logging', () {
13071399
testWidgets('can receive console log messages',
13081400
(WidgetTester tester) async {

packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,38 @@ const String kLogExamplePage = '''
108108
</html>
109109
''';
110110

111+
const String kAlertTestPage = '''
112+
<!DOCTYPE html>
113+
<html>
114+
<head>
115+
<script type = "text/javascript">
116+
function showAlert(text) {
117+
alert(text);
118+
}
119+
120+
function showConfirm(text) {
121+
var result = confirm(text);
122+
alert(result);
123+
}
124+
125+
function showPrompt(text, defaultText) {
126+
var inputString = prompt('Enter input', 'Default text');
127+
alert(inputString);
128+
}
129+
</script>
130+
</head>
131+
132+
<body>
133+
<p> Click the following button to see the effect </p>
134+
<form>
135+
<input type = "button" value = "Alert" onclick = "showAlert('Test Alert');" />
136+
<input type = "button" value = "Confirm" onclick = "showConfirm('Test Confirm');" />
137+
<input type = "button" value = "Prompt" onclick = "showPrompt('Test Prompt', 'Default Value');" />
138+
</form>
139+
</body>
140+
</html>
141+
''';
142+
111143
class WebViewExample extends StatefulWidget {
112144
const WebViewExample({super.key, this.cookieManager});
113145

@@ -297,6 +329,7 @@ enum MenuOptions {
297329
setCookie,
298330
logExample,
299331
basicAuthentication,
332+
javaScriptAlert,
300333
}
301334

302335
class SampleMenu extends StatelessWidget {
@@ -348,6 +381,8 @@ class SampleMenu extends StatelessWidget {
348381
_onLogExample();
349382
case MenuOptions.basicAuthentication:
350383
_promptForUrl(context);
384+
case MenuOptions.javaScriptAlert:
385+
_onJavaScriptAlertExample(context);
351386
}
352387
},
353388
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
@@ -412,6 +447,10 @@ class SampleMenu extends StatelessWidget {
412447
value: MenuOptions.basicAuthentication,
413448
child: Text('Basic Authentication Example'),
414449
),
450+
const PopupMenuItem<MenuOptions>(
451+
value: MenuOptions.javaScriptAlert,
452+
child: Text('JavaScript Alert Example'),
453+
),
415454
],
416455
);
417456
}
@@ -536,6 +575,28 @@ class SampleMenu extends StatelessWidget {
536575
return webViewController.loadHtmlString(kTransparentBackgroundPage);
537576
}
538577

578+
Future<void> _onJavaScriptAlertExample(BuildContext context) {
579+
webViewController.setOnJavaScriptAlertDialog(
580+
(JavaScriptAlertDialogRequest request) async {
581+
await _showAlert(context, request.message);
582+
});
583+
584+
webViewController.setOnJavaScriptConfirmDialog(
585+
(JavaScriptConfirmDialogRequest request) async {
586+
final bool result = await _showConfirm(context, request.message);
587+
return result;
588+
});
589+
590+
webViewController.setOnJavaScriptTextInputDialog(
591+
(JavaScriptTextInputDialogRequest request) async {
592+
final String result =
593+
await _showTextInput(context, request.message, request.defaultText);
594+
return result;
595+
});
596+
597+
return webViewController.loadHtmlString(kAlertTestPage);
598+
}
599+
539600
Widget _getCookieList(String cookies) {
540601
if (cookies == '""') {
541602
return Container();
@@ -605,6 +666,65 @@ class SampleMenu extends StatelessWidget {
605666
},
606667
);
607668
}
669+
670+
Future<void> _showAlert(BuildContext context, String message) async {
671+
return showDialog<void>(
672+
context: context,
673+
builder: (BuildContext ctx) {
674+
return AlertDialog(
675+
content: Text(message),
676+
actions: <Widget>[
677+
TextButton(
678+
onPressed: () {
679+
Navigator.of(ctx).pop();
680+
},
681+
child: const Text('OK'))
682+
],
683+
);
684+
});
685+
}
686+
687+
Future<bool> _showConfirm(BuildContext context, String message) async {
688+
return await showDialog<bool>(
689+
context: context,
690+
builder: (BuildContext ctx) {
691+
return AlertDialog(
692+
content: Text(message),
693+
actions: <Widget>[
694+
TextButton(
695+
onPressed: () {
696+
Navigator.of(ctx).pop(false);
697+
},
698+
child: const Text('Cancel')),
699+
TextButton(
700+
onPressed: () {
701+
Navigator.of(ctx).pop(true);
702+
},
703+
child: const Text('OK')),
704+
],
705+
);
706+
}) ??
707+
false;
708+
}
709+
710+
Future<String> _showTextInput(
711+
BuildContext context, String message, String? defaultText) async {
712+
return await showDialog<String>(
713+
context: context,
714+
builder: (BuildContext ctx) {
715+
return AlertDialog(
716+
content: Text(message),
717+
actions: <Widget>[
718+
TextButton(
719+
onPressed: () {
720+
Navigator.of(ctx).pop('Text test');
721+
},
722+
child: const Text('Enter')),
723+
],
724+
);
725+
}) ??
726+
'';
727+
}
608728
}
609729

610730
class NavigationControls extends StatelessWidget {

packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dependencies:
1010
flutter:
1111
sdk: flutter
1212
path_provider: ^2.0.6
13-
webview_flutter_platform_interface: ^2.7.0
13+
webview_flutter_platform_interface: ^2.9.0
1414
webview_flutter_wkwebview:
1515
# When depending on this package from a real application you should use:
1616
# webview_flutter: ^x.y.z

packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData(
167167

168168
FWFNSUrlRequestData *FWFNSUrlRequestDataFromNativeNSURLRequest(NSURLRequest *request) {
169169
return [FWFNSUrlRequestData
170-
makeWithUrl:request.URL.absoluteString
170+
makeWithUrl:request.URL.absoluteString == nil ? @"" : request.URL.absoluteString
171171
httpMethod:request.HTTPMethod
172172
httpBody:request.HTTPBody
173173
? [FlutterStandardTypedData typedDataWithBytes:request.HTTPBody]
@@ -176,7 +176,9 @@ WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData(
176176
}
177177

178178
FWFWKFrameInfoData *FWFWKFrameInfoDataFromNativeWKFrameInfo(WKFrameInfo *info) {
179-
return [FWFWKFrameInfoData makeWithIsMainFrame:info.isMainFrame];
179+
return [FWFWKFrameInfoData
180+
makeWithIsMainFrame:info.isMainFrame
181+
request:FWFNSUrlRequestDataFromNativeNSURLRequest(info.request)];
180182
}
181183

182184
WKNavigationActionPolicy FWFNativeWKNavigationActionPolicyFromEnumData(

packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,9 @@ typedef NS_ENUM(NSUInteger, FWFNSUrlCredentialPersistence) {
464464
@interface FWFWKFrameInfoData : NSObject
465465
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
466466
- (instancetype)init NS_UNAVAILABLE;
467-
+ (instancetype)makeWithIsMainFrame:(BOOL)isMainFrame;
467+
+ (instancetype)makeWithIsMainFrame:(BOOL)isMainFrame request:(FWFNSUrlRequestData *)request;
468468
@property(nonatomic, assign) BOOL isMainFrame;
469+
@property(nonatomic, strong) FWFNSUrlRequestData *request;
469470
@end
470471

471472
/// Mirror of NSError.
@@ -949,6 +950,27 @@ NSObject<FlutterMessageCodec> *FWFWKUIDelegateFlutterApiGetCodec(void);
949950
(void (^)(
950951
FWFWKPermissionDecisionData *_Nullable,
951952
FlutterError *_Nullable))completion;
953+
/// Callback to Dart function `WKUIDelegate.runJavaScriptAlertPanel`.
954+
- (void)runJavaScriptAlertPanelForDelegateWithIdentifier:(NSInteger)identifier
955+
message:(NSString *)message
956+
frame:(FWFWKFrameInfoData *)frame
957+
completion:
958+
(void (^)(FlutterError *_Nullable))completion;
959+
/// Callback to Dart function `WKUIDelegate.runJavaScriptConfirmPanel`.
960+
- (void)runJavaScriptConfirmPanelForDelegateWithIdentifier:(NSInteger)identifier
961+
message:(NSString *)message
962+
frame:(FWFWKFrameInfoData *)frame
963+
completion:
964+
(void (^)(NSNumber *_Nullable,
965+
FlutterError *_Nullable))completion;
966+
/// Callback to Dart function `WKUIDelegate.runJavaScriptTextInputPanel`.
967+
- (void)runJavaScriptTextInputPanelForDelegateWithIdentifier:(NSInteger)identifier
968+
prompt:(NSString *)prompt
969+
defaultText:(NSString *)defaultText
970+
frame:(FWFWKFrameInfoData *)frame
971+
completion:
972+
(void (^)(NSString *_Nullable,
973+
FlutterError *_Nullable))completion;
952974
@end
953975

954976
/// The codec used by FWFWKHttpCookieStoreHostApi.

0 commit comments

Comments
 (0)