diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index d95a843cc7..cc60d2de09 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 3.10.0 +* Adds support for `PlatformNavigationDelegate.setOnHttpAuthRequest`. * Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 3.9.4 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 64d7702f20..89ecad8d0e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -34,6 +34,15 @@ Future main() async { request.response.writeln('${request.headers}'); } else if (request.uri.path == '/favicon.ico') { request.response.statusCode = HttpStatus.notFound; + } else if (request.uri.path == '/http-basic-authentication') { + final bool isAuthenticating = request.headers['Authorization'] != null; + if (isAuthenticating) { + request.response.writeln('Authorized'); + } else { + request.response.headers + .add('WWW-Authenticate', 'Basic realm="Test realm"'); + request.response.statusCode = HttpStatus.unauthorized; + } } else { fail('unexpected request: ${request.method} ${request.uri}'); } @@ -43,6 +52,7 @@ Future main() async { final String primaryUrl = '$prefixUrl/hello.txt'; final String secondaryUrl = '$prefixUrl/secondary.txt'; final String headersUrl = '$prefixUrl/headers'; + final String basicAuthUrl = '$prefixUrl/http-basic-authentication'; testWidgets( 'withWeakReferenceTo allows encapsulating class to be garbage collected', @@ -1127,6 +1137,82 @@ Future main() async { }); }); + testWidgets('can receive HTTP basic auth requests', + (WidgetTester tester) async { + final Completer authRequested = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ); + + final PlatformNavigationDelegate navigationDelegate = + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ); + await navigationDelegate.setOnHttpAuthRequest( + (HttpAuthRequest request) => authRequested.complete()); + await controller.setPlatformNavigationDelegate(navigationDelegate); + + // Clear cache so that the auth request is always received and we don't get + // a cached response. + await controller.clearCache(); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + WebKitWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + ), + ); + + await controller.loadRequest( + LoadRequestParams(uri: Uri.parse(basicAuthUrl)), + ); + + await expectLater(authRequested.future, completes); + }); + + testWidgets('can reply to HTTP basic auth requests', + (WidgetTester tester) async { + final Completer pageFinished = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ); + + final PlatformNavigationDelegate navigationDelegate = + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ); + await navigationDelegate.setOnPageFinished((_) => pageFinished.complete()); + await navigationDelegate.setOnHttpAuthRequest( + (HttpAuthRequest request) => request.onProceed( + const WebViewCredential(user: 'user', password: 'password'), + ), + ); + await controller.setPlatformNavigationDelegate(navigationDelegate); + + // Clear cache so that the auth request is always received and we don't get + // a cached response. + await controller.clearCache(); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + WebKitWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + ), + ); + + await controller.loadRequest( + LoadRequestParams(uri: Uri.parse(basicAuthUrl)), + ); + + await expectLater(pageFinished.future, completes); + }); + testWidgets('launches with gestureNavigationEnabled on iOS', (WidgetTester tester) async { final WebKitWebViewController controller = WebKitWebViewController( 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 32c0bf9872..5b13ebad68 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 @@ -11,6 +11,9 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */; }; 8F4FF94B29AC223F000A6586 /* FWFURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF94A29AC223F000A6586 /* FWFURLTests.m */; }; + 8F562F902A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */; }; + 8F562F922A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */; }; + 8F562F942A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */; }; 8F78EAAA2A02CB9100C2E520 /* FWFErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */; }; 8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; }; 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; }; @@ -81,6 +84,9 @@ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewFlutterWKWebViewExternalAPITests.m; sourceTree = ""; }; 8F4FF94A29AC223F000A6586 /* FWFURLTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLTests.m; sourceTree = ""; }; + 8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLCredentialHostApiTests.m; sourceTree = ""; }; + 8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLProtectionSpaceHostApiTests.m; sourceTree = ""; }; + 8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLAuthenticationChallengeHostApiTests.m; sourceTree = ""; }; 8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFErrorTests.m; sourceTree = ""; }; 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = ""; }; 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = ""; }; @@ -167,6 +173,9 @@ 8FB79B902820BAC700C101D3 /* FWFUIViewHostApiTests.m */, 8FB79B962821985200C101D3 /* FWFObjectHostApiTests.m */, 8F4FF94A29AC223F000A6586 /* FWFURLTests.m */, + 8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */, + 8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */, + 8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */, 8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */, ); path = RunnerTests; @@ -318,7 +327,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 68BDCAE823C3F7CB00D9C032 = { @@ -327,7 +336,6 @@ }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 7624MWN53C; }; F7151F73266057800028CB91 = { CreatedOnToolsVersion = 12.5; @@ -474,12 +482,15 @@ 8FB79B852820A3A400C101D3 /* FWFUIDelegateHostApiTests.m in Sources */, 8FB79B972821985200C101D3 /* FWFObjectHostApiTests.m in Sources */, 8FB79B672820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m in Sources */, + 8F562F942A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m in Sources */, 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */, 8FB79B73282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m in Sources */, 8FB79B7928209D1300C101D3 /* FWFUserContentControllerHostApiTests.m in Sources */, + 8F562F902A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m in Sources */, 8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */, 8FB79B6B28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m in Sources */, 8FB79B8F2820BAB300C101D3 /* FWFScrollViewHostApiTests.m in Sources */, + 8F562F922A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m in Sources */, 8FB79B912820BAC700C101D3 /* FWFUIViewHostApiTests.m in Sources */, 8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */, 8FB79B6D2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m in Sources */, @@ -689,6 +700,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -715,6 +727,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index cb713d7676..cf07c46df2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + +@interface FWFURLAuthenticationChallengeHostApiTests : XCTestCase + +@end + +@implementation FWFURLAuthenticationChallengeHostApiTests +- (void)testFlutterApiCreate { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFURLAuthenticationChallengeFlutterApiImpl *flutterApi = + [[FWFURLAuthenticationChallengeFlutterApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + flutterApi.api = OCMClassMock([FWFNSUrlAuthenticationChallengeFlutterApi class]); + + NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host" + port:0 + protocol:nil + realm:@"realm" + authenticationMethod:nil]; + + NSURLAuthenticationChallenge *mockChallenge = OCMClassMock([NSURLAuthenticationChallenge class]); + OCMStub([mockChallenge protectionSpace]).andReturn(protectionSpace); + + [flutterApi createWithInstance:mockChallenge + protectionSpace:protectionSpace + completion:^(FlutterError *error){ + + }]; + + long identifier = [instanceManager identifierWithStrongReferenceForInstance:mockChallenge]; + long protectionSpaceIdentifier = + [instanceManager identifierWithStrongReferenceForInstance:protectionSpace]; + OCMVerify([flutterApi.api createWithIdentifier:identifier + protectionSpaceIdentifier:protectionSpaceIdentifier + completion:OCMOCK_ANY]); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFURLCredentialHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFURLCredentialHostApiTests.m new file mode 100644 index 0000000000..7f3aa3426d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFURLCredentialHostApiTests.m @@ -0,0 +1,35 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFURLCredentialHostApiTests : XCTestCase +@end + +@implementation FWFURLCredentialHostApiTests +- (void)testHostApiCreate { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFURLCredentialHostApiImpl *hostApi = [[FWFURLCredentialHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostApi createWithUserWithIdentifier:0 + user:@"user" + password:@"password" + persistence:FWFNSUrlCredentialPersistencePermanent + error:&error]; + XCTAssertNil(error); + + NSURLCredential *credential = (NSURLCredential *)[instanceManager instanceForIdentifier:0]; + XCTAssertEqualObjects(credential.user, @"user"); + XCTAssertEqualObjects(credential.password, @"password"); + XCTAssertEqual(credential.persistence, NSURLCredentialPersistencePermanent); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFURLProtectionSpaceHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFURLProtectionSpaceHostApiTests.m new file mode 100644 index 0000000000..c5a6cdf36c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFURLProtectionSpaceHostApiTests.m @@ -0,0 +1,43 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFURLProtectionSpaceHostApiTests : XCTestCase +@end + +@implementation FWFURLProtectionSpaceHostApiTests +- (void)testFlutterApiCreate { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFURLProtectionSpaceFlutterApiImpl *flutterApi = [[FWFURLProtectionSpaceFlutterApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + flutterApi.api = OCMClassMock([FWFNSUrlProtectionSpaceFlutterApi class]); + + NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host" + port:0 + protocol:nil + realm:@"realm" + authenticationMethod:nil]; + [flutterApi createWithInstance:protectionSpace + host:@"host" + realm:@"realm" + authenticationMethod:@"method" + completion:^(FlutterError *error){ + + }]; + + long identifier = [instanceManager identifierWithStrongReferenceForInstance:protectionSpace]; + OCMVerify([flutterApi.api createWithIdentifier:identifier + host:@"host" + realm:@"realm" + authenticationMethod:@"method" + completion:OCMOCK_ANY]); +} +@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 8f3dfa8aeb..c2370184bf 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -162,6 +162,9 @@ Page resource error: }) ..setOnUrlChange((UrlChange change) { debugPrint('url change to ${change.url}'); + }) + ..setOnHttpAuthRequest((HttpAuthRequest request) { + openDialog(request); }), ) ..addJavaScriptChannel(JavaScriptChannelParams( @@ -220,6 +223,62 @@ Page resource error: child: const Icon(Icons.favorite), ); } + + Future openDialog(HttpAuthRequest httpRequest) async { + final TextEditingController usernameTextController = + TextEditingController(); + final TextEditingController passwordTextController = + TextEditingController(); + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text('${httpRequest.host}: ${httpRequest.realm ?? '-'}'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: const InputDecoration(labelText: 'Username'), + autofocus: true, + controller: usernameTextController, + ), + TextField( + decoration: const InputDecoration(labelText: 'Password'), + controller: passwordTextController, + ), + ], + ), + ), + actions: [ + // Explicitly cancel the request on iOS as the OS does not emit new + // requests when a previous request is pending. + TextButton( + onPressed: () { + httpRequest.onCancel(); + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + httpRequest.onProceed( + WebViewCredential( + user: usernameTextController.text, + password: passwordTextController.text, + ), + ); + Navigator.of(context).pop(); + }, + child: const Text('Authenticate'), + ), + ], + ); + }, + ); + } } enum MenuOptions { @@ -237,6 +296,7 @@ enum MenuOptions { transparentBackground, setCookie, logExample, + basicAuthentication, } class SampleMenu extends StatelessWidget { @@ -300,6 +360,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.logExample: _onLogExample(); break; + case MenuOptions.basicAuthentication: + _promptForUrl(context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -360,6 +423,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.logExample, child: Text('Log example'), ), + const PopupMenuItem( + value: MenuOptions.basicAuthentication, + child: Text('Basic Authentication Example'), + ), ], ); } @@ -518,6 +585,41 @@ class SampleMenu extends StatelessWidget { return webViewController.loadHtmlString(kLogExamplePage); } + + Future _promptForUrl(BuildContext context) { + final TextEditingController urlTextController = + TextEditingController(text: 'https://'); + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Input URL to visit'), + content: TextField( + decoration: const InputDecoration(labelText: 'URL'), + autofocus: true, + controller: urlTextController, + ), + actions: [ + TextButton( + onPressed: () { + if (urlTextController.text.isNotEmpty) { + final Uri? uri = Uri.tryParse(urlTextController.text); + if (uri != null && uri.scheme.isNotEmpty) { + webViewController.loadRequest( + LoadRequestParams(uri: uri), + ); + Navigator.pop(context); + } + } + }, + child: const Text('Visit'), + ), + ], + ); + }, + ); + } } class NavigationControls extends StatelessWidget { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index e4066c0483..6590404632 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter path_provider: ^2.0.6 - webview_flutter_platform_interface: ^2.6.0 + webview_flutter_platform_interface: ^2.7.0 webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m index c3d6699e41..e776726693 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m @@ -13,6 +13,7 @@ #import "FWFScrollViewHostApi.h" #import "FWFUIDelegateHostApi.h" #import "FWFUIViewHostApi.h" +#import "FWFURLCredentialHostApi.h" #import "FWFURLHostApi.h" #import "FWFUserContentControllerHostApi.h" #import "FWFWebViewConfigurationHostApi.h" @@ -105,6 +106,11 @@ [[FWFURLHostApiImpl alloc] initWithBinaryMessenger:registrar.messenger instanceManager:instanceManager]); + SetUpFWFNSUrlCredentialHostApi( + registrar.messenger, + [[FWFURLCredentialHostApiImpl alloc] initWithBinaryMessenger:registrar.messenger + instanceManager:instanceManager]); + FWFWebViewFactory *webviewFactory = [[FWFWebViewFactory alloc] initWithManager:instanceManager]; [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h index 005cecb39b..e9405f3bb9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h @@ -193,4 +193,26 @@ API_AVAILABLE(ios(15.0)) extern FWFWKMediaCaptureTypeData *FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType( WKMediaCaptureType type); +/** + * Converts an FWFNSUrlSessionAuthChallengeDisposition to an NSURLSessionAuthChallengeDisposition. + * + * @param value The object containing information to create an NSURLSessionAuthChallengeDisposition. + * + * @return A NSURLSessionAuthChallengeDisposition or -1 if data could not be converted. + */ +extern NSURLSessionAuthChallengeDisposition +FWFNativeNSURLSessionAuthChallengeDispositionFromFWFNSUrlSessionAuthChallengeDisposition( + FWFNSUrlSessionAuthChallengeDisposition value); + +/** + * Converts an FWFNSUrlCredentialPersistence to an NSURLCredentialPersistence. + * + * @param value The object containing information to create an NSURLCredentialPersistence. + * + * @return A NSURLCredentialPersistence or -1 if data could not be converted. + */ +extern NSURLCredentialPersistence +FWFNativeNSURLCredentialPersistenceFromFWFNSUrlCredentialPersistence( + FWFNSUrlCredentialPersistence value); + NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m index 3cbcea844a..a4dda8bf42 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m @@ -285,3 +285,36 @@ FWFWKMediaCaptureTypeData *FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType return nil; } + +NSURLSessionAuthChallengeDisposition +FWFNativeNSURLSessionAuthChallengeDispositionFromFWFNSUrlSessionAuthChallengeDisposition( + FWFNSUrlSessionAuthChallengeDisposition value) { + switch (value) { + case FWFNSUrlSessionAuthChallengeDispositionUseCredential: + return NSURLSessionAuthChallengeUseCredential; + case FWFNSUrlSessionAuthChallengeDispositionPerformDefaultHandling: + return NSURLSessionAuthChallengePerformDefaultHandling; + case FWFNSUrlSessionAuthChallengeDispositionCancelAuthenticationChallenge: + return NSURLSessionAuthChallengeCancelAuthenticationChallenge; + case FWFNSUrlSessionAuthChallengeDispositionRejectProtectionSpace: + return NSURLSessionAuthChallengeRejectProtectionSpace; + } + + return -1; +} + +NSURLCredentialPersistence FWFNativeNSURLCredentialPersistenceFromFWFNSUrlCredentialPersistence( + FWFNSUrlCredentialPersistence value) { + switch (value) { + case FWFNSUrlCredentialPersistenceNone: + return NSURLCredentialPersistenceNone; + case FWFNSUrlCredentialPersistenceSession: + return NSURLCredentialPersistenceForSession; + case FWFNSUrlCredentialPersistencePermanent: + return NSURLCredentialPersistencePermanent; + case FWFNSUrlCredentialPersistenceSynchronizable: + return NSURLCredentialPersistenceSynchronizable; + } + + return -1; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h index 8469438ab0..1e312a2d2a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -264,6 +264,73 @@ typedef NS_ENUM(NSUInteger, FWFWKMediaCaptureType) { - (instancetype)initWithValue:(FWFWKMediaCaptureType)value; @end +/// Responses to an authentication challenge. +/// +/// See +/// https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition?language=objc. +typedef NS_ENUM(NSUInteger, FWFNSUrlSessionAuthChallengeDisposition) { + /// Use the specified credential, which may be nil. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengeusecredential?language=objc. + FWFNSUrlSessionAuthChallengeDispositionUseCredential = 0, + /// Use the default handling for the challenge as though this delegate method + /// were not implemented. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengeperformdefaulthandling?language=objc. + FWFNSUrlSessionAuthChallengeDispositionPerformDefaultHandling = 1, + /// Cancel the entire request. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengecancelauthenticationchallenge?language=objc. + FWFNSUrlSessionAuthChallengeDispositionCancelAuthenticationChallenge = 2, + /// Reject this challenge, and call the authentication delegate method again + /// with the next authentication protection space. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengerejectprotectionspace?language=objc. + FWFNSUrlSessionAuthChallengeDispositionRejectProtectionSpace = 3, +}; + +/// Wrapper for FWFNSUrlSessionAuthChallengeDisposition to allow for nullability. +@interface FWFNSUrlSessionAuthChallengeDispositionBox : NSObject +@property(nonatomic, assign) FWFNSUrlSessionAuthChallengeDisposition value; +- (instancetype)initWithValue:(FWFNSUrlSessionAuthChallengeDisposition)value; +@end + +/// Specifies how long a credential will be kept. +typedef NS_ENUM(NSUInteger, FWFNSUrlCredentialPersistence) { + /// The credential should not be stored. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencenone?language=objc. + FWFNSUrlCredentialPersistenceNone = 0, + /// The credential should be stored only for this session. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistenceforsession?language=objc. + FWFNSUrlCredentialPersistenceSession = 1, + /// The credential should be stored in the keychain. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencepermanent?language=objc. + FWFNSUrlCredentialPersistencePermanent = 2, + /// The credential should be stored permanently in the keychain, and in + /// addition should be distributed to other devices based on the owning Apple + /// ID. + /// + /// See + /// https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencesynchronizable?language=objc. + FWFNSUrlCredentialPersistenceSynchronizable = 3, +}; + +/// Wrapper for FWFNSUrlCredentialPersistence to allow for nullability. +@interface FWFNSUrlCredentialPersistenceBox : NSObject +@property(nonatomic, assign) FWFNSUrlCredentialPersistence value; +- (instancetype)initWithValue:(FWFNSUrlCredentialPersistence)value; +@end + @class FWFNSKeyValueObservingOptionsEnumData; @class FWFNSKeyValueChangeKeyEnumData; @class FWFWKUserScriptInjectionTimeEnumData; @@ -282,6 +349,7 @@ typedef NS_ENUM(NSUInteger, FWFWKMediaCaptureType) { @class FWFWKSecurityOriginData; @class FWFNSHttpCookieData; @class FWFObjectOrIdentifier; +@class FWFAuthenticationChallengeResponse; @interface FWFNSKeyValueObservingOptionsEnumData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. @@ -462,6 +530,15 @@ typedef NS_ENUM(NSUInteger, FWFWKMediaCaptureType) { @property(nonatomic, assign) BOOL isIdentifier; @end +@interface FWFAuthenticationChallengeResponse : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithDisposition:(FWFNSUrlSessionAuthChallengeDisposition)disposition + credentialIdentifier:(nullable NSNumber *)credentialIdentifier; +@property(nonatomic, assign) FWFNSUrlSessionAuthChallengeDisposition disposition; +@property(nonatomic, strong, nullable) NSNumber *credentialIdentifier; +@end + /// The codec used by FWFWKWebsiteDataStoreHostApi. NSObject *FWFWKWebsiteDataStoreHostApiGetCodec(void); @@ -711,6 +788,14 @@ NSObject *FWFWKNavigationDelegateFlutterApiGetCodec(void); completion: (void (^)(FlutterError *_Nullable)) completion; +- (void)didReceiveAuthenticationChallengeForDelegateWithIdentifier:(NSInteger)identifier + webViewIdentifier:(NSInteger)webViewIdentifier + challengeIdentifier:(NSInteger)challengeIdentifier + completion: + (void (^)( + FWFAuthenticationChallengeResponse + *_Nullable, + FlutterError *_Nullable))completion; @end /// The codec used by FWFNSObjectHostApi. @@ -919,4 +1004,65 @@ NSObject *FWFNSUrlFlutterApiGetCodec(void); completion:(void (^)(FlutterError *_Nullable))completion; @end +/// The codec used by FWFNSUrlCredentialHostApi. +NSObject *FWFNSUrlCredentialHostApiGetCodec(void); + +/// Host API for `NSUrlCredential`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlcredential?language=objc. +@protocol FWFNSUrlCredentialHostApi +/// Create a new native instance and add it to the `InstanceManager`. +- (void)createWithUserWithIdentifier:(NSInteger)identifier + user:(NSString *)user + password:(NSString *)password + persistence:(FWFNSUrlCredentialPersistence)persistence + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void SetUpFWFNSUrlCredentialHostApi(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFNSUrlProtectionSpaceFlutterApi. +NSObject *FWFNSUrlProtectionSpaceFlutterApiGetCodec(void); + +/// Flutter API for `NSUrlProtectionSpace`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlprotectionspace?language=objc. +@interface FWFNSUrlProtectionSpaceFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +/// Create a new Dart instance and add it to the `InstanceManager`. +- (void)createWithIdentifier:(NSInteger)identifier + host:(nullable NSString *)host + realm:(nullable NSString *)realm + authenticationMethod:(nullable NSString *)authenticationMethod + completion:(void (^)(FlutterError *_Nullable))completion; +@end + +/// The codec used by FWFNSUrlAuthenticationChallengeFlutterApi. +NSObject *FWFNSUrlAuthenticationChallengeFlutterApiGetCodec(void); + +/// Flutter API for `NSUrlAuthenticationChallenge`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See +/// https://developer.apple.com/documentation/foundation/nsurlauthenticationchallenge?language=objc. +@interface FWFNSUrlAuthenticationChallengeFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +/// Create a new Dart instance and add it to the `InstanceManager`. +- (void)createWithIdentifier:(NSInteger)identifier + protectionSpaceIdentifier:(NSInteger)protectionSpaceIdentifier + completion:(void (^)(FlutterError *_Nullable))completion; +@end + NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m index 52fbbee935..5d8017832b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -164,6 +164,31 @@ } @end +/// Responses to an authentication challenge. +/// +/// See +/// https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition?language=objc. +@implementation FWFNSUrlSessionAuthChallengeDispositionBox +- (instancetype)initWithValue:(FWFNSUrlSessionAuthChallengeDisposition)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + +/// Specifies how long a credential will be kept. +@implementation FWFNSUrlCredentialPersistenceBox +- (instancetype)initWithValue:(FWFNSUrlCredentialPersistence)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + static NSArray *wrapResult(id result, FlutterError *error) { if (error) { return @[ @@ -285,6 +310,12 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { - (NSArray *)toList; @end +@interface FWFAuthenticationChallengeResponse () ++ (FWFAuthenticationChallengeResponse *)fromList:(NSArray *)list; ++ (nullable FWFAuthenticationChallengeResponse *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FWFNSKeyValueObservingOptionsEnumData + (instancetype)makeWithValue:(FWFNSKeyValueObservingOptionsEnum)value { FWFNSKeyValueObservingOptionsEnumData *pigeonResult = @@ -727,6 +758,33 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { } @end +@implementation FWFAuthenticationChallengeResponse ++ (instancetype)makeWithDisposition:(FWFNSUrlSessionAuthChallengeDisposition)disposition + credentialIdentifier:(nullable NSNumber *)credentialIdentifier { + FWFAuthenticationChallengeResponse *pigeonResult = + [[FWFAuthenticationChallengeResponse alloc] init]; + pigeonResult.disposition = disposition; + pigeonResult.credentialIdentifier = credentialIdentifier; + return pigeonResult; +} ++ (FWFAuthenticationChallengeResponse *)fromList:(NSArray *)list { + FWFAuthenticationChallengeResponse *pigeonResult = + [[FWFAuthenticationChallengeResponse alloc] init]; + pigeonResult.disposition = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.credentialIdentifier = GetNullableObjectAtIndex(list, 1); + return pigeonResult; +} ++ (nullable FWFAuthenticationChallengeResponse *)nullableFromList:(NSArray *)list { + return (list) ? [FWFAuthenticationChallengeResponse fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.disposition), + self.credentialIdentifier ?: [NSNull null], + ]; +} +@end + @interface FWFWKWebsiteDataStoreHostApiCodecReader : FlutterStandardReader @end @implementation FWFWKWebsiteDataStoreHostApiCodecReader @@ -1684,14 +1742,16 @@ void SetUpFWFWKNavigationDelegateHostApi(id binaryMessen - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSErrorData fromList:[self readValue]]; + return [FWFAuthenticationChallengeResponse fromList:[self readValue]]; case 129: - return [FWFNSUrlRequestData fromList:[self readValue]]; + return [FWFNSErrorData fromList:[self readValue]]; case 130: - return [FWFWKFrameInfoData fromList:[self readValue]]; + return [FWFNSUrlRequestData fromList:[self readValue]]; case 131: - return [FWFWKNavigationActionData fromList:[self readValue]]; + return [FWFWKFrameInfoData fromList:[self readValue]]; case 132: + return [FWFWKNavigationActionData fromList:[self readValue]]; + case 133: return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1703,21 +1763,24 @@ void SetUpFWFWKNavigationDelegateHostApi(id binaryMessen @end @implementation FWFWKNavigationDelegateFlutterApiCodecWriter - (void)writeValue:(id)value { - if ([value isKindOfClass:[FWFNSErrorData class]]) { + if ([value isKindOfClass:[FWFAuthenticationChallengeResponse class]]) { [self writeByte:128]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { + } else if ([value isKindOfClass:[FWFNSErrorData class]]) { [self writeByte:129]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { + } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { [self writeByte:130]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:131]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:132]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -1934,6 +1997,40 @@ NSObject *FWFWKNavigationDelegateFlutterApiGetCodec(void) { } }]; } +- (void) + didReceiveAuthenticationChallengeForDelegateWithIdentifier:(NSInteger)arg_identifier + webViewIdentifier:(NSInteger)arg_webViewIdentifier + challengeIdentifier:(NSInteger)arg_challengeIdentifier + completion: + (void (^)(FWFAuthenticationChallengeResponse + *_Nullable, + FlutterError *_Nullable)) + completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview." + @"WKNavigationDelegateFlutterApi.didReceiveAuthenticationChallenge" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ @(arg_identifier), @(arg_webViewIdentifier), @(arg_challengeIdentifier) ] + reply:^(NSArray *reply) { + if (reply != nil) { + if (reply.count > 1) { + completion(nil, [FlutterError errorWithCode:reply[0] + message:reply[1] + details:reply[2]]); + } else { + FWFAuthenticationChallengeResponse *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 FWFNSObjectHostApiCodecReader : FlutterStandardReader @@ -2201,40 +2298,42 @@ NSObject *FWFNSObjectFlutterApiGetCodec(void) { - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSErrorData fromList:[self readValue]]; + return [FWFAuthenticationChallengeResponse fromList:[self readValue]]; case 129: - return [FWFNSHttpCookieData fromList:[self readValue]]; + return [FWFNSErrorData fromList:[self readValue]]; case 130: - return [FWFNSHttpCookiePropertyKeyEnumData fromList:[self readValue]]; + return [FWFNSHttpCookieData fromList:[self readValue]]; case 131: - return [FWFNSKeyValueChangeKeyEnumData fromList:[self readValue]]; + return [FWFNSHttpCookiePropertyKeyEnumData fromList:[self readValue]]; case 132: - return [FWFNSKeyValueObservingOptionsEnumData fromList:[self readValue]]; + return [FWFNSKeyValueChangeKeyEnumData fromList:[self readValue]]; case 133: - return [FWFNSUrlRequestData fromList:[self readValue]]; + return [FWFNSKeyValueObservingOptionsEnumData fromList:[self readValue]]; case 134: - return [FWFObjectOrIdentifier fromList:[self readValue]]; + return [FWFNSUrlRequestData fromList:[self readValue]]; case 135: - return [FWFWKAudiovisualMediaTypeEnumData fromList:[self readValue]]; + return [FWFObjectOrIdentifier fromList:[self readValue]]; case 136: - return [FWFWKFrameInfoData fromList:[self readValue]]; + return [FWFWKAudiovisualMediaTypeEnumData fromList:[self readValue]]; case 137: - return [FWFWKMediaCaptureTypeData fromList:[self readValue]]; + return [FWFWKFrameInfoData fromList:[self readValue]]; case 138: - return [FWFWKNavigationActionData fromList:[self readValue]]; + return [FWFWKMediaCaptureTypeData fromList:[self readValue]]; case 139: - return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; + return [FWFWKNavigationActionData fromList:[self readValue]]; case 140: - return [FWFWKPermissionDecisionData fromList:[self readValue]]; + return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; case 141: - return [FWFWKScriptMessageData fromList:[self readValue]]; + return [FWFWKPermissionDecisionData fromList:[self readValue]]; case 142: - return [FWFWKSecurityOriginData fromList:[self readValue]]; + return [FWFWKScriptMessageData fromList:[self readValue]]; case 143: - return [FWFWKUserScriptData fromList:[self readValue]]; + return [FWFWKSecurityOriginData fromList:[self readValue]]; case 144: - return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; + return [FWFWKUserScriptData fromList:[self readValue]]; case 145: + return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; + case 146: return [FWFWKWebsiteDataTypeEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -2246,60 +2345,63 @@ NSObject *FWFNSObjectFlutterApiGetCodec(void) { @end @implementation FWFWKWebViewHostApiCodecWriter - (void)writeValue:(id)value { - if ([value isKindOfClass:[FWFNSErrorData class]]) { + if ([value isKindOfClass:[FWFAuthenticationChallengeResponse class]]) { [self writeByte:128]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { + } else if ([value isKindOfClass:[FWFNSErrorData class]]) { [self writeByte:129]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { + } else if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { [self writeByte:130]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFNSKeyValueChangeKeyEnumData class]]) { + } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { [self writeByte:131]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { + } else if ([value isKindOfClass:[FWFNSKeyValueChangeKeyEnumData class]]) { [self writeByte:132]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { + } else if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { [self writeByte:133]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFObjectOrIdentifier class]]) { + } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { [self writeByte:134]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { + } else if ([value isKindOfClass:[FWFObjectOrIdentifier class]]) { [self writeByte:135]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { + } else if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { [self writeByte:136]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKMediaCaptureTypeData class]]) { + } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:137]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + } else if ([value isKindOfClass:[FWFWKMediaCaptureTypeData class]]) { [self writeByte:138]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:139]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKPermissionDecisionData class]]) { + } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { [self writeByte:140]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { + } else if ([value isKindOfClass:[FWFWKPermissionDecisionData class]]) { [self writeByte:141]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKSecurityOriginData class]]) { + } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { [self writeByte:142]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + } else if ([value isKindOfClass:[FWFWKSecurityOriginData class]]) { [self writeByte:143]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { [self writeByte:144]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { [self writeByte:145]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + [self writeByte:146]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -3181,3 +3283,143 @@ NSObject *FWFNSUrlFlutterApiGetCodec(void) { }]; } @end + +NSObject *FWFNSUrlCredentialHostApiGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; + return sSharedObject; +} + +void SetUpFWFNSUrlCredentialHostApi(id binaryMessenger, + NSObject *api) { + /// Create a new native instance and add it to the `InstanceManager`. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser" + binaryMessenger:binaryMessenger + codec:FWFNSUrlCredentialHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (createWithUserWithIdentifier:user:password:persistence:error:)], + @"FWFNSUrlCredentialHostApi api (%@) doesn't respond to " + @"@selector(createWithUserWithIdentifier:user:password:persistence:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_identifier = [GetNullableObjectAtIndex(args, 0) integerValue]; + NSString *arg_user = GetNullableObjectAtIndex(args, 1); + NSString *arg_password = GetNullableObjectAtIndex(args, 2); + FWFNSUrlCredentialPersistence arg_persistence = + [GetNullableObjectAtIndex(args, 3) integerValue]; + FlutterError *error; + [api createWithUserWithIdentifier:arg_identifier + user:arg_user + password:arg_password + persistence:arg_persistence + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +NSObject *FWFNSUrlProtectionSpaceFlutterApiGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; + return sSharedObject; +} + +@interface FWFNSUrlProtectionSpaceFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFNSUrlProtectionSpaceFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)createWithIdentifier:(NSInteger)arg_identifier + host:(nullable NSString *)arg_host + realm:(nullable NSString *)arg_realm + authenticationMethod:(nullable NSString *)arg_authenticationMethod + completion:(void (^)(FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlProtectionSpaceFlutterApi.create" + binaryMessenger:self.binaryMessenger + codec:FWFNSUrlProtectionSpaceFlutterApiGetCodec()]; + [channel + sendMessage:@[ + @(arg_identifier), arg_host ?: [NSNull null], arg_realm ?: [NSNull null], + arg_authenticationMethod ?: [NSNull null] + ] + reply:^(NSArray *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:@""]); + } + }]; +} +@end + +NSObject *FWFNSUrlAuthenticationChallengeFlutterApiGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; + return sSharedObject; +} + +@interface FWFNSUrlAuthenticationChallengeFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFNSUrlAuthenticationChallengeFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)createWithIdentifier:(NSInteger)arg_identifier + protectionSpaceIdentifier:(NSInteger)arg_protectionSpaceIdentifier + completion:(void (^)(FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview." + @"NSUrlAuthenticationChallengeFlutterApi.create" + binaryMessenger:self.binaryMessenger + codec:FWFNSUrlAuthenticationChallengeFlutterApiGetCodec()]; + [channel + sendMessage:@[ @(arg_identifier), @(arg_protectionSpaceIdentifier) ] + reply:^(NSArray *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:@""]); + } + }]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m index 30035d04e5..db8c456274 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m @@ -4,9 +4,13 @@ #import "FWFNavigationDelegateHostApi.h" #import "FWFDataConverters.h" +#import "FWFURLAuthenticationChallengeHostApi.h" #import "FWFWebViewConfigurationHostApi.h" @interface FWFNavigationDelegateFlutterApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; // InstanceManager must be weak to prevent a circular reference with the object it stores. @property(nonatomic, weak) FWFInstanceManager *instanceManager; @end @@ -16,6 +20,7 @@ instanceManager:(FWFInstanceManager *)instanceManager { self = [self initWithBinaryMessenger:binaryMessenger]; if (self) { + _binaryMessenger = binaryMessenger; _instanceManager = instanceManager; } return self; @@ -102,6 +107,37 @@ webViewIdentifier:webViewIdentifier completion:completion]; } + +- (void) + didReceiveAuthenticationChallengeForDelegate:(FWFNavigationDelegate *)instance + webView:(WKWebView *)webView + challenge:(NSURLAuthenticationChallenge *)challenge + completion: + (void (^)(FWFAuthenticationChallengeResponse *_Nullable, + FlutterError *_Nullable))completion { + NSInteger webViewIdentifier = + [self.instanceManager identifierWithStrongReferenceForInstance:webView]; + + FWFURLAuthenticationChallengeFlutterApiImpl *challengeApi = + [[FWFURLAuthenticationChallengeFlutterApiImpl alloc] + initWithBinaryMessenger:self.binaryMessenger + instanceManager:self.instanceManager]; + [challengeApi createWithInstance:challenge + protectionSpace:challenge.protectionSpace + completion:^(FlutterError *error) { + NSAssert(!error, @"%@", error); + }]; + + [self + didReceiveAuthenticationChallengeForDelegateWithIdentifier:[self + identifierForDelegate:instance] + webViewIdentifier:webViewIdentifier + challengeIdentifier: + [self.instanceManager + identifierWithStrongReferenceForInstance: + challenge] + completion:completion]; +} @end @implementation FWFNavigationDelegate @@ -144,8 +180,13 @@ completion:^(FWFWKNavigationActionPolicyEnumData *policy, FlutterError *error) { NSAssert(!error, @"%@", error); - decisionHandler( - FWFNativeWKNavigationActionPolicyFromEnumData(policy)); + if (!error) { + decisionHandler( + FWFNativeWKNavigationActionPolicyFromEnumData( + policy)); + } else { + decisionHandler(WKNavigationActionPolicyCancel); + } }]; } @@ -179,6 +220,40 @@ NSAssert(!error, @"%@", error); }]; } + +- (void)webView:(WKWebView *)webView + didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, + NSURLCredential *_Nullable))completionHandler { + [self.navigationDelegateAPI + didReceiveAuthenticationChallengeForDelegate:self + webView:webView + challenge:challenge + completion:^(FWFAuthenticationChallengeResponse *response, + FlutterError *error) { + NSAssert(!error, @"%@", error); + if (!error) { + NSURLSessionAuthChallengeDisposition disposition = + FWFNativeNSURLSessionAuthChallengeDispositionFromFWFNSUrlSessionAuthChallengeDisposition( + response.disposition); + + NSURLCredential *credential = + response.credentialIdentifier + ? (NSURLCredential *)[self.navigationDelegateAPI + .instanceManager + instanceForIdentifier: + response.credentialIdentifier + .longValue] + : nil; + + completionHandler(disposition, credential); + } else { + completionHandler( + NSURLSessionAuthChallengeCancelAuthenticationChallenge, + nil); + } + }]; +} @end @interface FWFNavigationDelegateHostApiImpl () diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLAuthenticationChallengeHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLAuthenticationChallengeHostApi.h new file mode 100644 index 0000000000..6100b703c2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLAuthenticationChallengeHostApi.h @@ -0,0 +1,33 @@ +// 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 "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Flutter API implementation for `NSURLAuthenticationChallenge`. + * + * This class may handle instantiating and adding Dart instances that are attached to a native + * instance or sending callback methods from an overridden native class. + */ +@interface FWFURLAuthenticationChallengeFlutterApiImpl : NSObject +/** + * The Flutter API used to send messages back to Dart. + */ +@property FWFNSUrlAuthenticationChallengeFlutterApi *api; +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +/** + * Sends a message to Dart to create a new Dart instance and add it to the `InstanceManager`. + */ +- (void)createWithInstance:(NSURLAuthenticationChallenge *)instance + protectionSpace:(NSURLProtectionSpace *)protectionSpace + completion:(void (^)(FlutterError *_Nullable))completion; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLAuthenticationChallengeHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLAuthenticationChallengeHostApi.m new file mode 100644 index 0000000000..965952dfeb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLAuthenticationChallengeHostApi.m @@ -0,0 +1,52 @@ +// 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 "FWFURLAuthenticationChallengeHostApi.h" +#import "FWFURLProtectionSpaceHostApi.h" + +@interface FWFURLAuthenticationChallengeFlutterApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFURLAuthenticationChallengeFlutterApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + _api = + [[FWFNSUrlAuthenticationChallengeFlutterApi alloc] initWithBinaryMessenger:binaryMessenger]; + } + return self; +} + +- (void)createWithInstance:(NSURLAuthenticationChallenge *)instance + protectionSpace:(NSURLProtectionSpace *)protectionSpace + completion:(void (^)(FlutterError *_Nullable))completion { + if ([self.instanceManager containsInstance:instance]) { + return; + } + + FWFURLProtectionSpaceFlutterApiImpl *protectionSpaceApi = + [[FWFURLProtectionSpaceFlutterApiImpl alloc] initWithBinaryMessenger:self.binaryMessenger + instanceManager:self.instanceManager]; + [protectionSpaceApi createWithInstance:protectionSpace + host:protectionSpace.host + realm:protectionSpace.realm + authenticationMethod:protectionSpace.authenticationMethod + completion:^(FlutterError *error) { + NSAssert(!error, @"%@", error); + }]; + + [self.api createWithIdentifier:[self.instanceManager addHostCreatedInstance:instance] + protectionSpaceIdentifier:[self.instanceManager + identifierWithStrongReferenceForInstance:protectionSpace] + completion:completion]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLCredentialHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLCredentialHostApi.h new file mode 100644 index 0000000000..fe9b3d0d8d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLCredentialHostApi.h @@ -0,0 +1,24 @@ +// 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 "FWFDataConverters.h" +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Host API implementation for `NSURLCredential`. + * + * This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or method calls on the associated native class or an instance of the class. + */ +@interface FWFURLCredentialHostApiImpl : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLCredentialHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLCredentialHostApi.m new file mode 100644 index 0000000000..2b6955ff28 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLCredentialHostApi.m @@ -0,0 +1,58 @@ +// 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 "FWFURLCredentialHostApi.h" + +@interface FWFURLCredentialHostApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFURLCredentialHostApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + } + return self; +} + +- (void)createWithUserWithIdentifier:(NSInteger)identifier + user:(nonnull NSString *)user + password:(nonnull NSString *)password + persistence:(FWFNSUrlCredentialPersistence)persistence + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [self.instanceManager + addDartCreatedInstance: + [NSURLCredential + credentialWithUser:user + password:password + persistence: + FWFNativeNSURLCredentialPersistenceFromFWFNSUrlCredentialPersistence( + persistence)] + withIdentifier:identifier]; +} + +- (nullable NSURL *)credentialForIdentifier:(NSNumber *)identifier + error: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSURL *instance = (NSURL *)[self.instanceManager instanceForIdentifier:identifier.longValue]; + + if (!instance) { + NSString *message = + [NSString stringWithFormat:@"InstanceManager does not contain an NSURL with identifier: %@", + identifier]; + *error = [FlutterError errorWithCode:NSInternalInconsistencyException + message:message + details:nil]; + } + + return instance; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLProtectionSpaceHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLProtectionSpaceHostApi.h new file mode 100644 index 0000000000..5e57ab5404 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLProtectionSpaceHostApi.h @@ -0,0 +1,35 @@ +// 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 "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Flutter API implementation for `NSURLProtectionSpace`. + * + * This class may handle instantiating and adding Dart instances that are attached to a native + * instance or sending callback methods from an overridden native class. + */ +@interface FWFURLProtectionSpaceFlutterApiImpl : NSObject +/** + * The Flutter API used to send messages back to Dart. + */ +@property FWFNSUrlProtectionSpaceFlutterApi *api; +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +/** + * Sends a message to Dart to create a new Dart instance and add it to the `InstanceManager`. + */ +- (void)createWithInstance:(NSURLProtectionSpace *)instance + host:(nullable NSString *)host + realm:(nullable NSString *)realm + authenticationMethod:(nullable NSString *)authenticationMethod + completion:(void (^)(FlutterError *_Nullable))completion; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLProtectionSpaceHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLProtectionSpaceHostApi.m new file mode 100644 index 0000000000..fc2d163070 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFURLProtectionSpaceHostApi.m @@ -0,0 +1,36 @@ +// 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 "FWFURLProtectionSpaceHostApi.h" + +@interface FWFURLProtectionSpaceFlutterApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFURLProtectionSpaceFlutterApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + _api = [[FWFNSUrlProtectionSpaceFlutterApi alloc] initWithBinaryMessenger:binaryMessenger]; + } + return self; +} + +- (void)createWithInstance:(NSURLProtectionSpace *)instance + host:(nullable NSString *)host + realm:(nullable NSString *)realm + authenticationMethod:(nullable NSString *)authenticationMethod + completion:(void (^)(FlutterError *_Nullable))completion { + if (![self.instanceManager containsInstance:instance]) { + [self.api createWithIdentifier:[self.instanceManager addHostCreatedInstance:instance] + host:host + realm:realm + authenticationMethod:authenticationMethod + completion:completion]; + } +} +@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 index 283682957c..6d97c07e37 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h @@ -16,7 +16,10 @@ #import "FWFScrollViewHostApi.h" #import "FWFUIDelegateHostApi.h" #import "FWFUIViewHostApi.h" +#import "FWFURLAuthenticationChallengeHostApi.h" +#import "FWFURLCredentialHostApi.h" #import "FWFURLHostApi.h" +#import "FWFURLProtectionSpaceHostApi.h" #import "FWFUserContentControllerHostApi.h" #import "FWFWebViewConfigurationHostApi.h" #import "FWFWebViewFlutterWKWebViewExternalAPI.h" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index ff08766ba9..1c632203f6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -202,6 +202,58 @@ enum WKMediaCaptureType { unknown, } +/// Responses to an authentication challenge. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition?language=objc. +enum NSUrlSessionAuthChallengeDisposition { + /// Use the specified credential, which may be nil. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengeusecredential?language=objc. + useCredential, + + /// Use the default handling for the challenge as though this delegate method + /// were not implemented. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengeperformdefaulthandling?language=objc. + performDefaultHandling, + + /// Cancel the entire request. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengecancelauthenticationchallenge?language=objc. + cancelAuthenticationChallenge, + + /// Reject this challenge, and call the authentication delegate method again + /// with the next authentication protection space. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengerejectprotectionspace?language=objc. + rejectProtectionSpace, +} + +/// Specifies how long a credential will be kept. +enum NSUrlCredentialPersistence { + /// The credential should not be stored. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencenone?language=objc. + none, + + /// The credential should be stored only for this session. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistenceforsession?language=objc. + session, + + /// The credential should be stored in the keychain. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencepermanent?language=objc. + permanent, + + /// The credential should be stored permanently in the keychain, and in + /// addition should be distributed to other devices based on the owning Apple + /// ID. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencesynchronizable?language=objc. + synchronizable, +} + class NSKeyValueObservingOptionsEnumData { NSKeyValueObservingOptionsEnumData({ required this.value, @@ -684,6 +736,33 @@ class ObjectOrIdentifier { } } +class AuthenticationChallengeResponse { + AuthenticationChallengeResponse({ + required this.disposition, + this.credentialIdentifier, + }); + + NSUrlSessionAuthChallengeDisposition disposition; + + int? credentialIdentifier; + + Object encode() { + return [ + disposition.index, + credentialIdentifier, + ]; + } + + static AuthenticationChallengeResponse decode(Object result) { + result as List; + return AuthenticationChallengeResponse( + disposition: + NSUrlSessionAuthChallengeDisposition.values[result[0]! as int], + credentialIdentifier: result[1] as int?, + ); + } +} + class _WKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { const _WKWebsiteDataStoreHostApiCodec(); @override @@ -1576,21 +1655,24 @@ class _WKNavigationDelegateFlutterApiCodec extends StandardMessageCodec { const _WKNavigationDelegateFlutterApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is NSErrorData) { + if (value is AuthenticationChallengeResponse) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is NSUrlRequestData) { + } else if (value is NSErrorData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is WKFrameInfoData) { + } else if (value is NSUrlRequestData) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKFrameInfoData) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionPolicyEnumData) { + } else if (value is WKNavigationActionData) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionPolicyEnumData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1600,14 +1682,16 @@ class _WKNavigationDelegateFlutterApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return NSErrorData.decode(readValue(buffer)!); + return AuthenticationChallengeResponse.decode(readValue(buffer)!); case 129: - return NSUrlRequestData.decode(readValue(buffer)!); + return NSErrorData.decode(readValue(buffer)!); case 130: - return WKFrameInfoData.decode(readValue(buffer)!); + return NSUrlRequestData.decode(readValue(buffer)!); case 131: - return WKNavigationActionData.decode(readValue(buffer)!); + return WKFrameInfoData.decode(readValue(buffer)!); case 132: + return WKNavigationActionData.decode(readValue(buffer)!); + case 133: return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1641,6 +1725,9 @@ abstract class WKNavigationDelegateFlutterApi { void webViewWebContentProcessDidTerminate( int identifier, int webViewIdentifier); + Future didReceiveAuthenticationChallenge( + int identifier, int webViewIdentifier, int challengeIdentifier); + static void setup(WKNavigationDelegateFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1842,6 +1929,41 @@ abstract class WKNavigationDelegateFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.WKNavigationDelegateFlutterApi.didReceiveAuthenticationChallenge', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKNavigationDelegateFlutterApi.didReceiveAuthenticationChallenge was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKNavigationDelegateFlutterApi.didReceiveAuthenticationChallenge was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKNavigationDelegateFlutterApi.didReceiveAuthenticationChallenge was null, expected non-null int.'); + final int? arg_challengeIdentifier = (args[2] as int?); + assert(arg_challengeIdentifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKNavigationDelegateFlutterApi.didReceiveAuthenticationChallenge was null, expected non-null int.'); + try { + final AuthenticationChallengeResponse output = + await api.didReceiveAuthenticationChallenge(arg_identifier!, + arg_webViewIdentifier!, arg_challengeIdentifier!); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } @@ -2082,60 +2204,63 @@ class _WKWebViewHostApiCodec extends StandardMessageCodec { const _WKWebViewHostApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is NSErrorData) { + if (value is AuthenticationChallengeResponse) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is NSHttpCookieData) { + } else if (value is NSErrorData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is NSHttpCookiePropertyKeyEnumData) { + } else if (value is NSHttpCookieData) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is NSKeyValueChangeKeyEnumData) { + } else if (value is NSHttpCookiePropertyKeyEnumData) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is NSKeyValueObservingOptionsEnumData) { + } else if (value is NSKeyValueChangeKeyEnumData) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is NSUrlRequestData) { + } else if (value is NSKeyValueObservingOptionsEnumData) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is ObjectOrIdentifier) { + } else if (value is NSUrlRequestData) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is WKAudiovisualMediaTypeEnumData) { + } else if (value is ObjectOrIdentifier) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is WKFrameInfoData) { + } else if (value is WKAudiovisualMediaTypeEnumData) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is WKMediaCaptureTypeData) { + } else if (value is WKFrameInfoData) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionPolicyEnumData) { + } else if (value is WKNavigationActionData) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is WKPermissionDecisionData) { + } else if (value is WKNavigationActionPolicyEnumData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is WKScriptMessageData) { + } else if (value is WKPermissionDecisionData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is WKSecurityOriginData) { + } else if (value is WKScriptMessageData) { buffer.putUint8(142); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptData) { + } else if (value is WKSecurityOriginData) { buffer.putUint8(143); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptInjectionTimeEnumData) { + } else if (value is WKUserScriptData) { buffer.putUint8(144); writeValue(buffer, value.encode()); - } else if (value is WKWebsiteDataTypeEnumData) { + } else if (value is WKUserScriptInjectionTimeEnumData) { buffer.putUint8(145); writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(146); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -2145,40 +2270,42 @@ class _WKWebViewHostApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return NSErrorData.decode(readValue(buffer)!); + return AuthenticationChallengeResponse.decode(readValue(buffer)!); case 129: - return NSHttpCookieData.decode(readValue(buffer)!); + return NSErrorData.decode(readValue(buffer)!); case 130: - return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + return NSHttpCookieData.decode(readValue(buffer)!); case 131: - return NSKeyValueChangeKeyEnumData.decode(readValue(buffer)!); + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); case 132: - return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + return NSKeyValueChangeKeyEnumData.decode(readValue(buffer)!); case 133: - return NSUrlRequestData.decode(readValue(buffer)!); + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); case 134: - return ObjectOrIdentifier.decode(readValue(buffer)!); + return NSUrlRequestData.decode(readValue(buffer)!); case 135: - return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + return ObjectOrIdentifier.decode(readValue(buffer)!); case 136: - return WKFrameInfoData.decode(readValue(buffer)!); + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); case 137: - return WKMediaCaptureTypeData.decode(readValue(buffer)!); + return WKFrameInfoData.decode(readValue(buffer)!); case 138: - return WKNavigationActionData.decode(readValue(buffer)!); + return WKMediaCaptureTypeData.decode(readValue(buffer)!); case 139: - return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + return WKNavigationActionData.decode(readValue(buffer)!); case 140: - return WKPermissionDecisionData.decode(readValue(buffer)!); + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); case 141: - return WKScriptMessageData.decode(readValue(buffer)!); + return WKPermissionDecisionData.decode(readValue(buffer)!); case 142: - return WKSecurityOriginData.decode(readValue(buffer)!); + return WKScriptMessageData.decode(readValue(buffer)!); case 143: - return WKUserScriptData.decode(readValue(buffer)!); + return WKSecurityOriginData.decode(readValue(buffer)!); case 144: - return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + return WKUserScriptData.decode(readValue(buffer)!); case 145: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + case 146: return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -3052,3 +3179,148 @@ abstract class NSUrlFlutterApi { } } } + +/// Host API for `NSUrlCredential`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlcredential?language=objc. +class NSUrlCredentialHostApi { + /// Constructor for [NSUrlCredentialHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NSUrlCredentialHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new native instance and add it to the `InstanceManager`. + Future createWithUser(int arg_identifier, String arg_user, + String arg_password, NSUrlCredentialPersistence arg_persistence) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send([ + arg_identifier, + arg_user, + arg_password, + arg_persistence.index + ]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +/// Flutter API for `NSUrlProtectionSpace`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlprotectionspace?language=objc. +abstract class NSUrlProtectionSpaceFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int identifier, String? host, String? realm, + String? authenticationMethod); + + static void setup(NSUrlProtectionSpaceFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlProtectionSpaceFlutterApi.create', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlProtectionSpaceFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlProtectionSpaceFlutterApi.create was null, expected non-null int.'); + final String? arg_host = (args[1] as String?); + final String? arg_realm = (args[2] as String?); + final String? arg_authenticationMethod = (args[3] as String?); + try { + api.create( + arg_identifier!, arg_host, arg_realm, arg_authenticationMethod); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} + +/// Flutter API for `NSUrlAuthenticationChallenge`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlauthenticationchallenge?language=objc. +abstract class NSUrlAuthenticationChallengeFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int identifier, int protectionSpaceIdentifier); + + static void setup(NSUrlAuthenticationChallengeFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlAuthenticationChallengeFlutterApi.create', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlAuthenticationChallengeFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlAuthenticationChallengeFlutterApi.create was null, expected non-null int.'); + final int? arg_protectionSpaceIdentifier = (args[1] as int?); + assert(arg_protectionSpaceIdentifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlAuthenticationChallengeFlutterApi.create was null, expected non-null int.'); + try { + api.create(arg_identifier!, arg_protectionSpaceIdentifier!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart index 3fe1a57c1b..2647ed95b8 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart @@ -9,6 +9,9 @@ import '../common/instance_manager.dart'; import '../common/weak_reference_utils.dart'; import 'foundation_api_impls.dart'; +export 'foundation_api_impls.dart' + show NSUrlSessionAuthChallengeDisposition, NSUrlCredentialPersistence; + /// The values that can be returned in a change map. /// /// Wraps [NSKeyValueObservingOptions](https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions?language=objc). @@ -387,3 +390,131 @@ class NSObject with Copyable { ); } } + +/// An authentication credential consisting of information specific to the type +/// of credential and the type of persistent storage to use, if any. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlcredential?language=objc. +class NSUrlCredential extends NSObject { + /// Creates a URL credential instance for internet password authentication + /// with a given user name and password, using a given persistence setting. + NSUrlCredential.withUser({ + required String user, + required String password, + required NSUrlCredentialPersistence persistence, + @visibleForTesting super.binaryMessenger, + @visibleForTesting super.instanceManager, + }) : _urlCredentialApi = NSUrlCredentialHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + super.detached() { + // Ensures Flutter Apis are setup. + FoundationFlutterApis.instance.ensureSetUp(); + _urlCredentialApi.createWithUserFromInstances( + this, + user, + password, + persistence, + ); + } + + /// Instantiates a [NSUrlCredential] without creating and attaching to an + /// instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an [InstanceManager]. + @protected + NSUrlCredential.detached({super.binaryMessenger, super.instanceManager}) + : _urlCredentialApi = NSUrlCredentialHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + super.detached(); + + final NSUrlCredentialHostApiImpl _urlCredentialApi; + + @override + NSObject copy() { + return NSUrlCredential.detached( + binaryMessenger: _urlCredentialApi.binaryMessenger, + instanceManager: _urlCredentialApi.instanceManager, + ); + } +} + +/// A server or an area on a server, commonly referred to as a realm, that +/// requires authentication. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlprotectionspace?language=objc. +class NSUrlProtectionSpace extends NSObject { + /// Instantiates a [NSUrlProtectionSpace] without creating and attaching to an + /// instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an [InstanceManager]. + @protected + NSUrlProtectionSpace.detached({ + required this.host, + required this.realm, + required this.authenticationMethod, + super.binaryMessenger, + super.instanceManager, + }) : super.detached(); + + /// The receiver’s host. + final String? host; + + /// The receiver’s authentication realm. + final String? realm; + + /// The authentication method used by the receiver. + final String? authenticationMethod; + + @override + NSUrlProtectionSpace copy() { + return NSUrlProtectionSpace.detached( + host: host, + realm: realm, + authenticationMethod: authenticationMethod, + ); + } +} + +/// The authentication method used by the receiver. +class NSUrlAuthenticationMethod { + /// Use the default authentication method for a protocol. + static const String default_ = 'NSURLAuthenticationMethodDefault'; + + /// Use HTML form authentication for this protection space. + static const String htmlForm = 'NSURLAuthenticationMethodHTMLForm'; + + /// Use HTTP basic authentication for this protection space. + static const String httpBasic = 'NSURLAuthenticationMethodHTTPBasic'; + + /// Use HTTP digest authentication for this protection space. + static const String httpDigest = 'NSURLAuthenticationMethodHTTPDigest'; +} + +/// A challenge from a server requiring authentication from the client. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlauthenticationchallenge?language=objc. +class NSUrlAuthenticationChallenge extends NSObject { + /// Instantiates a [NSUrlAuthenticationChallenge] without creating and + /// attaching to an instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an [InstanceManager]. + @protected + NSUrlAuthenticationChallenge.detached({ + required this.protectionSpace, + super.binaryMessenger, + super.instanceManager, + }) : super.detached(); + + /// The receiver’s protection space. + late final NSUrlProtectionSpace protectionSpace; + + @override + NSUrlAuthenticationChallenge copy() { + return NSUrlAuthenticationChallenge.detached( + protectionSpace: protectionSpace, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart index 4f73c08255..7bd1259e2f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart @@ -9,6 +9,9 @@ import '../common/instance_manager.dart'; import '../common/web_kit.g.dart'; import 'foundation.dart'; +export '../common/web_kit.g.dart' + show NSUrlSessionAuthChallengeDisposition, NSUrlCredentialPersistence; + Iterable _toNSKeyValueObservingOptionsEnumData( Iterable options, @@ -56,6 +59,14 @@ class FoundationFlutterApis { url = NSUrlFlutterApiImpl( binaryMessenger: binaryMessenger, instanceManager: instanceManager, + ), + urlProtectionSpace = NSUrlProtectionSpaceFlutterApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + urlAuthenticationChallenge = NSUrlAuthenticationChallengeFlutterApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, ); static FoundationFlutterApis _instance = FoundationFlutterApis(); @@ -82,6 +93,14 @@ class FoundationFlutterApis { @visibleForTesting final NSUrlFlutterApiImpl url; + /// Flutter Api for [NSUrlProtectionSpace]. + @visibleForTesting + final NSUrlProtectionSpaceFlutterApiImpl urlProtectionSpace; + + /// Flutter Api for [NSUrlAuthenticationChallenge]. + @visibleForTesting + final NSUrlAuthenticationChallengeFlutterApiImpl urlAuthenticationChallenge; + /// Ensures all the Flutter APIs have been set up to receive calls from native code. void ensureSetUp() { if (!_hasBeenSetUp) { @@ -90,6 +109,14 @@ class FoundationFlutterApis { binaryMessenger: _binaryMessenger, ); NSUrlFlutterApi.setup(url, binaryMessenger: _binaryMessenger); + NSUrlProtectionSpaceFlutterApi.setup( + urlProtectionSpace, + binaryMessenger: _binaryMessenger, + ); + NSUrlAuthenticationChallengeFlutterApi.setup( + urlAuthenticationChallenge, + binaryMessenger: _binaryMessenger, + ); _hasBeenSetUp = true; } } @@ -249,3 +276,121 @@ class NSUrlFlutterApiImpl implements NSUrlFlutterApi { ); } } + +/// Host api implementation for [NSUrlCredential]. +class NSUrlCredentialHostApiImpl extends NSUrlCredentialHostApi { + /// Constructs an [NSUrlCredentialHostApiImpl]. + NSUrlCredentialHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createWithUser] with the ids of the provided object instances. + Future createWithUserFromInstances( + NSUrlCredential instance, + String user, + String password, + NSUrlCredentialPersistence persistence, + ) { + return createWithUser( + instanceManager.addDartCreatedInstance(instance), + user, + password, + persistence, + ); + } +} + +/// Flutter API implementation for [NSUrlProtectionSpace]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +@protected +class NSUrlProtectionSpaceFlutterApiImpl + implements NSUrlProtectionSpaceFlutterApi { + /// Constructs a [NSUrlProtectionSpaceFlutterApiImpl]. + NSUrlProtectionSpaceFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create( + int identifier, + String? host, + String? realm, + String? authenticationMethod, + ) { + instanceManager.addHostCreatedInstance( + NSUrlProtectionSpace.detached( + host: host, + realm: realm, + authenticationMethod: authenticationMethod, + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + ); + } +} + +/// Flutter API implementation for [NSUrlAuthenticationChallenge]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +@protected +class NSUrlAuthenticationChallengeFlutterApiImpl + implements NSUrlAuthenticationChallengeFlutterApi { + /// Constructs a [NSUrlAuthenticationChallengeFlutterApiImpl]. + NSUrlAuthenticationChallengeFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create( + int identifier, + int protectionSpaceIdentifier, + ) { + instanceManager.addHostCreatedInstance( + NSUrlAuthenticationChallenge.detached( + protectionSpace: instanceManager.getInstanceWithWeakReference( + protectionSpaceIdentifier, + )!, + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index db9f41c285..b5bfcd9d34 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -830,6 +830,7 @@ class WKNavigationDelegate extends NSObject { this.didFailNavigation, this.didFailProvisionalNavigation, this.webViewWebContentProcessDidTerminate, + this.didReceiveAuthenticationChallenge, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -855,6 +856,7 @@ class WKNavigationDelegate extends NSObject { this.didFailNavigation, this.didFailProvisionalNavigation, this.webViewWebContentProcessDidTerminate, + this.didReceiveAuthenticationChallenge, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -901,6 +903,16 @@ class WKNavigationDelegate extends NSObject { /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function(WKWebView webView)? webViewWebContentProcessDidTerminate; + /// Called when the delegate needs a response to an authentication challenge. + final void Function( + WKWebView webView, + NSUrlAuthenticationChallenge challenge, + void Function( + NSUrlSessionAuthChallengeDisposition disposition, + NSUrlCredential? credential, + ) completionHandler, + )? didReceiveAuthenticationChallenge; + @override WKNavigationDelegate copy() { return WKNavigationDelegate.detached( @@ -911,6 +923,7 @@ class WKNavigationDelegate extends NSObject { didFailProvisionalNavigation: didFailProvisionalNavigation, webViewWebContentProcessDidTerminate: webViewWebContentProcessDidTerminate, + didReceiveAuthenticationChallenge: didReceiveAuthenticationChallenge, observeValue: observeValue, binaryMessenger: _navigationDelegateApi.binaryMessenger, instanceManager: _navigationDelegateApi.instanceManager, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart index ee545d45b7..bb86f2b5bc 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -904,6 +906,53 @@ class WKNavigationDelegateFlutterApiImpl as WKWebView, ); } + + @override + Future didReceiveAuthenticationChallenge( + int identifier, + int webViewIdentifier, + int challengeIdentifier, + ) async { + final void Function( + WKWebView webView, + NSUrlAuthenticationChallenge challenge, + void Function( + NSUrlSessionAuthChallengeDisposition disposition, + NSUrlCredential? credential, + ), + )? function = _getDelegate(identifier).didReceiveAuthenticationChallenge; + + if (function == null) { + return AuthenticationChallengeResponse( + disposition: NSUrlSessionAuthChallengeDisposition.rejectProtectionSpace, + ); + } + + final Completer responseCompleter = + Completer(); + + function.call( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + instanceManager.getInstanceWithWeakReference(challengeIdentifier)! + as NSUrlAuthenticationChallenge, + ( + NSUrlSessionAuthChallengeDisposition disposition, + NSUrlCredential? credential, + ) { + responseCompleter.complete( + AuthenticationChallengeResponse( + disposition: disposition, + credentialIdentifier: credential != null + ? instanceManager.getIdentifier(credential) + : null, + ), + ); + }, + ); + + return responseCompleter.future; + } } /// Host api implementation for [WKWebView]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart index 68ce913202..4b0c4ccd99 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart @@ -71,6 +71,14 @@ class WebKitProxy { void Function(WKWebView webView, NSError error)? didFailProvisionalNavigation, void Function(WKWebView webView)? webViewWebContentProcessDidTerminate, + void Function( + WKWebView webView, + NSUrlAuthenticationChallenge challenge, + void Function( + NSUrlSessionAuthChallengeDisposition disposition, + NSUrlCredential? credential, + ) completionHandler, + )? didReceiveAuthenticationChallenge, }) createNavigationDelegate; /// Constructs a [WKUIDelegate]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index d15b282518..62a4deb8ad 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -948,6 +948,54 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { ); } }, + didReceiveAuthenticationChallenge: ( + WKWebView webView, + NSUrlAuthenticationChallenge challenge, + void Function( + NSUrlSessionAuthChallengeDisposition disposition, + NSUrlCredential? credential, + ) completionHandler, + ) { + if (challenge.protectionSpace.authenticationMethod == + NSUrlAuthenticationMethod.httpBasic) { + final void Function(HttpAuthRequest)? callback = + weakThis.target?._onHttpAuthRequest; + final String? host = challenge.protectionSpace.host; + final String? realm = challenge.protectionSpace.realm; + + if (callback != null && host != null) { + callback( + HttpAuthRequest( + onProceed: (WebViewCredential credential) { + completionHandler( + NSUrlSessionAuthChallengeDisposition.useCredential, + NSUrlCredential.withUser( + user: credential.user, + password: credential.password, + persistence: NSUrlCredentialPersistence.session, + ), + ); + }, + onCancel: () { + completionHandler( + NSUrlSessionAuthChallengeDisposition + .cancelAuthenticationChallenge, + null, + ); + }, + host: host, + realm: realm, + ), + ); + return; + } + } + + completionHandler( + NSUrlSessionAuthChallengeDisposition.performDefaultHandling, + null, + ); + }, ); } @@ -960,6 +1008,7 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { WebResourceErrorCallback? _onWebResourceError; NavigationRequestCallback? _onNavigationRequest; UrlChangeCallback? _onUrlChange; + HttpAuthRequestCallback? _onHttpAuthRequest; @override Future setOnPageFinished(PageEventCallback onPageFinished) async { @@ -994,6 +1043,13 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { Future setOnUrlChange(UrlChangeCallback onUrlChange) async { _onUrlChange = onUrlChange; } + + @override + Future setOnHttpAuthRequest( + HttpAuthRequestCallback onHttpAuthRequest, + ) async { + _onHttpAuthRequest = onHttpAuthRequest; + } } /// WebKit implementation of [PlatformWebViewPermissionRequest]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index 8e9e16ff42..c4894692f5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -258,6 +258,58 @@ class WKMediaCaptureTypeData { late WKMediaCaptureType value; } +/// Responses to an authentication challenge. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition?language=objc. +enum NSUrlSessionAuthChallengeDisposition { + /// Use the specified credential, which may be nil. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengeusecredential?language=objc. + useCredential, + + /// Use the default handling for the challenge as though this delegate method + /// were not implemented. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengeperformdefaulthandling?language=objc. + performDefaultHandling, + + /// Cancel the entire request. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengecancelauthenticationchallenge?language=objc. + cancelAuthenticationChallenge, + + /// Reject this challenge, and call the authentication delegate method again + /// with the next authentication protection space. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlsessionauthchallengedisposition/nsurlsessionauthchallengerejectprotectionspace?language=objc. + rejectProtectionSpace, +} + +/// Specifies how long a credential will be kept. +enum NSUrlCredentialPersistence { + /// The credential should not be stored. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencenone?language=objc. + none, + + /// The credential should be stored only for this session. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistenceforsession?language=objc. + session, + + /// The credential should be stored in the keychain. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencepermanent?language=objc. + permanent, + + /// The credential should be stored permanently in the keychain, and in + /// addition should be distributed to other devices based on the owning Apple + /// ID. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlcredentialpersistence/nsurlcredentialpersistencesynchronizable?language=objc. + synchronizable, +} + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -343,6 +395,11 @@ class ObjectOrIdentifier { late bool isIdentifier; } +class AuthenticationChallengeResponse { + late NSUrlSessionAuthChallengeDisposition disposition; + late int? credentialIdentifier; +} + /// Mirror of WKWebsiteDataStore. /// /// See https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc. @@ -582,6 +639,16 @@ abstract class WKNavigationDelegateFlutterApi { int identifier, int webViewIdentifier, ); + + @async + @ObjCSelector( + 'didReceiveAuthenticationChallengeForDelegateWithIdentifier:webViewIdentifier:challengeIdentifier:', + ) + AuthenticationChallengeResponse didReceiveAuthenticationChallenge( + int identifier, + int webViewIdentifier, + int challengeIdentifier, + ); } /// Mirror of NSObject. @@ -781,3 +848,57 @@ abstract class NSUrlFlutterApi { @ObjCSelector('createWithIdentifier:') void create(int identifier); } + +/// Host API for `NSUrlCredential`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlcredential?language=objc. +@HostApi(dartHostTestHandler: 'TestNSUrlCredentialHostApi') +abstract class NSUrlCredentialHostApi { + /// Create a new native instance and add it to the `InstanceManager`. + @ObjCSelector( + 'createWithUserWithIdentifier:user:password:persistence:', + ) + void createWithUser( + int identifier, + String user, + String password, + NSUrlCredentialPersistence persistence, + ); +} + +/// Flutter API for `NSUrlProtectionSpace`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlprotectionspace?language=objc. +@FlutterApi() +abstract class NSUrlProtectionSpaceFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + @ObjCSelector('createWithIdentifier:host:realm:authenticationMethod:') + void create( + int identifier, + String? host, + String? realm, + String? authenticationMethod, + ); +} + +/// Flutter API for `NSUrlAuthenticationChallenge`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlauthenticationchallenge?language=objc. +@FlutterApi() +abstract class NSUrlAuthenticationChallengeFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + @ObjCSelector('createWithIdentifier:protectionSpaceIdentifier:') + void create(int identifier, int protectionSpaceIdentifier); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 4c7eca1467..a7e58858c0 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/packages/tree/main/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: 3.9.4 +version: 3.10.0 environment: sdk: ">=3.0.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: flutter: sdk: flutter path: ^1.8.0 - webview_flutter_platform_interface: ^2.6.0 + webview_flutter_platform_interface: ^2.7.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart index 1541f02adc..a9b0157783 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart @@ -1199,60 +1199,63 @@ class _TestWKWebViewHostApiCodec extends StandardMessageCodec { const _TestWKWebViewHostApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is NSErrorData) { + if (value is AuthenticationChallengeResponse) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is NSHttpCookieData) { + } else if (value is NSErrorData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is NSHttpCookiePropertyKeyEnumData) { + } else if (value is NSHttpCookieData) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is NSKeyValueChangeKeyEnumData) { + } else if (value is NSHttpCookiePropertyKeyEnumData) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is NSKeyValueObservingOptionsEnumData) { + } else if (value is NSKeyValueChangeKeyEnumData) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is NSUrlRequestData) { + } else if (value is NSKeyValueObservingOptionsEnumData) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is ObjectOrIdentifier) { + } else if (value is NSUrlRequestData) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is WKAudiovisualMediaTypeEnumData) { + } else if (value is ObjectOrIdentifier) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is WKFrameInfoData) { + } else if (value is WKAudiovisualMediaTypeEnumData) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is WKMediaCaptureTypeData) { + } else if (value is WKFrameInfoData) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionPolicyEnumData) { + } else if (value is WKNavigationActionData) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is WKPermissionDecisionData) { + } else if (value is WKNavigationActionPolicyEnumData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is WKScriptMessageData) { + } else if (value is WKPermissionDecisionData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is WKSecurityOriginData) { + } else if (value is WKScriptMessageData) { buffer.putUint8(142); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptData) { + } else if (value is WKSecurityOriginData) { buffer.putUint8(143); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptInjectionTimeEnumData) { + } else if (value is WKUserScriptData) { buffer.putUint8(144); writeValue(buffer, value.encode()); - } else if (value is WKWebsiteDataTypeEnumData) { + } else if (value is WKUserScriptInjectionTimeEnumData) { buffer.putUint8(145); writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(146); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1262,40 +1265,42 @@ class _TestWKWebViewHostApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return NSErrorData.decode(readValue(buffer)!); + return AuthenticationChallengeResponse.decode(readValue(buffer)!); case 129: - return NSHttpCookieData.decode(readValue(buffer)!); + return NSErrorData.decode(readValue(buffer)!); case 130: - return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + return NSHttpCookieData.decode(readValue(buffer)!); case 131: - return NSKeyValueChangeKeyEnumData.decode(readValue(buffer)!); + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); case 132: - return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + return NSKeyValueChangeKeyEnumData.decode(readValue(buffer)!); case 133: - return NSUrlRequestData.decode(readValue(buffer)!); + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); case 134: - return ObjectOrIdentifier.decode(readValue(buffer)!); + return NSUrlRequestData.decode(readValue(buffer)!); case 135: - return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + return ObjectOrIdentifier.decode(readValue(buffer)!); case 136: - return WKFrameInfoData.decode(readValue(buffer)!); + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); case 137: - return WKMediaCaptureTypeData.decode(readValue(buffer)!); + return WKFrameInfoData.decode(readValue(buffer)!); case 138: - return WKNavigationActionData.decode(readValue(buffer)!); + return WKMediaCaptureTypeData.decode(readValue(buffer)!); case 139: - return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + return WKNavigationActionData.decode(readValue(buffer)!); case 140: - return WKPermissionDecisionData.decode(readValue(buffer)!); + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); case 141: - return WKScriptMessageData.decode(readValue(buffer)!); + return WKPermissionDecisionData.decode(readValue(buffer)!); case 142: - return WKSecurityOriginData.decode(readValue(buffer)!); + return WKScriptMessageData.decode(readValue(buffer)!); case 143: - return WKUserScriptData.decode(readValue(buffer)!); + return WKSecurityOriginData.decode(readValue(buffer)!); case 144: - return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + return WKUserScriptData.decode(readValue(buffer)!); case 145: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + case 146: return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -2196,3 +2201,66 @@ abstract class TestNSUrlHostApi { } } } + +/// Host API for `NSUrlCredential`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlcredential?language=objc. +abstract class TestNSUrlCredentialHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new native instance and add it to the `InstanceManager`. + void createWithUser(int identifier, String user, String password, + NSUrlCredentialPersistence persistence); + + static void setup(TestNSUrlCredentialHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser was null, expected non-null int.'); + final String? arg_user = (args[1] as String?); + assert(arg_user != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser was null, expected non-null String.'); + final String? arg_password = (args[2] as String?); + assert(arg_password != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser was null, expected non-null String.'); + final NSUrlCredentialPersistence? arg_persistence = args[3] == null + ? null + : NSUrlCredentialPersistence.values[args[3]! as int]; + assert(arg_persistence != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.NSUrlCredentialHostApi.createWithUser was null, expected non-null NSUrlCredentialPersistence.'); + try { + api.createWithUser( + arg_identifier!, arg_user!, arg_password!, arg_persistence!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart index ea2e37e2cd..345c6fad67 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart @@ -17,6 +17,7 @@ import 'foundation_test.mocks.dart'; @GenerateMocks([ TestNSObjectHostApi, + TestNSUrlCredentialHostApi, TestNSUrlHostApi, ]) void main() { @@ -245,6 +246,102 @@ void main() { expect(instanceManager.getInstanceWithWeakReference(0), isA()); }); }); + + group('NSUrlCredential', () { + tearDown(() { + TestNSUrlCredentialHostApi.setup(null); + }); + + test('HostApi createWithUser', () { + final MockTestNSUrlCredentialHostApi mockApi = + MockTestNSUrlCredentialHostApi(); + TestNSUrlCredentialHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const String user = 'testString'; + const String password = 'testString2'; + + const NSUrlCredentialPersistence persistence = + NSUrlCredentialPersistence.permanent; + + final NSUrlCredential instance = NSUrlCredential.withUser( + user: user, + password: password, + persistence: persistence, + instanceManager: instanceManager, + ); + + verify(mockApi.createWithUser( + instanceManager.getIdentifier(instance), + user, + password, + persistence, + )); + }); + }); + + group('NSUrlProtectionSpace', () { + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final NSUrlProtectionSpaceFlutterApiImpl api = + NSUrlProtectionSpaceFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + api.create( + instanceIdentifier, + 'testString', + 'testString', + 'testAuthenticationMethod', + ); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); + }); + + group('NSUrlAuthenticationChallenge', () { + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final NSUrlAuthenticationChallengeFlutterApiImpl api = + NSUrlAuthenticationChallengeFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + const int protectionSpaceIdentifier = 1; + instanceManager.addHostCreatedInstance( + NSUrlProtectionSpace.detached( + host: null, + realm: null, + authenticationMethod: null, + instanceManager: instanceManager, + ), + protectionSpaceIdentifier, + ); + + api.create(instanceIdentifier, protectionSpaceIdentifier); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); + }); }); test('NSError', () { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart index 29d9e63951..ef23208841 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart @@ -78,6 +78,36 @@ class MockTestNSObjectHostApi extends _i1.Mock ); } +/// A class which mocks [TestNSUrlCredentialHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestNSUrlCredentialHostApi extends _i1.Mock + implements _i2.TestNSUrlCredentialHostApi { + MockTestNSUrlCredentialHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createWithUser( + int? identifier, + String? user, + String? password, + _i3.NSUrlCredentialPersistence? persistence, + ) => + super.noSuchMethod( + Invocation.method( + #createWithUser, + [ + identifier, + user, + password, + persistence, + ], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [TestNSUrlHostApi]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart index 98a70de7b3..2c95a52e1b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -683,6 +683,63 @@ void main() { expect(argsCompleter.future, completion([webView])); }); + + test('didReceiveAuthenticationChallenge', () async { + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + const int credentialIdentifier = 3; + final NSUrlCredential credential = NSUrlCredential.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + credential, + credentialIdentifier, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + didReceiveAuthenticationChallenge: ( + WKWebView webView, + NSUrlAuthenticationChallenge challenge, + void Function( + NSUrlSessionAuthChallengeDisposition disposition, + NSUrlCredential? credential, + ) completionHandler, + ) { + completionHandler( + NSUrlSessionAuthChallengeDisposition.useCredential, + credential, + ); + }, + ); + + const int challengeIdentifier = 27; + instanceManager.addHostCreatedInstance( + NSUrlAuthenticationChallenge.detached( + protectionSpace: NSUrlProtectionSpace.detached( + host: null, + realm: null, + authenticationMethod: null, + ), + instanceManager: instanceManager, + ), + challengeIdentifier, + ); + + final AuthenticationChallengeResponse response = await WebKitFlutterApis + .instance.navigationDelegate + .didReceiveAuthenticationChallenge( + instanceManager.getIdentifier(navigationDelegate)!, + instanceManager.getIdentifier(webView)!, + challengeIdentifier, + ); + + expect(response.disposition, + NSUrlSessionAuthChallengeDisposition.useCredential); + expect(response.credentialIdentifier, credentialIdentifier); + }); }); group('WKWebView', () { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart index 4581c92b78..63d432c6ac 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/src/webkit_proxy.dart'; @@ -214,6 +215,44 @@ void main() { expect(callbackRequest.url, 'https://www.google.com'); expect(callbackRequest.isMainFrame, isFalse); }); + + test('onHttpBasicAuthRequest emits host and realm', () { + final WebKitNavigationDelegate iosNavigationDelegate = + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + String? callbackHost; + String? callbackRealm; + + iosNavigationDelegate.setOnHttpAuthRequest((HttpAuthRequest request) { + callbackHost = request.host; + callbackRealm = request.realm; + }); + + const String expectedHost = 'expectedHost'; + const String expectedRealm = 'expectedRealm'; + + CapturingNavigationDelegate + .lastCreatedDelegate.didReceiveAuthenticationChallenge!( + WKWebView.detached(), + NSUrlAuthenticationChallenge.detached( + protectionSpace: NSUrlProtectionSpace.detached( + host: expectedHost, + realm: expectedRealm, + authenticationMethod: NSUrlAuthenticationMethod.httpBasic, + ), + ), + (NSUrlSessionAuthChallengeDisposition disposition, + NSUrlCredential? credential) {}); + + expect(callbackHost, expectedHost); + expect(callbackRealm, expectedRealm); + }); }); } @@ -226,6 +265,7 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { super.didFailProvisionalNavigation, super.decidePolicyForNavigationAction, super.webViewWebContentProcessDidTerminate, + super.didReceiveAuthenticationChallenge, }) : super.detached() { lastCreatedDelegate = this; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index 46badc0690..0bada4087f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -1366,6 +1366,7 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { super.didFailProvisionalNavigation, super.decidePolicyForNavigationAction, super.webViewWebContentProcessDidTerminate, + super.didReceiveAuthenticationChallenge, }) : super.detached() { lastCreatedDelegate = this; }