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 below 11.0 (LIMITED SUPPORT!)**: in this case, almost all of the methods ([CookieManager.deleteAllCookies] and [IOSCookieManager.getAllCookies] are 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( 'com.pichillilorenzo/flutter_inappwebview_cookiemanager'); ///Contains only iOS-specific methods of [CookieManager]. late IOSCookieManager ios; ///Gets the [CookieManager] shared instance. static CookieManager instance() { return (_instance != null) ? _instance! : _init(); } static CookieManager _init() { _channel.setMethodCallHandler(_handleMethod); _instance = CookieManager(); _instance!.ios = IOSCookieManager.instance(); return _instance!; } 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. /// ///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, required String value, String? domain, String path = "/", int? expiresDate, int? maxAge, bool? isSecure, bool? isHttpOnly, HTTPCookieSameSitePolicy? sameSite, InAppWebViewController? iosBelow11WebViewController}) async { if (domain == null) domain = _getDomainName(url); assert(url.isNotEmpty); assert(name.isNotEmpty); assert(value.isNotEmpty); 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); args.putIfAbsent('value', () => value); args.putIfAbsent('domain', () => domain); args.putIfAbsent('path', () => path); args.putIfAbsent('expiresDate', () => expiresDate?.toString()); args.putIfAbsent('maxAge', () => maxAge); args.putIfAbsent('isSecure', () => isSecure); args.putIfAbsent('isHttpOnly', () => isHttpOnly); args.putIfAbsent('sameSite', () => sameSite?.toValue()); await _channel.invokeMethod('setCookie', args); } ///Gets all the cookies for the given [url]. /// ///[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>(); cookieListMap.forEach((cookieMap) { cookies.add(Cookie( name: cookieMap["name"], value: cookieMap["value"], expiresDate: cookieMap["expiresDate"], isSessionOnly: cookieMap["isSessionOnly"], domain: cookieMap["domain"], sameSite: 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, 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); cookies = cookies.cast>(); for (var i = 0; i < cookies.length; i++) { cookies[i] = cookies[i].cast(); if (cookies[i]["name"] == name) return Cookie( name: cookies[i]["name"], value: cookies[i]["value"], expiresDate: cookies[i]["expiresDate"], isSessionOnly: cookies[i]["isSessionOnly"], domain: cookies[i]["domain"], sameSite: HTTPCookieSameSitePolicy.fromValue(cookies[i]["sameSite"]), isSecure: cookies[i]["isSecure"], isHttpOnly: cookies[i]["isHttpOnly"], path: cookies[i]["path"]); } return null; } ///Removes a cookie by its [name] for the given [url], [domain] and [path]. /// ///The default value of [path] is `"/"`. ///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 = "/", InAppWebViewController? iosBelow11WebViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); 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) { await setCookie(url: url, name: name, value: "", path: path, domain: domain, maxAge: -1, iosBelow11WebViewController: iosBelow11WebViewController); return; } } Map args = {}; args.putIfAbsent('url', () => url); args.putIfAbsent('name', () => name); args.putIfAbsent('domain', () => domain); args.putIfAbsent('path', () => path); await _channel.invokeMethod('deleteCookie', args); } ///Removes all cookies for the given [url], [domain] and [path]. /// ///The default value of [path] is `"/"`. ///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 = "/", InAppWebViewController? iosBelow11WebViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); 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); args.putIfAbsent('domain', () => domain); args.putIfAbsent('path', () => path); await _channel.invokeMethod('deleteCookies', args); } ///Removes all cookies. /// ///**NOTE for iOS**: available from iOS 11.0+. Future deleteAllCookies() async { Map args = {}; await _channel.invokeMethod('deleteAllCookies', args); } String _getDomainName(String url) { Uri uri = Uri.parse(url); String domain = uri.host; // ignore: unnecessary_null_comparison 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'; } } ///Class that contains only iOS-specific methods of [CookieManager]. class IOSCookieManager { static IOSCookieManager? _instance; ///Gets the [IOSCookieManager] shared instance. static IOSCookieManager instance() { return (_instance != null) ? _instance! : _init(); } static IOSCookieManager _init() { _instance = IOSCookieManager(); return _instance!; } ///Fetches all stored cookies. /// ///**NOTE**: available on iOS 11.0+. /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882005-getallcookies Future> getAllCookies() async { List cookies = []; Map args = {}; List cookieListMap = await CookieManager._channel.invokeMethod('getAllCookies', args); cookieListMap = cookieListMap.cast>(); cookieListMap.forEach((cookieMap) { cookies.add(Cookie( name: cookieMap["name"], value: cookieMap["value"], expiresDate: cookieMap["expiresDate"], isSessionOnly: cookieMap["isSessionOnly"], domain: cookieMap["domain"], sameSite: HTTPCookieSameSitePolicy.fromValue(cookieMap["sameSite"]), isSecure: cookieMap["isSecure"], isHttpOnly: cookieMap["isHttpOnly"], path: cookieMap["path"])); }); return cookies; } }