diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index 65bb3679..eabf1e7a 100755 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -2,6 +2,7 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md index 61595997..d86a1c31 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Added support for Dart null-safety feature - Added Android Hybrid Composition support "Use PlatformViewLink widget for Android WebView" [#462](https://github.com/pichillilorenzo/flutter_inappwebview/pull/462) (thanks to [plateaukao](https://github.com/plateaukao) and [tneotia](https://github.com/tneotia)) - Added `allowUniversalAccessFromFileURLs` and `allowFileAccessFromFileURLs` WebView options also for iOS (also thanks to [liranhao](https://github.com/liranhao)) +- Added limited cookies support on iOS below 11.0 using JavaScript - Updated integration tests - Merge "Upgraded appcompat to 1.2.0-rc-02" [#465](https://github.com/pichillilorenzo/flutter_inappwebview/pull/465) (thanks to [andreidiaconu](https://github.com/andreidiaconu)) - Merge "Added missing field 'headers' which returned by WebResourceResponse.toMap()" [#490](https://github.com/pichillilorenzo/flutter_inappwebview/pull/490) (thanks to [Doflatango](https://github.com/Doflatango)) @@ -13,6 +14,7 @@ - Merge "iOS ChromeSafariBrowserManager - Fixing unnecessary casting of rootViewController to FlutterViewController" [#567](https://github.com/pichillilorenzo/flutter_inappwebview/pull/567) (thanks to [gunantosteven](https://github.com/gunantosteven)) - Merge "Fix _channel.invokeMethod name for injectCSSFileFromUrl method" [#645](https://github.com/pichillilorenzo/flutter_inappwebview/pull/645) (thanks to [omralcrt](https://github.com/omralcrt)) - Merge "Add android media intents on wildcard input accept" [#620](https://github.com/pichillilorenzo/flutter_inappwebview/pull/620) (thanks to [cbodin](https://github.com/cbodin)) +- Merge "Add ChromeSafariBrowser support for Android 11" [#538](https://github.com/pichillilorenzo/flutter_inappwebview/pull/538) (thanks to [DRSchlaubi](https://github.com/DRSchlaubi)) - Fixed missing properties initialization when using InAppWebViewController.fromInAppBrowser - Fixed "Issue in Flutter web: 'Unsupported operation: Platform._operatingSystem'" [#507](https://github.com/pichillilorenzo/flutter_inappwebview/issues/507) - Fixed "window.flutter_inappwebview.callHandler is not a function" [#218](https://github.com/pichillilorenzo/flutter_inappwebview/issues/218) diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 9b4c8abe..c0913e90 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"android":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+8/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+2/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.0.4+3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-01-29 00:53:56.206541","version":"1.26.0-13.0.pre.194"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"device_info","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-2.0.0-nullsafety.2/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":["device_info"]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"android":[{"name":"device_info","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-2.0.0-nullsafety.2/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":["device_info"]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+8/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+2/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.0.4+3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"device_info","dependencies":[]},{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":["device_info"]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-01-30 14:49:49.654803","version":"1.26.0-18.0.pre.90"} \ No newline at end of file diff --git a/example/ios/Flutter/Flutter.podspec b/example/ios/Flutter/Flutter.podspec new file mode 100644 index 00000000..2c4421cf --- /dev/null +++ b/example/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# NOTE: This podspec is NOT to be published. It is only used as a local source! +# This is a generated file; do not edit or check into version control. +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'High-performance, high-fidelity mobile apps.' + s.homepage = 'https://flutter.io' + s.license = { :type => 'MIT' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '8.0' + # Framework linking is handled by Flutter tooling, not CocoaPods. + # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. + s.vendored_frameworks = 'path/to/nothing' +end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index db262e68..6ecfd663 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -254,6 +254,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/device_info/device_info.framework", "${BUILT_PRODUCTS_DIR}/flutter_downloader/flutter_downloader.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview/flutter_inappwebview.framework", "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", @@ -262,6 +263,7 @@ ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_downloader.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", diff --git a/ios/Classes/MyCookieManager.swift b/ios/Classes/MyCookieManager.swift index 8f20ec94..4b76bf82 100755 --- a/ios/Classes/MyCookieManager.swift +++ b/ios/Classes/MyCookieManager.swift @@ -146,7 +146,7 @@ class MyCookieManager: NSObject, FlutterPlugin { if let urlHost = URL(string: url)?.host { MyCookieManager.httpCookieStore!.getAllCookies { (cookies) in for cookie in cookies { - if urlHost.hasSuffix(cookie.domain) { + if urlHost.hasSuffix(cookie.domain) || cookie.domain.hasSuffix(urlHost) { var sameSite: String? = nil if #available(iOS 13.0, *) { if let sameSiteValue = cookie.sameSitePolicy?.rawValue { diff --git a/lib/src/cookie_manager.dart b/lib/src/cookie_manager.dart index dc738ac1..30fd2ecf 100755 --- a/lib/src/cookie_manager.dart +++ b/lib/src/cookie_manager.dart @@ -1,14 +1,21 @@ import 'dart:async'; +import 'dart:io'; +import 'package:device_info/device_info.dart'; import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'in_app_webview_controller.dart'; +import 'headless_in_app_webview.dart'; import 'types.dart'; ///Class that implements a singleton object (shared instance) which manages the cookies used by WebView instances. ///On Android, it is implemented using [CookieManager](https://developer.android.com/reference/android/webkit/CookieManager). ///On iOS, it is implemented using [WKHTTPCookieStore](https://developer.apple.com/documentation/webkit/wkhttpcookiestore). /// -///**NOTE for iOS**: available from iOS 11.0+. +///**NOTE for iOS below 11.0 (LIMITED SUPPORT!)**: in this case, almost all of the methods ([CookieManager.deleteAllCookies] is not supported!) +///has been implemented using JavaScript because there is no other way to work with them on iOS below 11.0. +///See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies for JavaScript restrictions. class CookieManager { static CookieManager? _instance; static const MethodChannel _channel = const MethodChannel( @@ -27,10 +34,17 @@ class CookieManager { static Future _handleMethod(MethodCall call) async {} - ///Sets a cookie for the given [url]. Any existing cookie with the same [host], [path] and [name] will be replaced with the new cookie. The cookie being set will be ignored if it is expired. + ///Sets a cookie for the given [url]. Any existing cookie with the same [host], [path] and [name] will be replaced with the new cookie. + ///The cookie being set will be ignored if it is expired. /// ///The default value of [path] is `"/"`. ///If [domain] is `null`, its default value will be the domain name of [url]. + /// + ///[iosBelow11WebViewController] could be used if you need to set a session-only cookie using JavaScript (so [isHttpOnly] cannot be set, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) + ///on the current URL of the [WebView] managed by that controller when you need to target iOS below 11. In this case the [url] parameter is ignored. + /// + ///**NOTE for iOS below 11.0**: If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///to set the cookie (session-only cookie won't work! In that case, you should set also [expiresDate] or [maxAge]). Future setCookie( {required String url, required String name, @@ -41,7 +55,8 @@ class CookieManager { int? maxAge, bool? isSecure, bool? isHttpOnly, - HTTPCookieSameSitePolicy? sameSite}) async { + HTTPCookieSameSitePolicy? sameSite, + InAppWebViewController? iosBelow11WebViewController}) async { if (domain == null) domain = _getDomainName(url); assert(url.isNotEmpty); @@ -50,6 +65,56 @@ class CookieManager { assert(domain.isNotEmpty); assert(path.isNotEmpty); + if (Platform.isIOS) { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + + var version = double.tryParse(iosInfo.systemVersion); + + if (version != null && version < 11.0) { + var cookieValue = name + "=" + value + "; Domain=" + domain + "; Path=" + path; + + if (expiresDate != null) + cookieValue += "; Expires=" + _getCookieExpirationDate(expiresDate); + + if (maxAge != null) + cookieValue += "; Max-Age=" + maxAge.toString(); + + if (isSecure != null && isSecure) + cookieValue += "; Secure"; + + if (sameSite != null) + cookieValue += "; SameSite=" + sameSite.toValue(); + + cookieValue += ";"; + + if (iosBelow11WebViewController != null) { + InAppWebViewGroupOptions? options = await iosBelow11WebViewController.getOptions(); + if (options != null && options.crossPlatform != null && + options.crossPlatform!.javaScriptEnabled == true) { + await iosBelow11WebViewController.evaluateJavascript( + source: 'document.cookie="$cookieValue"'); + return; + } + } + + var setCookieCompleter = Completer(); + var headlessWebView = new HeadlessInAppWebView( + initialUrl: url, + onLoadStop: (controller, url) async { + await controller.evaluateJavascript( + source: 'document.cookie="$cookieValue"'); + setCookieCompleter.complete(); + }, + ); + await headlessWebView.run(); + await setCookieCompleter.future; + await headlessWebView.dispose(); + + return; + } + } + Map args = {}; args.putIfAbsent('url', () => url); args.putIfAbsent('name', () => name); @@ -66,38 +131,157 @@ class CookieManager { } ///Gets all the cookies for the given [url]. - Future> getCookies({required String url}) async { + /// + ///[iosBelow11WebViewController] is used for getting the cookies (also session-only cookies) using JavaScript (cookies with `isHttpOnly` enabled cannot be found, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11. JavaScript must be enabled in order to work. + ///In this case the [url] parameter is ignored. + /// + ///**NOTE for iOS below 11.0**: All the cookies returned this way will have all the properties to `null` except for [Cookie.name] and [Cookie.value]. + ///If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///to get the cookies (session-only cookies and cookies with `isHttpOnly` enabled won't be found!). + Future> getCookies({required String url, InAppWebViewController? iosBelow11WebViewController}) async { assert(url.isNotEmpty); + List cookies = []; + + if (Platform.isIOS) { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + + var version = double.tryParse(iosInfo.systemVersion); + + if (version != null && version < 11.0) { + + if (iosBelow11WebViewController != null) { + InAppWebViewGroupOptions? options = await iosBelow11WebViewController.getOptions(); + if (options != null && options.crossPlatform != null && + options.crossPlatform!.javaScriptEnabled == true) { + List documentCookies = (await iosBelow11WebViewController.evaluateJavascript(source: 'document.cookie') as String) + .split(';').map((documentCookie) => documentCookie.trim()).toList(); + documentCookies.forEach((documentCookie) { + List cookie = documentCookie.split('='); + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + ) + ); + }); + return cookies; + } + } + + var pageLoaded = Completer(); + var headlessWebView = new HeadlessInAppWebView( + initialUrl: url, + onLoadStop: (controller, url) async { + pageLoaded.complete(); + }, + ); + await headlessWebView.run(); + await pageLoaded.future; + + List documentCookies = (await headlessWebView.webViewController.evaluateJavascript(source: 'document.cookie') as String) + .split(';').map((documentCookie) => documentCookie.trim()).toList(); + documentCookies.forEach((documentCookie) { + List cookie = documentCookie.split('='); + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + ) + ); + }); + await headlessWebView.dispose(); + return cookies; + } + } + Map args = {}; args.putIfAbsent('url', () => url); List cookieListMap = await _channel.invokeMethod('getCookies', args); cookieListMap = cookieListMap.cast>(); - List cookies = []; - for (var i = 0; i < cookieListMap.length; i++) { + cookieListMap.forEach((cookieMap) { cookies.add(Cookie( - name: cookieListMap[i]["name"], - value: cookieListMap[i]["value"], - expiresDate: cookieListMap[i]["expiresDate"], - isSessionOnly: cookieListMap[i]["isSessionOnly"], - domain: cookieListMap[i]["domain"], + name: cookieMap["name"], + value: cookieMap["value"], + expiresDate: cookieMap["expiresDate"], + isSessionOnly: cookieMap["isSessionOnly"], + domain: cookieMap["domain"], sameSite: - HTTPCookieSameSitePolicy.fromValue(cookieListMap[i]["sameSite"]), - isSecure: cookieListMap[i]["isSecure"], - isHttpOnly: cookieListMap[i]["isHttpOnly"], - path: cookieListMap[i]["path"])); - } + HTTPCookieSameSitePolicy.fromValue(cookieMap["sameSite"]), + isSecure: cookieMap["isSecure"], + isHttpOnly: cookieMap["isHttpOnly"], + path: cookieMap["path"])); + }); return cookies; } ///Gets a cookie by its [name] for the given [url]. + /// + ///[iosBelow11WebViewController] is used for getting the cookie (also session-only cookie) using JavaScript (cookie with `isHttpOnly` enabled cannot be found, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11. JavaScript must be enabled in order to work. + ///In this case the [url] parameter is ignored. + /// + ///**NOTE for iOS below 11.0**: All the cookies returned this way will have all the properties to `null` except for [Cookie.name] and [Cookie.value]. + ///If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///to get the cookie (session-only cookie and cookie with `isHttpOnly` enabled won't be found!). Future getCookie( - {required String url, required String name}) async { + {required String url, required String name, InAppWebViewController? iosBelow11WebViewController}) async { + assert(url.isNotEmpty); assert(name.isNotEmpty); + if (Platform.isIOS) { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + + var version = double.tryParse(iosInfo.systemVersion); + + if (version != null && version < 11.0) { + + if (iosBelow11WebViewController != null) { + InAppWebViewGroupOptions? options = await iosBelow11WebViewController.getOptions(); + if (options != null && options.crossPlatform != null && + options.crossPlatform!.javaScriptEnabled == true) { + List documentCookies = (await iosBelow11WebViewController.evaluateJavascript(source: 'document.cookie') as String) + .split(';').map((documentCookie) => documentCookie.trim()).toList(); + for (var i = 0; i < documentCookies.length; i++) { + List cookie = documentCookies[i].split('='); + if (cookie[0] == name) + return Cookie( + name: cookie[0], + value: cookie[1]); + } + return null; + } + } + + var pageLoaded = Completer(); + var headlessWebView = new HeadlessInAppWebView( + initialUrl: url, + onLoadStop: (controller, url) async { + pageLoaded.complete(); + }, + ); + await headlessWebView.run(); + await pageLoaded.future; + + List documentCookies = (await headlessWebView.webViewController.evaluateJavascript(source: 'document.cookie') as String) + .split(';').map((documentCookie) => documentCookie.trim()).toList(); + await headlessWebView.dispose(); + + for (var i = 0; i < documentCookies.length; i++) { + List cookie = documentCookies[i].split('='); + if (cookie[0] == name) + return Cookie( + name: cookie[0], + value: cookie[1]); + } + return null; + } + } + Map args = {}; args.putIfAbsent('url', () => url); List cookies = await _channel.invokeMethod('getCookies', args); @@ -123,18 +307,34 @@ class CookieManager { ///Removes a cookie by its [name] for the given [url], [domain] and [path]. /// ///The default value of [path] is `"/"`. - ///If [domain] is `null` or empty, its default value will be the domain name of [url]. + ///If [domain] is empty, its default value will be the domain name of [url]. + /// + ///[iosBelow11WebViewController] is used for deleting the cookie (also session-only cookie) using JavaScript (cookie with `isHttpOnly` enabled cannot be deleted, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11. JavaScript must be enabled in order to work. + ///In this case the [url] parameter is ignored. + /// + ///**NOTE for iOS below 11.0**: If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///to delete the cookie (session-only cookie and cookie with `isHttpOnly` enabled won't be deleted!). Future deleteCookie( {required String url, required String name, String domain = "", - String path = "/"}) async { + String path = "/", + InAppWebViewController? iosBelow11WebViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); assert(url.isNotEmpty); assert(name.isNotEmpty); - assert(url.isNotEmpty); - assert(url.isNotEmpty); + + if (Platform.isIOS) { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + var version = double.tryParse(iosInfo.systemVersion); + if (version != null && version < 11.0) { + await setCookie(url: url, name: name, value: "", path: path, domain: domain, maxAge: -1, iosBelow11WebViewController: iosBelow11WebViewController); + return; + } + } Map args = {}; args.putIfAbsent('url', () => url); @@ -147,14 +347,33 @@ class CookieManager { ///Removes all cookies for the given [url], [domain] and [path]. /// ///The default value of [path] is `"/"`. - ///If [domain] is `null` or empty, its default value will be the domain name of [url]. + ///If [domain] is empty, its default value will be the domain name of [url]. + /// + ///[iosBelow11WebViewController] is used for deleting the cookies (also session-only cookies) using JavaScript (cookies with `isHttpOnly` enabled cannot be deleted, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11. JavaScript must be enabled in order to work. + ///In this case the [url] parameter is ignored. + /// + ///**NOTE for iOS below 11.0**: If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///to delete the cookies (session-only cookies and cookies with `isHttpOnly` enabled won't be deleted!). Future deleteCookies( - {required String url, String domain = "", String path = "/"}) async { + {required String url, String domain = "", String path = "/", + InAppWebViewController? iosBelow11WebViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); assert(url.isNotEmpty); - assert(url.isNotEmpty); - assert(url.isNotEmpty); + + if (Platform.isIOS) { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + var version = double.tryParse(iosInfo.systemVersion); + if (version != null && version < 11.0) { + List cookies = await getCookies(url: url, iosBelow11WebViewController: iosBelow11WebViewController); + for (var i = 0; i < cookies.length; i++) { + await setCookie(url: url, name: cookies[i].name, value: "", path: path, domain: domain, maxAge: -1, iosBelow11WebViewController: iosBelow11WebViewController); + } + return; + } + } Map args = {}; args.putIfAbsent('url', () => url); @@ -164,6 +383,8 @@ class CookieManager { } ///Removes all cookies. + /// + ///**NOTE for iOS**: available from iOS 11.0+. Future deleteAllCookies() async { Map args = {}; await _channel.invokeMethod('deleteAllCookies', args); @@ -176,4 +397,9 @@ class CookieManager { if (domain == null) return ""; return domain.startsWith("www.") ? domain.substring(4) : domain; } + + String _getCookieExpirationDate(int expiresDate) { + var dateTime = DateTime.fromMillisecondsSinceEpoch(expiresDate).toUtc(); + return DateFormat('EEE, d MMM yyyy hh:mm:ss', "en_US").format(dateTime) + ' GMT'; + } } diff --git a/pubspec.yaml b/pubspec.yaml index b12043fb..66a46dfc 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,8 @@ dependencies: sdk: flutter uuid: ^3.0.0-nullsafety.0 mime: ^1.0.0-nullsafety.0 + intl: ^0.17.0-nullsafety.2 + device_info: ^2.0.0-nullsafety.2 dev_dependencies: flutter_test: