Skip to content

Commit

Permalink
[webview_flutter_wkwebview] Add javascript panel interface for wkwebv…
Browse files Browse the repository at this point in the history
…iew (flutter#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: flutter#5670
* The PR that contains all changes can be found at flutter#4704
  • Loading branch information
jsharp83 authored and arc-yong committed Jun 14, 2024
1 parent 5b68eed commit 6c5045b
Show file tree
Hide file tree
Showing 20 changed files with 872 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.11.0

* Adds support to show JavaScript dialog. See `PlatformWebViewController.setOnJavaScriptAlertDialog`, `PlatformWebViewController.setOnJavaScriptConfirmDialog` and `PlatformWebViewController.setOnJavaScriptTextInputDialog`.

## 3.10.3

* Adds a check that throws an `ArgumentError` when `WebKitWebViewController.addJavaScriptChannel`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,98 @@ Future<void> main() async {
},
);

testWidgets('can receive JavaScript alert dialogs',
(WidgetTester tester) async {
final PlatformWebViewController controller = PlatformWebViewController(
const PlatformWebViewControllerCreationParams(),
);

final Completer<String> alertMessage = Completer<String>();
unawaited(controller.setOnJavaScriptAlertDialog(
(JavaScriptAlertDialogRequest request) async {
alertMessage.complete(request.message);
},
));

unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
unawaited(
controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
);

await tester.pumpWidget(Builder(
builder: (BuildContext context) {
return PlatformWebViewWidget(
PlatformWebViewWidgetCreationParams(controller: controller),
).build(context);
},
));

await controller.runJavaScript('alert("alert message")');
await expectLater(alertMessage.future, completion('alert message'));
});

testWidgets('can receive JavaScript confirm dialogs',
(WidgetTester tester) async {
final PlatformWebViewController controller = PlatformWebViewController(
const PlatformWebViewControllerCreationParams(),
);

final Completer<String> confirmMessage = Completer<String>();
unawaited(controller.setOnJavaScriptConfirmDialog(
(JavaScriptConfirmDialogRequest request) async {
confirmMessage.complete(request.message);
return true;
},
));

unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
unawaited(
controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
);

await tester.pumpWidget(Builder(
builder: (BuildContext context) {
return PlatformWebViewWidget(
PlatformWebViewWidgetCreationParams(controller: controller),
).build(context);
},
));

await controller.runJavaScript('confirm("confirm message")');
await expectLater(confirmMessage.future, completion('confirm message'));
});

testWidgets('can receive JavaScript prompt dialogs',
(WidgetTester tester) async {
final PlatformWebViewController controller = PlatformWebViewController(
const PlatformWebViewControllerCreationParams(),
);

unawaited(controller.setOnJavaScriptTextInputDialog(
(JavaScriptTextInputDialogRequest request) async {
return 'return message';
},
));

unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
unawaited(
controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
);

await tester.pumpWidget(Builder(
builder: (BuildContext context) {
return PlatformWebViewWidget(
PlatformWebViewWidgetCreationParams(controller: controller),
).build(context);
},
));

final Object promptResponse = await controller.runJavaScriptReturningResult(
'prompt("input message", "default text")',
);
expect(promptResponse, 'return message');
});

group('Logging', () {
testWidgets('can receive console log messages',
(WidgetTester tester) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,38 @@ const String kLogExamplePage = '''
</html>
''';

const String kAlertTestPage = '''
<!DOCTYPE html>
<html>
<head>
<script type = "text/javascript">
function showAlert(text) {
alert(text);
}
function showConfirm(text) {
var result = confirm(text);
alert(result);
}
function showPrompt(text, defaultText) {
var inputString = prompt('Enter input', 'Default text');
alert(inputString);
}
</script>
</head>
<body>
<p> Click the following button to see the effect </p>
<form>
<input type = "button" value = "Alert" onclick = "showAlert('Test Alert');" />
<input type = "button" value = "Confirm" onclick = "showConfirm('Test Confirm');" />
<input type = "button" value = "Prompt" onclick = "showPrompt('Test Prompt', 'Default Value');" />
</form>
</body>
</html>
''';

class WebViewExample extends StatefulWidget {
const WebViewExample({super.key, this.cookieManager});

Expand Down Expand Up @@ -297,6 +329,7 @@ enum MenuOptions {
setCookie,
logExample,
basicAuthentication,
javaScriptAlert,
}

class SampleMenu extends StatelessWidget {
Expand Down Expand Up @@ -348,6 +381,8 @@ class SampleMenu extends StatelessWidget {
_onLogExample();
case MenuOptions.basicAuthentication:
_promptForUrl(context);
case MenuOptions.javaScriptAlert:
_onJavaScriptAlertExample(context);
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
Expand Down Expand Up @@ -412,6 +447,10 @@ class SampleMenu extends StatelessWidget {
value: MenuOptions.basicAuthentication,
child: Text('Basic Authentication Example'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.javaScriptAlert,
child: Text('JavaScript Alert Example'),
),
],
);
}
Expand Down Expand Up @@ -536,6 +575,28 @@ class SampleMenu extends StatelessWidget {
return webViewController.loadHtmlString(kTransparentBackgroundPage);
}

Future<void> _onJavaScriptAlertExample(BuildContext context) {
webViewController.setOnJavaScriptAlertDialog(
(JavaScriptAlertDialogRequest request) async {
await _showAlert(context, request.message);
});

webViewController.setOnJavaScriptConfirmDialog(
(JavaScriptConfirmDialogRequest request) async {
final bool result = await _showConfirm(context, request.message);
return result;
});

webViewController.setOnJavaScriptTextInputDialog(
(JavaScriptTextInputDialogRequest request) async {
final String result =
await _showTextInput(context, request.message, request.defaultText);
return result;
});

return webViewController.loadHtmlString(kAlertTestPage);
}

Widget _getCookieList(String cookies) {
if (cookies == '""') {
return Container();
Expand Down Expand Up @@ -605,6 +666,65 @@ class SampleMenu extends StatelessWidget {
},
);
}

Future<void> _showAlert(BuildContext context, String message) async {
return showDialog<void>(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
content: Text(message),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(ctx).pop();
},
child: const Text('OK'))
],
);
});
}

Future<bool> _showConfirm(BuildContext context, String message) async {
return await showDialog<bool>(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
content: Text(message),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(ctx).pop(false);
},
child: const Text('Cancel')),
TextButton(
onPressed: () {
Navigator.of(ctx).pop(true);
},
child: const Text('OK')),
],
);
}) ??
false;
}

Future<String> _showTextInput(
BuildContext context, String message, String? defaultText) async {
return await showDialog<String>(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
content: Text(message),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(ctx).pop('Text test');
},
child: const Text('Enter')),
],
);
}) ??
'';
}
}

class NavigationControls extends StatelessWidget {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.6
webview_flutter_platform_interface: ^2.7.0
webview_flutter_platform_interface: ^2.9.0
webview_flutter_wkwebview:
# When depending on this package from a real application you should use:
# webview_flutter: ^x.y.z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData(

FWFNSUrlRequestData *FWFNSUrlRequestDataFromNativeNSURLRequest(NSURLRequest *request) {
return [FWFNSUrlRequestData
makeWithUrl:request.URL.absoluteString
makeWithUrl:request.URL.absoluteString == nil ? @"" : request.URL.absoluteString
httpMethod:request.HTTPMethod
httpBody:request.HTTPBody
? [FlutterStandardTypedData typedDataWithBytes:request.HTTPBody]
Expand All @@ -176,7 +176,9 @@ WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData(
}

FWFWKFrameInfoData *FWFWKFrameInfoDataFromNativeWKFrameInfo(WKFrameInfo *info) {
return [FWFWKFrameInfoData makeWithIsMainFrame:info.isMainFrame];
return [FWFWKFrameInfoData
makeWithIsMainFrame:info.isMainFrame
request:FWFNSUrlRequestDataFromNativeNSURLRequest(info.request)];
}

WKNavigationActionPolicy FWFNativeWKNavigationActionPolicyFromEnumData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,9 @@ typedef NS_ENUM(NSUInteger, FWFNSUrlCredentialPersistence) {
@interface FWFWKFrameInfoData : NSObject
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)makeWithIsMainFrame:(BOOL)isMainFrame;
+ (instancetype)makeWithIsMainFrame:(BOOL)isMainFrame request:(FWFNSUrlRequestData *)request;
@property(nonatomic, assign) BOOL isMainFrame;
@property(nonatomic, strong) FWFNSUrlRequestData *request;
@end

/// Mirror of NSError.
Expand Down Expand Up @@ -949,6 +950,27 @@ NSObject<FlutterMessageCodec> *FWFWKUIDelegateFlutterApiGetCodec(void);
(void (^)(
FWFWKPermissionDecisionData *_Nullable,
FlutterError *_Nullable))completion;
/// Callback to Dart function `WKUIDelegate.runJavaScriptAlertPanel`.
- (void)runJavaScriptAlertPanelForDelegateWithIdentifier:(NSInteger)identifier
message:(NSString *)message
frame:(FWFWKFrameInfoData *)frame
completion:
(void (^)(FlutterError *_Nullable))completion;
/// Callback to Dart function `WKUIDelegate.runJavaScriptConfirmPanel`.
- (void)runJavaScriptConfirmPanelForDelegateWithIdentifier:(NSInteger)identifier
message:(NSString *)message
frame:(FWFWKFrameInfoData *)frame
completion:
(void (^)(NSNumber *_Nullable,
FlutterError *_Nullable))completion;
/// Callback to Dart function `WKUIDelegate.runJavaScriptTextInputPanel`.
- (void)runJavaScriptTextInputPanelForDelegateWithIdentifier:(NSInteger)identifier
prompt:(NSString *)prompt
defaultText:(NSString *)defaultText
frame:(FWFWKFrameInfoData *)frame
completion:
(void (^)(NSString *_Nullable,
FlutterError *_Nullable))completion;
@end

/// The codec used by FWFWKHttpCookieStoreHostApi.
Expand Down
Loading

0 comments on commit 6c5045b

Please sign in to comment.