Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
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
Expand Up @@ -63,6 +63,9 @@ public void onMethodCall(MethodCall methodCall, Result result) {
case "loadUrl":
loadUrl(methodCall, result);
break;
case "getUserAgent":
getUserAgent(methodCall, result);
break;
case "updateSettings":
updateSettings(methodCall, result);
break;
Expand Down Expand Up @@ -144,6 +147,11 @@ private void currentUrl(Result result) {
result.success(webView.getUrl());
}

private void getUserAgent(MethodCall methodCall, Result result) {
String userAgent = webView.getSettings().getUserAgentString();
result.success(userAgent);
}

@SuppressWarnings("unchecked")
private void updateSettings(MethodCall methodCall, Result result) {
applySettings((Map<String, Object>) methodCall.arguments);
Expand Down Expand Up @@ -207,6 +215,9 @@ private void applySettings(Map<String, Object> settings) {

webView.setWebContentsDebuggingEnabled(debuggingEnabled);
break;
case "userAgent":
updateUserAgent((String) settings.get(key));
break;
default:
throw new IllegalArgumentException("Unknown WebView setting: " + key);
}
Expand All @@ -233,6 +244,10 @@ private void registerJavaScriptChannelNames(List<String> channelNames) {
}
}

private void updateUserAgent(String userAgent) {
webView.getSettings().setUserAgentString(userAgent);
}

@Override
public void dispose() {
methodChannel.setMethodCallHandler(null);
Expand Down
43 changes: 43 additions & 0 deletions packages/webview_flutter/example/test_driver/webview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,49 @@ void main() {
await controller.evaluateJavascript('Echo.postMessage("hello");');
expect(messagesReceived, equals(<String>['hello']));
});

test('userAgent', () async {
final Completer<WebViewController> controllerCompleter01 =
Completer<WebViewController>();
await pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WebView(
key: GlobalKey(),
initialUrl: 'https://flutter.dev/',
javascriptMode: JavascriptMode.unrestricted,
userAgent: 'UA-01',
onWebViewCreated: (WebViewController controller) {
controllerCompleter01.complete(controller);
},
),
),
);
final WebViewController controller01 = await controllerCompleter01.future;
final String userAgent01 = await controller01.getUserAgent();
expect(userAgent01, 'UA-01');

// rebuilds a WebView with a different user agent.
final Completer<WebViewController> controllerCompleter02 =
Completer<WebViewController>();
await pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WebView(
key: GlobalKey(),
initialUrl: 'https://flutter.dev/',
javascriptMode: JavascriptMode.unrestricted,
userAgent: 'UA-02',
onWebViewCreated: (WebViewController controller) {
controllerCompleter02.complete(controller);
},
),
),
);
final WebViewController controller02 = await controllerCompleter02.future;
final String userAgent02 = await controller02.getUserAgent();
expect(userAgent02, 'UA-02');
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a test that rebuilds a WebView with a different user agent.


Future<void> pumpWidget(Widget widget) {
Expand Down
29 changes: 29 additions & 0 deletions packages/webview_flutter/ios/Classes/FlutterWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
[self onRemoveJavaScriptChannels:call result:result];
} else if ([[call method] isEqualToString:@"clearCache"]) {
[self clearCache:result];
} else if ([[call method] isEqualToString:@"getUserAgent"]) {
[self onGetUserAgent:call result:result];
} else {
result(FlutterMethodNotImplemented);
}
Expand Down Expand Up @@ -228,6 +230,22 @@ - (void)clearCache:(FlutterResult)result {
}
}

- (void)onGetUserAgent:(FlutterMethodCall*)call result:(FlutterResult)result {
[_webView evaluateJavaScript:@"navigator.userAgent"
completionHandler:^(NSString* userAgent, NSError* error) {
if (error) {
result([FlutterError
errorWithCode:@"userAgent_failed"
message:@"Failed getting UserAgent"
details:[NSString stringWithFormat:
@"webview_flutter: fail evaluating JavaScript: %@",
[error localizedDescription]]]);
} else {
result(userAgent);
}
}];
}

