diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 4db6dbfd2864..c1b19fd22d3e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +* Implemented new `loadRequest` method from platform interface. + ## 2.2.0 * Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index ba0deb4781d4..2be87fbf81be 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -193,7 +193,6 @@ C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */, 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m index 9d127c2c4aaa..61e43c104b57 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -5,6 +5,7 @@ @import Flutter; @import XCTest; @import webview_flutter_wkwebview; +@import webview_flutter_wkwebview.Test; // OCMock library doesn't generate a valid modulemap. #import @@ -301,4 +302,196 @@ - (void)testRunJavascriptReturningResultReturnsErrorResultForWKError { [self waitForExpectationsWithTimeout:30.0 handler:nil]; } +- (void)testBuildNSURLRequestReturnsNilForNonDictionaryValue { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : @"Non Dictionary Value"}]; + + // Verify + XCTAssertNil(request); +} + +- (void)testBuildNSURLRequestReturnsNilForMissingURI { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : @{}}]; + + // Verify + XCTAssertNil(request); +} + +- (void)testBuildNSURLRequestReturnsNilForInvalidURI { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSDictionary *requestData = @{@"uri" : @"invalid uri"}; + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : requestData}]; + + // Verify + XCTAssertNil(request); +} + +- (void)testBuildNSURLRequestBuildsNSMutableURLRequestWithOptionalParameters { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSDictionary *requestData = @{ + @"uri" : @"https://flutter.dev", + @"method" : @"POST", + @"headers" : @{@"Foo" : @"Bar"}, + @"body" : [FlutterStandardTypedData + typedDataWithBytes:[@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]], + }; + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : requestData}]; + + // Verify + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.URL.absoluteString, @"https://flutter.dev"); + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + XCTAssertEqualObjects(request.allHTTPHeaderFields, @{@"Foo" : @"Bar"}); + XCTAssertEqualObjects(request.HTTPBody, [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]); +} + +- (void)testBuildNSURLRequestBuildsNSMutableURLRequestWithoutOptionalParameters { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSDictionary *requestData = @{ + @"uri" : @"https://flutter.dev", + }; + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : requestData}]; + + // Verify + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.URL.absoluteString, @"https://flutter.dev"); + XCTAssertEqualObjects(request.HTTPMethod, @"GET"); + XCTAssertNil(request.allHTTPHeaderFields); + XCTAssertNil(request.HTTPBody); +} + +- (void)testOnLoadUrlReturnsErrorResultForInvalidRequest { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result when request cannot be built"]; + + // Run + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"loadUrl" + arguments:@{}]; + [controller onLoadUrl:methodCall + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testOnLoadUrlLoadsRequestWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return nil"]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockView; + + // Run + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"loadUrl" + arguments:@{@"url" : @"https://flutter.dev/"}]; + [controller onLoadUrl:methodCall + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + OCMVerify([mockView loadRequest:[OCMArg any]]); + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testOnLoadRequestReturnsErroResultForInvalidRequest { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result when request cannot be built"]; + + // Run + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"loadRequest" + arguments:@{}]; + [controller onLoadRequest:methodCall + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testOnLoadRequestLoadsRequestWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return nil"]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockView; + + // Run + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"loadRequest" + arguments:@{@"request" : @{@"uri" : @"https://flutter.dev/"}}]; + [controller onLoadRequest:methodCall + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + OCMVerify([mockView loadRequest:[OCMArg any]]); + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index 21240f63ec1a..ea95cddf9fc9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; @@ -120,6 +121,7 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + doPostRequest, } class _SampleMenu extends StatelessWidget { @@ -157,6 +159,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -189,6 +194,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), ], ); }, @@ -259,6 +268,17 @@ class _SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + void _onDoPostRequest( + WebViewController controller, BuildContext context) async { + WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index 403db1f08ac6..b2555cd831c6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -322,6 +322,11 @@ class WebViewController { return _webViewPlatformController.loadUrl(url, headers); } + /// Loads a page by making the specified request. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index db52124d6a7c..6d8e463c7b13 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -27,6 +27,7 @@ NS_ASSUME_NONNULL_BEGIN - (UIView*)view; - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; + @end @interface FLTWebViewFactory : NSObject diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m index 5e12f8acb2ea..b8355ad18183 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -5,6 +5,7 @@ #import "FlutterWebView.h" #import "FLTWKNavigationDelegate.h" #import "FLTWKProgressionDelegate.h" +#import "FlutterWebView_Test.h" #import "JavaScriptChannelHandler.h" @implementation FLTWebViewFactory { @@ -116,7 +117,11 @@ - (instancetype)initWithFrame:(CGRect)frame NSString* initialUrl = args[@"initialUrl"]; if ([initialUrl isKindOfClass:[NSString class]]) { - [self loadUrl:initialUrl]; + NSURL* url = [NSURL URLWithString:initialUrl]; + if (url) { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; + [_webView loadRequest:request]; + } } } return self; @@ -137,6 +142,8 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onUpdateSettings:call result:result]; } else if ([[call method] isEqualToString:@"loadUrl"]) { [self onLoadUrl:call result:result]; + } else if ([[call method] isEqualToString:@"loadRequest"]) { + [self onLoadRequest:call result:result]; } else if ([[call method] isEqualToString:@"canGoBack"]) { [self onCanGoBack:call result:result]; } else if ([[call method] isEqualToString:@"canGoForward"]) { @@ -186,12 +193,34 @@ - (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { } - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - if (![self loadRequest:[call arguments]]) { + NSMutableDictionary* requestData = [[NSMutableDictionary alloc] init]; + if (call.arguments[@"url"]) { + requestData[@"uri"] = call.arguments[@"url"]; + } + if (call.arguments[@"headers"]) { + requestData[@"headers"] = call.arguments[@"headers"]; + } + NSURLRequest* request = [self buildNSURLRequest:@{@"request" : requestData}]; + if (!request) { result([FlutterError errorWithCode:@"loadUrl_failed" message:@"Failed parsing the URL" details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); } else { + [_webView loadRequest:request]; + result(nil); + } +} + +- (void)onLoadRequest:(FlutterMethodCall*)call result:(FlutterResult)result { + NSURLRequest* request = [self buildNSURLRequest:[call arguments]]; + if (!request) { + result([FlutterError + errorWithCode:@"loadRequest_failed" + message:@"Failed parsing the URL" + details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); + } else { + [_webView loadRequest:request]; result(nil); } } @@ -459,37 +488,47 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy } } -- (bool)loadRequest:(NSDictionary*)request { - if (!request) { - return false; +/** + * Parses the method call arguments and converts them to an NSURLRequest object. + * + * @param arguments the method call arguments. + * + * @return NSURLRequest object. + */ +- (NSURLRequest*)buildNSURLRequest:(NSDictionary*)arguments { + id requestParameters = arguments[@"request"]; + if (![requestParameters isKindOfClass:[NSDictionary class]]) { + return nil; } - NSString* url = request[@"url"]; - if ([url isKindOfClass:[NSString class]]) { - id headers = request[@"headers"]; - if ([headers isKindOfClass:[NSDictionary class]]) { - return [self loadUrl:url withHeaders:headers]; - } else { - return [self loadUrl:url]; - } + NSString* urlString = requestParameters[@"uri"]; + if (!urlString) { + return nil; } - return false; -} + NSURL* url = [NSURL URLWithString:urlString]; + if (!url) { + return nil; + } -- (bool)loadUrl:(NSString*)url { - return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; -} + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; + + NSString* httpMethod = requestParameters[@"method"]; + if (httpMethod) { + [request setHTTPMethod:httpMethod]; + } -- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { - NSURL* nsUrl = [NSURL URLWithString:url]; - if (!nsUrl) { - return false; + id httpBody = requestParameters[@"body"]; + if ([httpBody isKindOfClass:[FlutterStandardTypedData class]]) { + [request setHTTPBody:[httpBody data]]; } - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; - [request setAllHTTPHeaderFields:headers]; - [_webView loadRequest:request]; - return true; + + id headers = requestParameters[@"headers"]; + if ([headers isKindOfClass:[NSDictionary class]]) { + [request setAllHTTPHeaderFields:headers]; + } + + return request; } - (void)registerJavaScriptChannels:(NSSet*)channelNames diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap new file mode 100644 index 000000000000..fa5143060381 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap @@ -0,0 +1,10 @@ +framework module webview_flutter_wkwebview { + umbrella header "webview-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "FlutterWebView_Test.h" + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView_Test.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView_Test.h new file mode 100644 index 000000000000..13973f8c5c6e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView_Test.h @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This header is available in the Test module. Import via "@import webview_flutter_wkwebview.Test;" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWebViewController () + +- (NSURLRequest*)buildNSURLRequest:(NSDictionary*)arguments; + +- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result; + +- (void)onLoadRequest:(FlutterMethodCall*)call result:(FlutterResult)result; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h new file mode 100644 index 000000000000..c37282e886bc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import +#import +#import +#import +#import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec index 2dfb336df35a..89c06237b203 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -14,8 +14,9 @@ Downloaded by pub (not CocoaPods). s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview' } s.documentation_url = 'https://pub.dev/packages/webview_flutter' - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/FlutterWebView.modulemap' s.dependency 'Flutter' s.platform = :ios, '9.0' diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 5176adb9749c..ff2b69fa8d20 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.2.0 +version: 2.3.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.2.0 + webview_flutter_platform_interface: ^1.3.0 dev_dependencies: flutter_driver: