Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.1

* Fixes new lint warnings.
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,67 @@ 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 {
final dynamic result = 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')),
],
);
});

return result.runtimeType == bool && result as bool;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be easier to read as result is bool ? result : false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bparrishMines I just remove the result of dynamic type on aebf948

}

Future<String> _showTextInput(
BuildContext context, String message, String? defaultText) async {
final dynamic result = 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')),
],
);
});

return result.runtimeType == String ? result as String : '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one could be result is String ? result : ''.

Although I just realized that the prompt() dialog can technically return a null String and should provide support for this according to Android docs: https://developer.android.com/reference/android/webkit/WebChromeClient#onJsPrompt(android.webkit.WebView,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20android.webkit.JsPromptResult). I'll actually go ahead and create a PR to update the interface method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bparrishMines

I agree with your opinion. but one thing I worried that confirm() dialog also should be handle null case with this opinion.

I just thought that dismissing dialog without selection should be false in confirm and empty string in prompt.
But if we handle it as optional null, confirm dialog also return null when we just dismiss dialog.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For confirm(), I considered the same thing. However, I think canceling the operation is the same as returning false.

prompt() is slightly different because there could be scenario where a user might want to return an empty string. And this could be considered different from a user that wants to cancel the operation.

}
}

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 All @@ -31,4 +31,4 @@ flutter:
- assets/sample_audio.ogg
- assets/sample_video.mp4
- assets/www/index.html
- assets/www/styles/style.css
- assets/www/styles/style.css
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.length > 0 ? request.URL.absoluteString : @""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im a bit confused here - will absoluteString ever be nil? if so, may be less confusing to do nil check rather than length check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testWidgets('target _blank opens in same window',
(WidgetTester tester) async {
final Completer<WebViewController> controllerCompleter =
Completer<WebViewController>();
final Completer<void> pageLoaded = Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WebView(
key: GlobalKey(),
onWebViewCreated: (WebViewController controller) {
controllerCompleter.complete(controller);
},
javascriptMode: JavascriptMode.unrestricted,
onPageFinished: (String url) {
pageLoaded.complete(null);
},
),
),
);
final WebViewController controller = await controllerCompleter.future;
await controller.runJavascript('window.open("$primaryUrl", "_blank")');
await pageLoaded.future;
final String? currentUrl = await controller.currentUrl();
expect(currentUrl, primaryUrl);
});

@hellohuanlin
It can be nil with this test case.
I updated code to nil check.
Thanks

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
Original file line number Diff line number Diff line change
Expand Up @@ -609,14 +609,16 @@ - (NSArray *)toList {
@end

@implementation FWFWKFrameInfoData
+ (instancetype)makeWithIsMainFrame:(BOOL)isMainFrame {
+ (instancetype)makeWithIsMainFrame:(BOOL)isMainFrame request:(FWFNSUrlRequestData *)request {
FWFWKFrameInfoData *pigeonResult = [[FWFWKFrameInfoData alloc] init];
pigeonResult.isMainFrame = isMainFrame;
pigeonResult.request = request;
return pigeonResult;
}
+ (FWFWKFrameInfoData *)fromList:(NSArray *)list {
FWFWKFrameInfoData *pigeonResult = [[FWFWKFrameInfoData alloc] init];
pigeonResult.isMainFrame = [GetNullableObjectAtIndex(list, 0) boolValue];
pigeonResult.request = [FWFNSUrlRequestData nullableFromList:(GetNullableObjectAtIndex(list, 1))];
return pigeonResult;
}
+ (nullable FWFWKFrameInfoData *)nullableFromList:(NSArray *)list {
Expand All @@ -625,6 +627,7 @@ + (nullable FWFWKFrameInfoData *)nullableFromList:(NSArray *)list {
- (NSArray *)toList {
return @[
@(self.isMainFrame),
(self.request ? [self.request toList] : [NSNull null]),
];
}
@end
Expand Down Expand Up @@ -3098,6 +3101,99 @@ - (void)requestMediaCapturePermissionForDelegateWithIdentifier:(NSInteger)arg_id
}
}];
}
- (void)runJavaScriptAlertPanelForDelegateWithIdentifier:(NSInteger)arg_identifier
message:(NSString *)arg_message
frame:(FWFWKFrameInfoData *)arg_frame
completion:
(void (^)(FlutterError *_Nullable))completion {
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi."
@"runJavaScriptAlertPanel"
binaryMessenger:self.binaryMessenger
codec:FWFWKUIDelegateFlutterApiGetCodec()];
[channel
sendMessage:@[ @(arg_identifier), arg_message ?: [NSNull null], arg_frame ?: [NSNull null] ]
reply:^(NSArray<id> *reply) {
if (reply != nil) {
if (reply.count > 1) {
completion([FlutterError errorWithCode:reply[0]
message:reply[1]
details:reply[2]]);
} else {
completion(nil);
}
} else {
completion([FlutterError errorWithCode:@"channel-error"
message:@"Unable to establish connection on channel."
details:@""]);
}
}];
}
- (void)runJavaScriptConfirmPanelForDelegateWithIdentifier:(NSInteger)arg_identifier
message:(NSString *)arg_message
frame:(FWFWKFrameInfoData *)arg_frame
completion:
(void (^)(NSNumber *_Nullable,
FlutterError *_Nullable))completion {
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi."
@"runJavaScriptConfirmPanel"
binaryMessenger:self.binaryMessenger
codec:FWFWKUIDelegateFlutterApiGetCodec()];
[channel
sendMessage:@[ @(arg_identifier), arg_message ?: [NSNull null], arg_frame ?: [NSNull null] ]
reply:^(NSArray<id> *reply) {
if (reply != nil) {
if (reply.count > 1) {
completion(nil, [FlutterError errorWithCode:reply[0]
message:reply[1]
details:reply[2]]);
} else {
NSNumber *output = reply[0] == [NSNull null] ? nil : reply[0];
completion(output, nil);
}
} else {
completion(nil,
[FlutterError errorWithCode:@"channel-error"
message:@"Unable to establish connection on channel."
details:@""]);
}
}];
}
- (void)runJavaScriptTextInputPanelForDelegateWithIdentifier:(NSInteger)arg_identifier
prompt:(NSString *)arg_prompt
defaultText:(NSString *)arg_defaultText
frame:(FWFWKFrameInfoData *)arg_frame
completion:(void (^)(NSString *_Nullable,
FlutterError *_Nullable))
completion {
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi."
@"runJavaScriptTextInputPanel"
binaryMessenger:self.binaryMessenger
codec:FWFWKUIDelegateFlutterApiGetCodec()];
[channel sendMessage:@[
@(arg_identifier), arg_prompt ?: [NSNull null], arg_defaultText ?: [NSNull null],
arg_frame ?: [NSNull null]
]
reply:^(NSArray<id> *reply) {
if (reply != nil) {
if (reply.count > 1) {
completion(nil, [FlutterError errorWithCode:reply[0]
message:reply[1]
details:reply[2]]);
} else {
NSString *output = reply[0] == [NSNull null] ? nil : reply[0];
completion(output, nil);
}
} else {
completion(nil, [FlutterError
errorWithCode:@"channel-error"
message:@"Unable to establish connection on channel."
details:@""]);
}
}];
}
@end

@interface FWFWKHttpCookieStoreHostApiCodecReader : FlutterStandardReader
Expand Down
Loading