- (void)applySettings:(NSDictionary<NSString*, id>*)settings {
for (NSString* key in settings) {
if ([key isEqualToString:@"jsMode"]) {
Expand All @@ -236,6 +254,9 @@ - (void)applySettings:(NSDictionary<NSString*, id>*)settings {
} else if ([key isEqualToString:@"hasNavigationDelegate"]) {
NSNumber* hasDartNavigationDelegate = settings[key];
_navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue];
} else if ([key isEqualToString:@"userAgent"]) {
NSString* userAgent = settings[key];
[self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent];
} else {
NSLog(@"webview_flutter: unknown setting key: %@", key);
}
Expand Down Expand Up @@ -274,6 +295,14 @@ - (bool)loadRequest:(NSDictionary<NSString*, id>*)request {
return false;
}

- (void)updateUserAgent:(NSString*)userAgent {
if (@available(iOS 9.0, *)) {
[_webView setCustomUserAgent:userAgent];
} else {
NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9.");
}
}

- (bool)loadUrl:(NSString*)url {
return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]];
}
Expand Down
16 changes: 16 additions & 0 deletions packages/webview_flutter/lib/webview_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class WebView extends StatefulWidget {
this.javascriptMode = JavascriptMode.disabled,
this.javascriptChannels,
this.navigationDelegate,
this.userAgent,
this.gestureRecognizers,
this.onPageFinished,
this.debuggingEnabled = false,
Expand Down Expand Up @@ -256,6 +257,9 @@ class WebView extends StatefulWidget {
/// By default `debuggingEnabled` is false.
final bool debuggingEnabled;

/// The custom UserAgent.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should have more information in this Dartdoc, some question I think I might be having if I were looking here:

  1. What would be the user agent when this is null?
  2. What happens when the widget is rebuild with a new userAgent? is it sending a new request with the new user agent?
  3. What happens when goBack is called after a user agent was changed? is the new or previous user agent used?

In general I recommend reading the documentation section in the Flutter style guide which we should follow here.

final String userAgent;

@override
State<StatefulWidget> createState() => _WebViewState();
}
Expand Down Expand Up @@ -349,25 +353,29 @@ class _WebSettings {
this.javascriptMode,
this.hasNavigationDelegate,
this.debuggingEnabled,
this.userAgent,
});

static _WebSettings fromWidget(WebView widget) {
return _WebSettings(
javascriptMode: widget.javascriptMode,
hasNavigationDelegate: widget.navigationDelegate != null,
debuggingEnabled: widget.debuggingEnabled,
userAgent: widget.userAgent,
);
}

final JavascriptMode javascriptMode;
final bool hasNavigationDelegate;
final bool debuggingEnabled;
final String userAgent;

Map<String, dynamic> toMap() {
return <String, dynamic>{
'jsMode': javascriptMode.index,
'hasNavigationDelegate': hasNavigationDelegate,
'debuggingEnabled': debuggingEnabled,
'userAgent': userAgent,
};
}

Expand All @@ -384,6 +392,9 @@ class _WebSettings {
updates['debuggingEnabled'] = newSettings.debuggingEnabled;
}

if (userAgent != newSettings.userAgent) {
updates['userAgent'] = newSettings.userAgent;
}
return updates;
}
}
Expand Down Expand Up @@ -529,6 +540,11 @@ class WebViewController {
return _channel.invokeMethod("reload");
}

/// Returns the User-Agent value that will be used for subsequent HTTP requests.
Future<String> getUserAgent() async {
return await _channel.invokeMethod('getUserAgent');
}

/// Clears all caches used by the [WebView].
///
/// The following caches are cleared:
Expand Down
43 changes: 43 additions & 0 deletions packages/webview_flutter/test/webview_flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,42 @@ void main() {
expect(platformWebView.javascriptMode, JavascriptMode.disabled);
});

testWidgets('Set UserAgent', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
javascriptMode: JavascriptMode.unrestricted,
));

final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;

expect(platformWebView.userAgent, isNull);

await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
javascriptMode: JavascriptMode.unrestricted,
userAgent: 'UA',
));

expect(platformWebView.userAgent, 'UA');
});

testWidgets('Get UserAgent', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
javascriptMode: JavascriptMode.unrestricted,
userAgent: 'UA',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
expect(await controller.getUserAgent(), 'UA');
});

testWidgets('Load url', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
Expand Down Expand Up @@ -823,6 +859,7 @@ class FakePlatformWebView {
hasNavigationDelegate =
params['settings']['hasNavigationDelegate'] ?? false;
debuggingEnabled = params['settings']['debuggingEnabled'];
userAgent = params['settings']['userAgent'];

channel = MethodChannel(
'plugins.flutter.io/webview_$id', const StandardMethodCodec());
Expand All @@ -842,13 +879,16 @@ class FakePlatformWebView {

bool hasNavigationDelegate;
bool debuggingEnabled;
String userAgent;

Future<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
case 'loadUrl':
final Map<dynamic, dynamic> request = call.arguments;
_loadUrl(request['url']);
return Future<void>.sync(() {});
case 'userAgent':
return Future<String>.value(userAgent);
case 'updateSettings':
if (call.arguments['jsMode'] != null) {
javascriptMode = JavascriptMode.values[call.arguments['jsMode']];
Expand All @@ -859,6 +899,9 @@ class FakePlatformWebView {
if (call.arguments['debuggingEnabled'] != null) {
debuggingEnabled = call.arguments['debuggingEnabled'];
}
if (call.arguments['userAgent'] != null) {
userAgent = call.arguments['userAgent'];
}
break;
case 'canGoBack':
return Future<bool>.sync(() => currentPosition > 0);
Expand Down