From bb33ec2362d6007fe6aca3085fd5a4a4c3a34dd7 Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Mon, 25 Apr 2022 22:36:21 +0200 Subject: [PATCH] added headless and cookie manager web support --- example/lib/in_app_webiew_example.screen.dart | 10 +- lib/assets/web/web_support.js | 620 +++++++++--------- .../chrome_safari_browser.dart | 4 + lib/src/context_menu.dart | 4 + lib/src/cookie_manager.dart | 195 ++++-- lib/src/http_auth_credentials_database.dart | 4 + lib/src/in_app_browser/in_app_browser.dart | 4 + lib/src/in_app_localhost_server.dart | 4 + .../headless_in_app_webview.dart | 5 + lib/src/in_app_webview/in_app_webview.dart | 5 + .../in_app_webview_settings.dart | 3 + lib/src/platform_util.dart | 13 + .../pull_to_refresh_controller.dart | 4 + lib/src/types.dart | 14 + .../headless_in_app_web_view_web_element.dart | 67 ++ .../web/headless_inappwebview_manager.dart | 62 ++ lib/src/web/in_app_web_view_web_element.dart | 82 ++- lib/src/web/platform_util.dart | 46 ++ lib/src/web/web_platform.dart | 23 +- lib/src/web/web_platform_manager.dart | 3 +- 20 files changed, 762 insertions(+), 410 deletions(-) create mode 100644 lib/src/web/headless_in_app_web_view_web_element.dart create mode 100644 lib/src/web/headless_inappwebview_manager.dart create mode 100644 lib/src/web/platform_util.dart diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index 825fc26c..a8d56638 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -24,8 +24,6 @@ class _InAppWebViewExampleScreenState extends State { allowsInlineMediaPlayback: true, iframeAllow: "camera; microphone", iframeAllowFullscreen: true, - disableVerticalScroll: true, - disableHorizontalScroll: true, ); PullToRefreshController? pullToRefreshController; @@ -123,7 +121,7 @@ class _InAppWebViewExampleScreenState extends State { initialUserScripts: UnmodifiableListView([]), initialSettings: settings, pullToRefreshController: pullToRefreshController, - onWebViewCreated: (controller) { + onWebViewCreated: (controller) async { webViewController = controller; }, onLoadStart: (controller, url) async { @@ -162,17 +160,11 @@ class _InAppWebViewExampleScreenState extends State { return NavigationActionPolicy.ALLOW; }, onLoadStop: (controller, url) async { - print("onLoadStop"); pullToRefreshController?.endRefreshing(); setState(() { this.url = url.toString(); urlController.text = this.url; }); - await Future.delayed(Duration(seconds: 2)); - await controller.setSettings(settings: settings.copy() - ..disableVerticalScroll = false - ..disableHorizontalScroll = false - ); }, onLoadError: (controller, url, code, message) { pullToRefreshController?.endRefreshing(); diff --git a/lib/assets/web/web_support.js b/lib/assets/web/web_support.js index fc2d3b24..5e2b18a6 100644 --- a/lib/assets/web/web_support.js +++ b/lib/assets/web/web_support.js @@ -1,337 +1,365 @@ window.flutter_inappwebview = { - viewId: null, - iframeId: null, - iframe: null, - windowAutoincrementId: 0, - windows: {}, - isFullscreen: false, - documentTitle: null, - functionMap: {}, - settings: {}, - prepare: function (settings) { - window.flutter_inappwebview.settings = settings; - var iframe = document.getElementById(window.flutter_inappwebview.iframeId); + webViews: {}, + createFlutterInAppWebView: function(viewId, iframeId) { + var webView = { + viewId: viewId, + iframeId: iframeId, + iframe: null, + windowAutoincrementId: 0, + windows: {}, + isFullscreen: false, + documentTitle: null, + functionMap: {}, + settings: {}, + disableContextMenuHandler: function(event) { + event.preventDefault(); + event.stopPropagation(); + return false; + }, + prepare: function (settings) { + webView.settings = settings; + var iframe = document.getElementById(iframeId); - document.addEventListener('fullscreenchange', function(event) { - // document.fullscreenElement will point to the element that - // is in fullscreen mode if there is one. If there isn't one, - // the value of the property is null. - if (document.fullscreenElement && document.fullscreenElement.id == window.flutter_inappwebview.iframeId) { - window.flutter_inappwebview.isFullscreen = true; - window.flutter_inappwebview.nativeCommunication('onEnterFullscreen', window.flutter_inappwebview.viewId); - } else if (!document.fullscreenElement && window.flutter_inappwebview.isFullscreen) { - window.flutter_inappwebview.isFullscreen = false; - window.flutter_inappwebview.nativeCommunication('onExitFullscreen', window.flutter_inappwebview.viewId); - } else { - window.flutter_inappwebview.isFullscreen = false; - } - }); + document.addEventListener('fullscreenchange', function(event) { + // document.fullscreenElement will point to the element that + // is in fullscreen mode if there is one. If there isn't one, + // the value of the property is null. + if (document.fullscreenElement && document.fullscreenElement.id == iframeId) { + webView.isFullscreen = true; + window.flutter_inappwebview.nativeCommunication('onEnterFullscreen', viewId); + } else if (!document.fullscreenElement && webView.isFullscreen) { + webView.isFullscreen = false; + window.flutter_inappwebview.nativeCommunication('onExitFullscreen', viewId); + } else { + webView.isFullscreen = false; + } + }); - if (iframe != null) { - window.flutter_inappwebview.iframe = iframe; - iframe.addEventListener('load', function (event) { - window.flutter_inappwebview.windowAutoincrementId = 0; - window.flutter_inappwebview.windows = {}; + if (iframe != null) { + webView.iframe = iframe; + iframe.addEventListener('load', function (event) { + webView.windowAutoincrementId = 0; + webView.windows = {}; - var url = iframe.src; - try { - url = iframe.contentWindow.location.href; - } catch (e) { - console.log(e); - } - window.flutter_inappwebview.nativeCommunication('onLoadStart', window.flutter_inappwebview.viewId, [url]); + var url = iframe.src; + try { + url = iframe.contentWindow.location.href; + } catch (e) { + console.log(e); + } + window.flutter_inappwebview.nativeCommunication('onLoadStart', viewId, [url]); - try { - var oldLogs = { - 'log': iframe.contentWindow.console.log, - 'debug': iframe.contentWindow.console.debug, - 'error': iframe.contentWindow.console.error, - 'info': iframe.contentWindow.console.info, - 'warn': iframe.contentWindow.console.warn - }; - for (var k in oldLogs) { - (function(oldLog) { - iframe.contentWindow.console[oldLog] = function() { - var message = ''; - for (var i in arguments) { - if (message == '') { - message += arguments[i]; - } else { - message += ' ' + arguments[i]; + try { + var oldLogs = { + 'log': iframe.contentWindow.console.log, + 'debug': iframe.contentWindow.console.debug, + 'error': iframe.contentWindow.console.error, + 'info': iframe.contentWindow.console.info, + 'warn': iframe.contentWindow.console.warn + }; + for (var k in oldLogs) { + (function(oldLog) { + iframe.contentWindow.console[oldLog] = function() { + var message = ''; + for (var i in arguments) { + if (message == '') { + message += arguments[i]; + } else { + message += ' ' + arguments[i]; + } + } + oldLogs[oldLog].call(iframe.contentWindow.console, ...arguments); + window.flutter_inappwebview.nativeCommunication('onConsoleMessage', viewId, [oldLog, message]); } + })(k); + } + } catch (e) { + console.log(e); + } + + try { + var originalPushState = iframe.contentWindow.history.pushState; + iframe.contentWindow.history.pushState = function (state, unused, url) { + originalPushState.call(iframe.contentWindow.history, state, unused, url); + var iframeUrl = iframe.src; + try { + iframeUrl = iframe.contentWindow.location.href; + } catch (e) { + console.log(e); } - oldLogs[oldLog].call(iframe.contentWindow.console, ...arguments); - window.flutter_inappwebview.nativeCommunication('onConsoleMessage', window.flutter_inappwebview.viewId, [oldLog, message]); + window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', viewId, [iframeUrl]); + }; + + var originalReplaceState = iframe.contentWindow.history.replaceState; + iframe.contentWindow.history.replaceState = function (state, unused, url) { + originalReplaceState.call(iframe.contentWindow.history, state, unused, url); + var iframeUrl = iframe.src; + try { + iframeUrl = iframe.contentWindow.location.href; + } catch (e) { + console.log(e); + } + window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', viewId, [iframeUrl]); + }; + + var originalOpen = iframe.contentWindow.open; + iframe.contentWindow.open = function (url, target, windowFeatures) { + var newWindow = originalOpen.call(iframe.contentWindow, ...arguments); + var windowId = webView.windowAutoincrementId; + webView.windowAutoincrementId++; + webView.windows[windowId] = newWindow; + window.flutter_inappwebview.nativeCommunication('onCreateWindow', viewId, [windowId, url, target, windowFeatures]).then(function(){}, function(handledByClient) { + if (handledByClient) { + newWindow.close(); + } + }); + return newWindow; + }; + + var originalPrint = iframe.contentWindow.print; + iframe.contentWindow.print = function () { + var iframeUrl = iframe.src; + try { + iframeUrl = iframe.contentWindow.location.href; + } catch (e) { + console.log(e); + } + window.flutter_inappwebview.nativeCommunication('onPrint', viewId, [iframeUrl]); + originalPrint.call(iframe.contentWindow); + }; + + webView.functionMap = { + "window.open": iframe.contentWindow.open, + "window.print": iframe.contentWindow.print, + "window.history.pushState": iframe.contentWindow.history.pushState, + "window.history.replaceState": iframe.contentWindow.history.replaceState, } - })(k); - } - } catch (e) { - console.log(e); - } - try { - var originalPushState = iframe.contentWindow.history.pushState; - iframe.contentWindow.history.pushState = function (state, unused, url) { - originalPushState.call(iframe.contentWindow.history, state, unused, url); - var iframeUrl = iframe.src; - try { - iframeUrl = iframe.contentWindow.location.href; + var initialTitle = iframe.contentDocument.title; + webView.documentTitle = initialTitle; + window.flutter_inappwebview.nativeCommunication('onTitleChanged', viewId, [initialTitle]); + new MutationObserver(function(mutations) { + var title = mutations[0].target.nodeValue; + if (title != webView.documentTitle) { + webView.documentTitle = title; + window.flutter_inappwebview.nativeCommunication('onTitleChanged', viewId, [title]); + } + }).observe( + iframe.contentDocument.querySelector('title'), + { subtree: true, characterData: true, childList: true } + ); + + var oldPixelRatio = iframe.contentWindow.devicePixelRatio; + iframe.contentWindow.addEventListener('resize', function (e) { + var newPixelRatio = iframe.contentWindow.devicePixelRatio; + if(newPixelRatio !== oldPixelRatio){ + window.flutter_inappwebview.nativeCommunication('onZoomScaleChanged', viewId, [oldPixelRatio, newPixelRatio]); + oldPixelRatio = newPixelRatio; + } + }); + + iframe.contentWindow.addEventListener('popstate', function (event) { + var iframeUrl = iframe.src; + try { + iframeUrl = iframe.contentWindow.location.href; + } catch (e) { + console.log(e); + } + window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', viewId, [iframeUrl]); + }); + + iframe.contentWindow.addEventListener('scroll', function (event) { + var x = 0; + var y = 0; + try { + x = iframe.contentWindow.scrollX; + y = iframe.contentWindow.scrollY; + } catch (e) { + console.log(e); + } + window.flutter_inappwebview.nativeCommunication('onScrollChanged', viewId, [x, y]); + }); + + iframe.contentWindow.addEventListener('focus', function (event) { + window.flutter_inappwebview.nativeCommunication('onWindowFocus', viewId); + }); + + iframe.contentWindow.addEventListener('blur', function (event) { + window.flutter_inappwebview.nativeCommunication('onWindowBlur', viewId); + }); } catch (e) { console.log(e); } - window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', window.flutter_inappwebview.viewId, [iframeUrl]); - }; - var originalReplaceState = iframe.contentWindow.history.replaceState; - iframe.contentWindow.history.replaceState = function (state, unused, url) { - originalReplaceState.call(iframe.contentWindow.history, state, unused, url); - var iframeUrl = iframe.src; try { - iframeUrl = iframe.contentWindow.location.href; - } catch (e) { - console.log(e); - } - window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', window.flutter_inappwebview.viewId, [iframeUrl]); - }; - var originalOpen = iframe.contentWindow.open; - iframe.contentWindow.open = function (url, target, windowFeatures) { - var newWindow = originalOpen.call(iframe.contentWindow, ...arguments); - var windowId = window.flutter_inappwebview.windowAutoincrementId; - window.flutter_inappwebview.windowAutoincrementId++; - window.flutter_inappwebview.windows[windowId] = newWindow; - window.flutter_inappwebview.nativeCommunication('onCreateWindow', window.flutter_inappwebview.viewId, [windowId, url, target, windowFeatures]).then(function(){}, function(handledByClient) { - if (handledByClient) { - newWindow.close(); + if (!webView.settings.javaScriptCanOpenWindowsAutomatically) { + iframe.contentWindow.open = function () { + throw new Error('JavaScript cannot open windows automatically'); + }; } - }); - return newWindow; - }; - var originalPrint = iframe.contentWindow.print; - iframe.contentWindow.print = function () { - var iframeUrl = iframe.src; - try { - iframeUrl = iframe.contentWindow.location.href; + if (!webView.settings.verticalScrollBarEnabled && !webView.settings.horizontalScrollBarEnabled) { + var style = iframe.contentDocument.createElement('style'); + style.id = "settings.verticalScrollBarEnabled-settings.horizontalScrollBarEnabled"; + style.innerHTML = "body::-webkit-scrollbar { width: 0px; height: 0px; }"; + iframe.contentDocument.head.append(style); + } + + if (webView.settings.disableVerticalScroll) { + var style = iframe.contentDocument.createElement('style'); + style.id = "settings.disableVerticalScroll"; + style.innerHTML = "body { overflow-y: hidden; }"; + iframe.contentDocument.head.append(style); + } + + if (webView.settings.disableHorizontalScroll) { + var style = iframe.contentDocument.createElement('style'); + style.id = "settings.disableHorizontalScroll"; + style.innerHTML = "body { overflow-x: hidden; }"; + iframe.contentDocument.head.append(style); + } + + if (webView.settings.disableContextMenu) { + iframe.contentWindow.addEventListener('contextmenu', webView.disableContextMenuHandler); + } } catch (e) { console.log(e); } - window.flutter_inappwebview.nativeCommunication('onPrint', window.flutter_inappwebview.viewId, [iframeUrl]); - originalPrint.call(iframe.contentWindow); - }; - window.flutter_inappwebview.functionMap = { - "window.open": iframe.contentWindow.open, - "window.print": iframe.contentWindow.print, - "window.history.pushState": iframe.contentWindow.history.pushState, - "window.history.replaceState": iframe.contentWindow.history.replaceState, - } - - var initialTitle = iframe.contentDocument.title; - window.flutter_inappwebview.documentTitle = initialTitle; - window.flutter_inappwebview.nativeCommunication('onTitleChanged', window.flutter_inappwebview.viewId, [initialTitle]); - new MutationObserver(function(mutations) { - var title = mutations[0].target.nodeValue; - if (title != window.flutter_inappwebview.documentTitle) { - window.flutter_inappwebview.documentTitle = title; - window.flutter_inappwebview.nativeCommunication('onTitleChanged', window.flutter_inappwebview.viewId, [title]); - } - }).observe( - iframe.contentDocument.querySelector('title'), - { subtree: true, characterData: true, childList: true } - ); - - var oldPixelRatio = iframe.contentWindow.devicePixelRatio; - iframe.contentWindow.addEventListener('resize', function (e) { - var newPixelRatio = iframe.contentWindow.devicePixelRatio; - if(newPixelRatio !== oldPixelRatio){ - window.flutter_inappwebview.nativeCommunication('onZoomScaleChanged', window.flutter_inappwebview.viewId, [oldPixelRatio, newPixelRatio]); - oldPixelRatio = newPixelRatio; - } + window.flutter_inappwebview.nativeCommunication('onLoadStop', viewId, [url]); }); - - iframe.contentWindow.addEventListener('popstate', function (event) { - var iframeUrl = iframe.src; - try { - iframeUrl = iframe.contentWindow.location.href; - } catch (e) { - console.log(e); - } - window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', window.flutter_inappwebview.viewId, [iframeUrl]); - }); - - iframe.contentWindow.addEventListener('scroll', function (event) { - var x = 0; - var y = 0; - try { - x = iframe.contentWindow.scrollX; - y = iframe.contentWindow.scrollY; - } catch (e) { - console.log(e); - } - window.flutter_inappwebview.nativeCommunication('onScrollChanged', window.flutter_inappwebview.viewId, [x, y]); - }); - - iframe.contentWindow.addEventListener('focus', function (event) { - window.flutter_inappwebview.nativeCommunication('onWindowFocus', window.flutter_inappwebview.viewId); - }); - - iframe.contentWindow.addEventListener('blur', function (event) { - window.flutter_inappwebview.nativeCommunication('onWindowBlur', window.flutter_inappwebview.viewId); - }); - } catch (e) { - console.log(e); } - + }, + setSettings: function (newSettings) { + var iframe = webView.iframe; try { - - if (!window.flutter_inappwebview.settings.javaScriptCanOpenWindowsAutomatically) { - iframe.contentWindow.open = function () { - throw new Error('JavaScript cannot open windows automatically'); - }; + if (webView.settings.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically) { + if (!newSettings.javaScriptCanOpenWindowsAutomatically) { + iframe.contentWindow.open = function () { + throw new Error('JavaScript cannot open windows automatically'); + }; + } else { + iframe.contentWindow.open = webView.functionMap["window.open"]; + } } - if (!window.flutter_inappwebview.settings.verticalScrollBarEnabled && !window.flutter_inappwebview.settings.horizontalScrollBarEnabled) { - var style = iframe.contentDocument.createElement('style'); - style.id = "settings.verticalScrollBarEnabled-settings.horizontalScrollBarEnabled"; - style.innerHTML = "body::-webkit-scrollbar { width: 0px; height: 0px; }"; - iframe.contentDocument.head.append(style); + if (webView.settings.verticalScrollBarEnabled != newSettings.verticalScrollBarEnabled && + webView.settings.horizontalScrollBarEnabled != newSettings.horizontalScrollBarEnabled) { + if (!newSettings.verticalScrollBarEnabled && !newSettings.horizontalScrollBarEnabled) { + var style = iframe.contentDocument.createElement('style'); + style.id = "settings.verticalScrollBarEnabled-settings.horizontalScrollBarEnabled"; + style.innerHTML = "body::-webkit-scrollbar { width: 0px; height: 0px; }"; + iframe.contentDocument.head.append(style); + } else { + var styleElement = iframe.contentDocument.getElementById("settings.verticalScrollBarEnabled-settings.horizontalScrollBarEnabled"); + if (styleElement) { styleElement.remove() } + } } - if (window.flutter_inappwebview.settings.disableVerticalScroll) { - var style = iframe.contentDocument.createElement('style'); - style.id = "settings.disableVerticalScroll"; - style.innerHTML = "body { overflow-y: hidden; }"; - iframe.contentDocument.head.append(style); + if (webView.settings.disableVerticalScroll != newSettings.disableVerticalScroll) { + if (newSettings.disableVerticalScroll) { + var style = iframe.contentDocument.createElement('style'); + style.id = "settings.disableVerticalScroll"; + style.innerHTML = "body { overflow-y: hidden; }"; + iframe.contentDocument.head.append(style); + } else { + var styleElement = iframe.contentDocument.getElementById("settings.disableVerticalScroll"); + if (styleElement) { styleElement.remove() } + } } - if (window.flutter_inappwebview.settings.disableHorizontalScroll) { - var style = iframe.contentDocument.createElement('style'); - style.id = "settings.disableHorizontalScroll"; - style.innerHTML = "body { overflow-x: hidden; }"; - iframe.contentDocument.head.append(style); + if (webView.settings.disableHorizontalScroll != newSettings.disableHorizontalScroll) { + if (newSettings.disableHorizontalScroll) { + var style = iframe.contentDocument.createElement('style'); + style.id = "settings.disableHorizontalScroll"; + style.innerHTML = "body { overflow-x: hidden; }"; + iframe.contentDocument.head.append(style); + } else { + var styleElement = iframe.contentDocument.getElementById("settings.disableHorizontalScroll"); + if (styleElement) { styleElement.remove() } + } + } + + if (webView.settings.disableContextMenu != newSettings.disableContextMenu) { + if (newSettings.disableContextMenu) { + iframe.contentWindow.addEventListener('contextmenu', webView.disableContextMenuHandler); + } else { + iframe.contentWindow.removeEventListener('contextmenu', webView.disableContextMenuHandler); + } } } catch (e) { console.log(e); } - window.flutter_inappwebview.nativeCommunication('onLoadStop', window.flutter_inappwebview.viewId, [url]); - }); - } - }, - setSettings: function (newSettings) { - var iframe = window.flutter_inappwebview.iframe; - try { - if (window.flutter_inappwebview.settings.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically) { - if (!newSettings.javaScriptCanOpenWindowsAutomatically) { - iframe.contentWindow.open = function () { - throw new Error('JavaScript cannot open windows automatically'); - }; - } else { - iframe.contentWindow.open = window.flutter_inappwebview.functionMap["window.open"]; + webView.settings = newSettings; + }, + reload: function () { + var iframe = webView.iframe; + if (iframe != null && iframe.contentWindow != null) { + try { + iframe.contentWindow.location.reload(); + } catch (e) { + console.log(e); + iframe.contentWindow.location.href = iframe.src; + } + } + }, + goBack: function () { + var iframe = webView.iframe; + if (iframe != null) { + try { + iframe.contentWindow.history.back(); + } catch (e) { + console.log(e); + } + } + }, + goForward: function () { + var iframe = webView.iframe; + if (iframe != null) { + try { + iframe.contentWindow.history.forward(); + } catch (e) { + console.log(e); + } + } + }, + goForwardOrForward: function (steps) { + var iframe = webView.iframe; + if (iframe != null) { + try { + iframe.contentWindow.history.go(steps); + } catch (e) { + console.log(e); + } + } + }, + evaluateJavascript: function (source) { + var iframe = webView.iframe; + var result = null; + if (iframe != null) { + try { + result = JSON.stringify(iframe.contentWindow.eval(source)); + } catch (e) {} + } + return result; + }, + stopLoading: function (steps) { + var iframe = webView.iframe; + if (iframe != null) { + try { + iframe.contentWindow.stop(); + } catch (e) { + console.log(e); + } } } + }; - if (window.flutter_inappwebview.settings.verticalScrollBarEnabled != newSettings.verticalScrollBarEnabled && - window.flutter_inappwebview.settings.horizontalScrollBarEnabled != newSettings.horizontalScrollBarEnabled) { - if (!newSettings.verticalScrollBarEnabled && !newSettings.horizontalScrollBarEnabled) { - var style = iframe.contentDocument.createElement('style'); - style.id = "settings.verticalScrollBarEnabled-settings.horizontalScrollBarEnabled"; - style.innerHTML = "body::-webkit-scrollbar { width: 0px; height: 0px; }"; - iframe.contentDocument.head.append(style); - } else { - var styleElement = iframe.contentDocument.getElementById("settings.verticalScrollBarEnabled-settings.horizontalScrollBarEnabled"); - if (styleElement) { styleElement.remove() } - } - } - - if (window.flutter_inappwebview.settings.disableVerticalScroll != newSettings.disableVerticalScroll) { - if (newSettings.disableVerticalScroll) { - var style = iframe.contentDocument.createElement('style'); - style.id = "settings.disableVerticalScroll"; - style.innerHTML = "body { overflow-y: hidden; }"; - iframe.contentDocument.head.append(style); - } else { - var styleElement = iframe.contentDocument.getElementById("settings.disableVerticalScroll"); - if (styleElement) { styleElement.remove() } - } - } - - if (window.flutter_inappwebview.settings.disableHorizontalScroll != newSettings.disableHorizontalScroll) { - if (newSettings.disableHorizontalScroll) { - var style = iframe.contentDocument.createElement('style'); - style.id = "settings.disableHorizontalScroll"; - style.innerHTML = "body { overflow-x: hidden; }"; - iframe.contentDocument.head.append(style); - } else { - var styleElement = iframe.contentDocument.getElementById("settings.disableHorizontalScroll"); - if (styleElement) { styleElement.remove() } - } - } - } catch (e) { - console.log(e); - } - window.flutter_inappwebview.settings = newSettings; + return webView; }, - reload: function () { - var iframe = window.flutter_inappwebview.iframe; - if (iframe != null && iframe.contentWindow != null) { - try { - iframe.contentWindow.location.reload(); - } catch (e) { - console.log(e); - iframe.contentWindow.location.href = iframe.src; - } - } - }, - goBack: function () { - var iframe = window.flutter_inappwebview.iframe; - if (iframe != null) { - try { - iframe.contentWindow.history.back(); - } catch (e) { - console.log(e); - } - } - }, - goForward: function () { - var iframe = window.flutter_inappwebview.iframe; - if (iframe != null) { - try { - iframe.contentWindow.history.forward(); - } catch (e) { - console.log(e); - } - } - }, - goForwardOrForward: function (steps) { - var iframe = window.flutter_inappwebview.iframe; - if (iframe != null) { - try { - iframe.contentWindow.history.go(steps); - } catch (e) { - console.log(e); - } - } - }, - evaluateJavascript: function (source) { - var iframe = window.flutter_inappwebview.iframe; - var result = null; - if (iframe != null) { - try { - result = JSON.stringify(iframe.contentWindow.eval(source)); - } catch (e) {} - } - return result; - }, - stopLoading: function (steps) { - var iframe = window.flutter_inappwebview.iframe; - if (iframe != null) { - try { - iframe.contentWindow.stop(); - } catch (e) { - console.log(e); - } - } + getCookieExpirationDate: function(timestamp) { + return (new Date(timestamp)).toUTCString(); } }; \ No newline at end of file diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index 389f0662..b172b287 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -36,6 +36,10 @@ class ChromeSafariBrowserNotOpenedException implements Exception { ///**NOTE**: If you want to use the `ChromeSafariBrowser` class on Android 11+ you need to specify your app querying for ///`android.support.customtabs.action.CustomTabsService` in your `AndroidManifest.xml` ///(you can read more about it here: https://developers.google.com/web/android/custom-tabs/best-practices#applications_targeting_android_11_api_level_30_or_above). +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS class ChromeSafariBrowser { ///View ID used internally. late final String id; diff --git a/lib/src/context_menu.dart b/lib/src/context_menu.dart index a7a91892..f4f56361 100644 --- a/lib/src/context_menu.dart +++ b/lib/src/context_menu.dart @@ -6,6 +6,10 @@ import 'types.dart'; ///Class that represents the WebView context menu. It used by [WebView.contextMenu]. /// ///**NOTE**: To make it work properly on Android, JavaScript should be enabled! +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS class ContextMenu { ///Event fired when the context menu for this WebView is being built. /// diff --git a/lib/src/cookie_manager.dart b/lib/src/cookie_manager.dart index 00cc2cce..11c93661 100755 --- a/lib/src/cookie_manager.dart +++ b/lib/src/cookie_manager.dart @@ -17,6 +17,15 @@ import 'types.dart'; ///**NOTE for iOS below 11.0 (LIMITED SUPPORT!)**: in this case, almost all of the methods ([CookieManager.deleteAllCookies] and [CookieManager.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. +/// +///**NOTE for Web (LIMITED SUPPORT!)**: in this case, almost all of the methods ([CookieManager.deleteAllCookies] and [CookieManager.getAllCookies] are not supported!) +///has been implemented using JavaScript, so all methods will have effect only if the iframe has the same origin. +///See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies for JavaScript restrictions. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS +///- Web class CookieManager { static CookieManager? _instance; static const MethodChannel _channel = const MethodChannel( @@ -48,15 +57,20 @@ class CookieManager { ///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. + ///[webViewController] 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 and Web platform. 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] + ///**NOTE for iOS below 11.0**: If [webViewController] 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]). + /// + ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. + ///If [webViewController] 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]). /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - CookieManager.setCookie](https://developer.android.com/reference/android/webkit/CookieManager#setCookie(java.lang.String,%20java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.Boolean%3E))) ///- iOS ([Official API - WKHTTPCookieStore.setCookie](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882007-setcookie)) + ///- Web Future setCookie( {required Uri url, required String name, @@ -68,19 +82,26 @@ class CookieManager { bool? isSecure, bool? isHttpOnly, HTTPCookieSameSitePolicy? sameSite, - InAppWebViewController? iosBelow11WebViewController}) async { + @Deprecated("Use webViewController instead") InAppWebViewController? iosBelow11WebViewController, + InAppWebViewController? webViewController}) async { if (domain == null) domain = _getDomainName(url); + webViewController = webViewController ?? iosBelow11WebViewController; + assert(url.toString().isNotEmpty); assert(name.isNotEmpty); assert(value.isNotEmpty); assert(domain.isNotEmpty); assert(path.isNotEmpty); - if (defaultTargetPlatform == TargetPlatform.iOS) { - var platformUtil = PlatformUtil(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - if (version != null && version < 11.0) { + if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { + var shouldUseJavascript = kIsWeb; + if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { + var platformUtil = PlatformUtil.instance(); + var version = double.tryParse(await platformUtil.getSystemVersion()); + shouldUseJavascript = version != null && version < 11.0; + } + if (shouldUseJavascript) { await _setCookieWithJavaScript( url: url, name: name, @@ -91,7 +112,7 @@ class CookieManager { maxAge: maxAge, isSecure: isSecure, sameSite: sameSite, - webViewController: iosBelow11WebViewController); + webViewController: webViewController); return; } } @@ -161,28 +182,40 @@ class CookieManager { ///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. + ///[webViewController] 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 and Web platform. 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] + ///If [webViewController] 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!). + /// + ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. + ///If [webViewController] 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!). /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - CookieManager.getCookie](https://developer.android.com/reference/android/webkit/CookieManager#getCookie(java.lang.String))) ///- iOS ([Official API - WKHTTPCookieStore.getAllCookies](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882005-getallcookies)) + ///- Web Future> getCookies( {required Uri url, - InAppWebViewController? iosBelow11WebViewController}) async { + @Deprecated("Use webViewController instead") InAppWebViewController? iosBelow11WebViewController, + InAppWebViewController? webViewController}) async { assert(url.toString().isNotEmpty); - if (defaultTargetPlatform == TargetPlatform.iOS) { - var platformUtil = PlatformUtil(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - if (version != null && version < 11.0) { + webViewController = webViewController ?? iosBelow11WebViewController; + + if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { + var shouldUseJavascript = kIsWeb; + if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { + var platformUtil = PlatformUtil.instance(); + var version = double.tryParse(await platformUtil.getSystemVersion()); + shouldUseJavascript = version != null && version < 11.0; + } + if (shouldUseJavascript) { return await _getCookiesWithJavaScript( - url: url, webViewController: iosBelow11WebViewController); + url: url, webViewController: webViewController); } } @@ -225,10 +258,12 @@ class CookieManager { .toList(); documentCookies.forEach((documentCookie) { List cookie = documentCookie.split('='); - cookies.add(Cookie( - name: cookie[0], - value: cookie[1], - )); + if (cookie.length > 1) { + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + )); + } }); return cookies; } @@ -251,10 +286,12 @@ class CookieManager { .toList(); documentCookies.forEach((documentCookie) { List cookie = documentCookie.split('='); - cookies.add(Cookie( - name: cookie[0], - value: cookie[1], - )); + if (cookie.length > 1) { + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + )); + } }); await headlessWebView.dispose(); return cookies; @@ -262,30 +299,42 @@ class CookieManager { ///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. + ///[webViewController] 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 and Web platform. 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] + ///If [webViewController] 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!). + /// + ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. + ///If [webViewController] 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!). /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- Web Future getCookie( {required Uri url, required String name, - InAppWebViewController? iosBelow11WebViewController}) async { + @Deprecated("Use webViewController instead") InAppWebViewController? iosBelow11WebViewController, + InAppWebViewController? webViewController}) async { assert(url.toString().isNotEmpty); assert(name.isNotEmpty); - if (defaultTargetPlatform == TargetPlatform.iOS) { - var platformUtil = PlatformUtil(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - if (version != null && version < 11.0) { + webViewController = webViewController ?? iosBelow11WebViewController; + + if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { + var shouldUseJavascript = kIsWeb; + if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { + var platformUtil = PlatformUtil.instance(); + var version = double.tryParse(await platformUtil.getSystemVersion()); + shouldUseJavascript = version != null && version < 11.0; + } + if (shouldUseJavascript) { List cookies = await _getCookiesWithJavaScript( - url: url, webViewController: iosBelow11WebViewController); + url: url, webViewController: webViewController); return cookies .cast() .firstWhere((cookie) => cookie!.name == name, orElse: () => null); @@ -319,31 +368,43 @@ class CookieManager { ///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. + ///[webViewController] 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 and Web platform. 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] + ///**NOTE for iOS below 11.0**: If [webViewController] 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!). + /// + ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. + ///If [webViewController] 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!). /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKHTTPCookieStore.delete](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882009-delete) + ///- Web Future deleteCookie( {required Uri url, required String name, String domain = "", String path = "/", - InAppWebViewController? iosBelow11WebViewController}) async { + @Deprecated("Use webViewController instead") InAppWebViewController? iosBelow11WebViewController, + InAppWebViewController? webViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); assert(url.toString().isNotEmpty); assert(name.isNotEmpty); - if (defaultTargetPlatform == TargetPlatform.iOS) { - var platformUtil = PlatformUtil(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - if (version != null && version < 11.0) { + webViewController = webViewController ?? iosBelow11WebViewController; + + if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { + var shouldUseJavascript = kIsWeb; + if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { + var platformUtil = PlatformUtil.instance(); + var version = double.tryParse(await platformUtil.getSystemVersion()); + shouldUseJavascript = version != null && version < 11.0; + } + if (shouldUseJavascript) { await _setCookieWithJavaScript( url: url, name: name, @@ -351,7 +412,7 @@ class CookieManager { path: path, domain: domain, maxAge: -1, - webViewController: iosBelow11WebViewController); + webViewController: webViewController); return; } } @@ -369,31 +430,43 @@ class CookieManager { ///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. + ///[webViewController] 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 and Web platform. 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] + ///**NOTE for iOS below 11.0**: If [webViewController] 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!). + /// + ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. + ///If [webViewController] 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!). /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- Web Future deleteCookies( {required Uri url, String domain = "", String path = "/", - InAppWebViewController? iosBelow11WebViewController}) async { + @Deprecated("Use webViewController instead") InAppWebViewController? iosBelow11WebViewController, + InAppWebViewController? webViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); assert(url.toString().isNotEmpty); - if (defaultTargetPlatform == TargetPlatform.iOS) { - var platformUtil = PlatformUtil(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - if (version != null && version < 11.0) { + webViewController = webViewController ?? iosBelow11WebViewController; + + if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { + var shouldUseJavascript = kIsWeb; + if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { + var platformUtil = PlatformUtil.instance(); + var version = double.tryParse(await platformUtil.getSystemVersion()); + shouldUseJavascript = version != null && version < 11.0; + } + if (shouldUseJavascript) { List cookies = await _getCookiesWithJavaScript( - url: url, webViewController: iosBelow11WebViewController); + url: url, webViewController: webViewController); for (var i = 0; i < cookies.length; i++) { await _setCookieWithJavaScript( url: url, @@ -402,7 +475,7 @@ class CookieManager { path: path, domain: domain, maxAge: -1, - webViewController: iosBelow11WebViewController); + webViewController: webViewController); } return; } @@ -462,13 +535,15 @@ class CookieManager { } Future _getCookieExpirationDate(int expiresDate) async { - var platformUtil = PlatformUtil(); + var platformUtil = PlatformUtil.instance(); var dateTime = DateTime.fromMillisecondsSinceEpoch(expiresDate).toUtc(); - return await platformUtil.formatDate( - date: dateTime, - format: 'EEE, dd MMM yyyy hh:mm:ss z', - locale: 'en_US', - timezone: 'GMT'); + return !kIsWeb ? + await platformUtil.formatDate( + date: dateTime, + format: 'EEE, dd MMM yyyy hh:mm:ss z', + locale: 'en_US', + timezone: 'GMT') : + await platformUtil.getWebCookieExpirationDate(date: dateTime); } } diff --git a/lib/src/http_auth_credentials_database.dart b/lib/src/http_auth_credentials_database.dart index eaf4e236..7d4d8d5a 100755 --- a/lib/src/http_auth_credentials_database.dart +++ b/lib/src/http_auth_credentials_database.dart @@ -8,6 +8,10 @@ import 'package:flutter/services.dart'; ///On Android, this class has a custom implementation using `android.database.sqlite.SQLiteDatabase` because ///[WebViewDatabase](https://developer.android.com/reference/android/webkit/WebViewDatabase) ///doesn't offer the same functionalities as iOS `URLCredentialStorage`. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS class HttpAuthCredentialDatabase { static HttpAuthCredentialDatabase? _instance; static const MethodChannel _channel = const MethodChannel( diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index abd513f8..85d97d47 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -40,6 +40,10 @@ class InAppBrowserNotOpenedException implements Exception { ///This class uses the native WebView of the platform. ///The [webViewController] field can be used to access the [InAppWebViewController] API. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS class InAppBrowser { ///View ID used internally. late final String id; diff --git a/lib/src/in_app_localhost_server.dart b/lib/src/in_app_localhost_server.dart index 2c7d2c5d..e30c96d5 100755 --- a/lib/src/in_app_localhost_server.dart +++ b/lib/src/in_app_localhost_server.dart @@ -7,6 +7,10 @@ import 'package:flutter/services.dart' show rootBundle; import 'mime_type_resolver.dart'; ///This class allows you to create a simple server on `http://localhost:[port]/` in order to be able to load your assets file on a server. The default [port] value is `8080`. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS class InAppLocalhostServer { bool _started = false; HttpServer? _server; diff --git a/lib/src/in_app_webview/headless_in_app_webview.dart b/lib/src/in_app_webview/headless_in_app_webview.dart index da8d5064..b2f6d395 100644 --- a/lib/src/in_app_webview/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -18,6 +18,11 @@ import '../util.dart'; ///It can be used to run a WebView in background without attaching an `InAppWebView` to the widget tree. /// ///Remember to dispose it when you don't need it anymore. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS +///- Web class HeadlessInAppWebView implements WebView { ///View ID. late final String id; diff --git a/lib/src/in_app_webview/in_app_webview.dart b/lib/src/in_app_webview/in_app_webview.dart index 60855fd1..68b6ccb6 100755 --- a/lib/src/in_app_webview/in_app_webview.dart +++ b/lib/src/in_app_webview/in_app_webview.dart @@ -20,6 +20,11 @@ import 'in_app_webview_settings.dart'; import '../pull_to_refresh/main.dart'; ///Flutter Widget for adding an **inline native WebView** integrated in the flutter widget tree. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS +///- Web class InAppWebView extends StatefulWidget implements WebView { /// `gestureRecognizers` specifies which gestures should be consumed by the WebView. /// It is possible for other gesture recognizers to be competing with the web view on pointer diff --git a/lib/src/in_app_webview/in_app_webview_settings.dart b/lib/src/in_app_webview/in_app_webview_settings.dart index 83a0f6d6..2aed5e03 100755 --- a/lib/src/in_app_webview/in_app_webview_settings.dart +++ b/lib/src/in_app_webview/in_app_webview_settings.dart @@ -228,9 +228,12 @@ class InAppWebViewSettings ///Set to `true` to disable context menu. The default value is `false`. /// + ///**NOTE for Web**: this setting will have effect only if the iframe has the same origin. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- Web bool disableContextMenu; ///Set to `false` if the WebView should not support zooming using its on-screen zoom controls and gestures. The default value is `true`. diff --git a/lib/src/platform_util.dart b/lib/src/platform_util.dart index 5b24ef49..cccaee40 100644 --- a/lib/src/platform_util.dart +++ b/lib/src/platform_util.dart @@ -1,10 +1,12 @@ import 'package:flutter/services.dart'; +///Platform native utilities class PlatformUtil { static PlatformUtil? _instance; static const MethodChannel _channel = const MethodChannel( 'com.pichillilorenzo/flutter_inappwebview_platformutil'); + ///Get [PlatformUtil] instance. static PlatformUtil instance() { return (_instance != null) ? _instance! : _init(); } @@ -18,6 +20,8 @@ class PlatformUtil { static Future _handleMethod(MethodCall call) async {} String? _cachedSystemVersion; + + ///Get current platform system version. Future getSystemVersion() async { if (_cachedSystemVersion != null) { return _cachedSystemVersion!; @@ -28,6 +32,7 @@ class PlatformUtil { return _cachedSystemVersion!; } + ///Format date. Future formatDate( {required DateTime date, required String format, @@ -40,4 +45,12 @@ class PlatformUtil { args.putIfAbsent('timezone', () => timezone); return await _channel.invokeMethod('formatDate', args); } + + ///Get cookie expiration date used by Web platform. + Future getWebCookieExpirationDate( + {required DateTime date}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + return await _channel.invokeMethod('getWebCookieExpirationDate', args); + } } diff --git a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart index a100855e..927fffe0 100644 --- a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart +++ b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -15,6 +15,10 @@ import 'pull_to_refresh_settings.dart'; ///(for example [WebView.onWebViewCreated] or [InAppBrowser.onBrowserCreated]). /// ///**NOTE for Android**: to be able to use the "pull-to-refresh" feature, [InAppWebViewSettings.useHybridComposition] must be `true`. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS class PullToRefreshController { @Deprecated("Use settings instead") // ignore: deprecated_member_use_from_same_package diff --git a/lib/src/types.dart b/lib/src/types.dart index a2d76c4b..21d34307 100755 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -184,6 +184,20 @@ class InAppWebViewInitialData { this.androidHistoryUrl = this.historyUrl; } + static InAppWebViewInitialData? fromMap(Map? map) { + if (map == null) { + return null; + } + return InAppWebViewInitialData( + data: map["data"], + mimeType: map["mimeType"], + encoding: map["encoding"], + baseUrl: map["baseUrl"], + // ignore: deprecated_member_use_from_same_package + androidHistoryUrl: map["androidHistoryUrl"], + historyUrl: map["historyUrl"]); + } + Map toMap() { return { "data": data, diff --git a/lib/src/web/headless_in_app_web_view_web_element.dart b/lib/src/web/headless_in_app_web_view_web_element.dart new file mode 100644 index 00000000..076967ac --- /dev/null +++ b/lib/src/web/headless_in_app_web_view_web_element.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'in_app_web_view_web_element.dart'; +import '../util.dart'; + +class HeadlessInAppWebViewWebElement { + String id; + late BinaryMessenger _messenger; + InAppWebViewWebElement? webView; + late MethodChannel? _channel; + + HeadlessInAppWebViewWebElement({required this.id, required BinaryMessenger messenger, + required this.webView}) { + this._messenger = messenger; + + _channel = MethodChannel( + 'com.pichillilorenzo/flutter_headless_inappwebview_${this.id}', + const StandardMethodCodec(), + _messenger, + ); + + this._channel?.setMethodCallHandler(handleMethodCall); + } + + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case "dispose": + dispose(); + break; + case "setSize": + Size size = MapSize.fromMap(call.arguments['size'])!; + setSize(size); + break; + case "getSize": + return getSize().toMap(); + default: + throw PlatformException( + code: 'Unimplemented', + details: 'flutter_inappwebview for web doesn\'t implement \'${call.method}\'', + ); + } + } + + void onWebViewCreated() async { + await _channel?.invokeMethod("onWebViewCreated"); + } + + void setSize(Size size) { + webView?.iframe.style.width = size.width.toString() + "px"; + webView?.iframe.style.height = size.height.toString() + "px"; + } + + Size getSize() { + var width = webView?.iframe.getBoundingClientRect().width.toDouble() ?? 0.0; + var height = webView?.iframe.getBoundingClientRect().height.toDouble() ?? 0.0; + return Size(width, height); + } + + void dispose() { + _channel?.setMethodCallHandler(null); + _channel = null; + webView?.dispose(); + webView = null; + } +} \ No newline at end of file diff --git a/lib/src/web/headless_inappwebview_manager.dart b/lib/src/web/headless_inappwebview_manager.dart new file mode 100644 index 00000000..7a269588 --- /dev/null +++ b/lib/src/web/headless_inappwebview_manager.dart @@ -0,0 +1,62 @@ +import 'dart:html'; + +import 'package:flutter/services.dart'; +import 'web_platform_manager.dart'; +import '../in_app_webview/in_app_webview_settings.dart'; +import 'in_app_web_view_web_element.dart'; +import 'headless_in_app_web_view_web_element.dart'; + +import '../types.dart'; + +class HeadlessInAppWebViewManager { + static late MethodChannel _sharedChannel; + + late BinaryMessenger _messenger; + + HeadlessInAppWebViewManager({required BinaryMessenger messenger}) { + this._messenger = messenger; + HeadlessInAppWebViewManager._sharedChannel = MethodChannel( + 'com.pichillilorenzo/flutter_headless_inappwebview', + const StandardMethodCodec(), + _messenger, + ); + HeadlessInAppWebViewManager._sharedChannel.setMethodCallHandler(handleMethod); + } + + Future handleMethod(MethodCall call) async { + switch (call.method) { + case "run": + String id = call.arguments["id"]; + Map params = call.arguments["params"].cast(); + run(id, params); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + void run(String id, Map params) { + var webView = InAppWebViewWebElement(viewId: id, messenger: _messenger); + var headlessWebView = HeadlessInAppWebViewWebElement( + id: id, + messenger: _messenger, + webView: webView + ); + WebPlatformManager.webViews.putIfAbsent(id, () => webView); + webView.iframe.style.display = 'none'; + Map initialSettings = params["initialSettings"].cast(); + if (initialSettings.isEmpty) { + webView.initialSettings = InAppWebViewSettings(); + } else { + webView.initialSettings = InAppWebViewSettings.fromMap(initialSettings); + } + webView.initialUrlRequest = URLRequest.fromMap(params["initialUrlRequest"]?.cast()); + webView.initialFile = params["initialFile"]; + webView.initialData = InAppWebViewInitialData.fromMap(params["initialData"]?.cast()); + document.body?.append(webView.iframe); + webView.prepare(); + headlessWebView.onWebViewCreated(); + webView.makeInitialLoad(); + } +} \ No newline at end of file diff --git a/lib/src/web/in_app_web_view_web_element.dart b/lib/src/web/in_app_web_view_web_element.dart index adb4e62e..d0ac6bd1 100644 --- a/lib/src/web/in_app_web_view_web_element.dart +++ b/lib/src/web/in_app_web_view_web_element.dart @@ -3,14 +3,15 @@ import 'package:flutter/services.dart'; import 'dart:html'; import 'dart:js' as js; +import 'web_platform_manager.dart'; import '../in_app_webview/in_app_webview_settings.dart'; import '../types.dart'; class InAppWebViewWebElement { - late int _viewId; + late dynamic _viewId; late BinaryMessenger _messenger; late IFrameElement iframe; - late MethodChannel _channel; + late MethodChannel? _channel; InAppWebViewSettings? initialSettings; URLRequest? initialUrlRequest; InAppWebViewInitialData? initialData; @@ -20,7 +21,7 @@ class InAppWebViewWebElement { late js.JsObject bridgeJsObject; bool isLoading = false; - InAppWebViewWebElement({required int viewId, required BinaryMessenger messenger}) { + InAppWebViewWebElement({required dynamic viewId, required BinaryMessenger messenger}) { this._viewId = viewId; this._messenger = messenger; iframe = IFrameElement() @@ -35,11 +36,10 @@ class InAppWebViewWebElement { _messenger, ); - this._channel.setMethodCallHandler(handleMethodCall); + this._channel?.setMethodCallHandler(handleMethodCall); - bridgeJsObject = js.JsObject.fromBrowserObject(js.context['flutter_inappwebview']); - bridgeJsObject['viewId'] = _viewId; - bridgeJsObject['iframeId'] = iframe.id; + bridgeJsObject = js.JsObject.fromBrowserObject(js.context[WebPlatformManager.BRIDGE_JS_OBJECT_NAME]); + bridgeJsObject['webViews'][_viewId] = bridgeJsObject.callMethod("createFlutterInAppWebView", [_viewId, iframe.id]); } /// Handles method calls over the MethodChannel of this plugin. @@ -87,6 +87,9 @@ class InAppWebViewWebElement { InAppWebViewSettings newSettings = InAppWebViewSettings.fromMap(call.arguments["settings"].cast()); setSettings(newSettings); break; + case "dispose": + dispose(); + break; default: throw PlatformException( code: 'Unimplemented', @@ -118,7 +121,16 @@ class InAppWebViewWebElement { iframe.setAttribute("sandbox", sandbox.map((e) => e.toValue()).join(" ")); } - bridgeJsObject.callMethod("prepare", [js.JsObject.jsify(settings.toMap())]); + _callMethod("prepare", [js.JsObject.jsify(settings.toMap())]); + } + + dynamic _callMethod(Object method, [List? args]) { + var webViews = bridgeJsObject['webViews'] as js.JsObject; + if (webViews.hasProperty(_viewId)) { + var webview = bridgeJsObject['webViews'][_viewId] as js.JsObject; + return webview.callMethod(method, args); + } + return null; } void makeInitialLoad() async { @@ -168,27 +180,27 @@ class InAppWebViewWebElement { } Future reload() async { - bridgeJsObject.callMethod("reload"); + _callMethod("reload"); } Future goBack() async { - bridgeJsObject.callMethod("goBack"); + _callMethod("goBack"); } Future goForward() async { - bridgeJsObject.callMethod("goForward"); + _callMethod("goForward"); } Future goBackOrForward({required int steps}) async { - bridgeJsObject.callMethod("goBackOrForward", [steps]); + _callMethod("goBackOrForward", [steps]); } Future evaluateJavascript({required String source}) async { - return bridgeJsObject.callMethod("evaluateJavascript", [source]); + return _callMethod("evaluateJavascript", [source]); } Future stopLoading() async { - bridgeJsObject.callMethod("stopLoading"); + _callMethod("stopLoading"); } Set getSandbox() { @@ -243,7 +255,7 @@ class InAppWebViewWebElement { iframe.setAttribute("sandbox", sandbox.map((e) => e.toValue()).join(" ")); } - bridgeJsObject.callMethod("setSettings", [js.JsObject.jsify(newSettings.toMap())]); + _callMethod("setSettings", [js.JsObject.jsify(newSettings.toMap())]); settings = newSettings; } @@ -254,7 +266,7 @@ class InAppWebViewWebElement { var obj = { "url": url }; - await _channel.invokeMethod("onLoadStart", obj); + await _channel?.invokeMethod("onLoadStart", obj); } void onLoadStop(String url) async { @@ -263,14 +275,14 @@ class InAppWebViewWebElement { var obj = { "url": url }; - await _channel.invokeMethod("onLoadStop", obj); + await _channel?.invokeMethod("onLoadStop", obj); } void onUpdateVisitedHistory(String url) async { var obj = { "url": url }; - await _channel.invokeMethod("onUpdateVisitedHistory", obj); + await _channel?.invokeMethod("onUpdateVisitedHistory", obj); } void onScrollChanged(int x, int y) async { @@ -278,7 +290,7 @@ class InAppWebViewWebElement { "x": x, "y": y }; - await _channel.invokeMethod("onScrollChanged", obj); + await _channel?.invokeMethod("onScrollChanged", obj); } void onConsoleMessage(String type, String? message) async { @@ -302,7 +314,7 @@ class InAppWebViewWebElement { "messageLevel": messageLevel, "message": message }; - await _channel.invokeMethod("onConsoleMessage", obj); + await _channel?.invokeMethod("onConsoleMessage", obj); } Future onCreateWindow(int windowId, String url, String? target, String? windowFeatures) async { @@ -330,15 +342,15 @@ class InAppWebViewWebElement { }, "windowFeatures": windowFeaturesMap }; - return await _channel.invokeMethod("onCreateWindow", obj); + return await _channel?.invokeMethod("onCreateWindow", obj); } void onWindowFocus() async { - await _channel.invokeMethod("onWindowFocus"); + await _channel?.invokeMethod("onWindowFocus"); } void onWindowBlur() async { - await _channel.invokeMethod("onWindowBlur"); + await _channel?.invokeMethod("onWindowBlur"); } void onPrint(String? url) async { @@ -346,15 +358,15 @@ class InAppWebViewWebElement { "url": url }; - await _channel.invokeMethod("onPrint", obj); + await _channel?.invokeMethod("onPrint", obj); } void onEnterFullscreen() async { - await _channel.invokeMethod("onEnterFullscreen"); + await _channel?.invokeMethod("onEnterFullscreen"); } void onExitFullscreen() async { - await _channel.invokeMethod("onExitFullscreen"); + await _channel?.invokeMethod("onExitFullscreen"); } void onTitleChanged(String? title) async { @@ -362,7 +374,7 @@ class InAppWebViewWebElement { "title": title }; - await _channel.invokeMethod("onTitleChanged", obj); + await _channel?.invokeMethod("onTitleChanged", obj); } void onZoomScaleChanged(double oldScale, double newScale) async { @@ -371,6 +383,20 @@ class InAppWebViewWebElement { "newScale": newScale }; - await _channel.invokeMethod("onZoomScaleChanged", obj); + await _channel?.invokeMethod("onZoomScaleChanged", obj); + } + + void dispose() { + _channel?.setMethodCallHandler(null); + _channel = null; + iframe.remove(); + if (WebPlatformManager.webViews.containsKey(_viewId)) { + WebPlatformManager.webViews.remove(_viewId); + } + bridgeJsObject = js.JsObject.fromBrowserObject(js.context[WebPlatformManager.BRIDGE_JS_OBJECT_NAME]); + var webViews = bridgeJsObject['webViews'] as js.JsObject; + if (webViews.hasProperty(_viewId)) { + webViews.deleteProperty(_viewId); + } } } \ No newline at end of file diff --git a/lib/src/web/platform_util.dart b/lib/src/web/platform_util.dart new file mode 100644 index 00000000..5028d605 --- /dev/null +++ b/lib/src/web/platform_util.dart @@ -0,0 +1,46 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; + +import 'dart:js' as js; + +import 'web_platform_manager.dart'; + +class PlatformUtil { + late BinaryMessenger _messenger; + late MethodChannel? _channel; + + PlatformUtil({required BinaryMessenger messenger}) { + this._messenger = messenger; + + _channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_platformutil', + const StandardMethodCodec(), + _messenger, + ); + + this._channel?.setMethodCallHandler(handleMethodCall); + } + + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case "getWebCookieExpirationDate": + int timestamp = call.arguments['date']; + return getWebCookieExpirationDate(timestamp); + default: + throw PlatformException( + code: 'Unimplemented', + details: 'flutter_inappwebview for web doesn\'t implement \'${call.method}\'', + ); + } + } + + String getWebCookieExpirationDate(int timestamp) { + var bridgeJsObject = js.JsObject.fromBrowserObject(js.context[WebPlatformManager.BRIDGE_JS_OBJECT_NAME]); + return bridgeJsObject.callMethod("getCookieExpirationDate", [timestamp]); + } + + void dispose() { + _channel?.setMethodCallHandler(null); + _channel = null; + } +} \ No newline at end of file diff --git a/lib/src/web/web_platform.dart b/lib/src/web/web_platform.dart index 8acf5541..a66c01a1 100644 --- a/lib/src/web/web_platform.dart +++ b/lib/src/web/web_platform.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:flutter/services.dart'; +import 'headless_inappwebview_manager.dart'; import 'web_platform_manager.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'shims/dart_ui.dart' as ui; import 'in_app_web_view_web_element.dart'; - +import 'platform_util.dart'; import 'package:js/js.dart'; /// Builds an iframe based WebView. @@ -26,30 +26,21 @@ class FlutterInAppWebViewWebPlatform { static void registerWith(Registrar registrar) { final pluginInstance = FlutterInAppWebViewWebPlatform(registrar); + final platformUtil = PlatformUtil(messenger: registrar); + final headlessManager = HeadlessInAppWebViewManager(messenger: registrar); _nativeCommunication = allowInterop(_dartNativeCommunication); } - - /// Handles method calls over the MethodChannel of this plugin. - Future handleMethodCall(MethodCall call) async { - switch (call.method) { - default: - throw PlatformException( - code: 'Unimplemented', - details: 'flutter_inappwebview for web doesn\'t implement \'${call.method}\'', - ); - } - } } /// Allows assigning a function to be callable from `window.flutter_inappwebview.nativeCommunication()` @JS('flutter_inappwebview.nativeCommunication') -external set _nativeCommunication(Future Function(String method, int viewId, [List? args]) f); +external set _nativeCommunication(Future Function(String method, dynamic viewId, [List? args]) f); /// Allows calling the assigned function from Dart as well. @JS() -external Future nativeCommunication(String method, int viewId, [List? args]); +external Future nativeCommunication(String method, dynamic viewId, [List? args]); -Future _dartNativeCommunication(String method, int viewId, [List? args]) async { +Future _dartNativeCommunication(String method, dynamic viewId, [List? args]) async { if (WebPlatformManager.webViews.containsKey(viewId)) { var webViewHtmlElement = WebPlatformManager.webViews[viewId] as InAppWebViewWebElement; switch (method) { diff --git a/lib/src/web/web_platform_manager.dart b/lib/src/web/web_platform_manager.dart index acf8b3b3..74a73c24 100644 --- a/lib/src/web/web_platform_manager.dart +++ b/lib/src/web/web_platform_manager.dart @@ -1,3 +1,4 @@ abstract class WebPlatformManager { - static final Map webViews = {}; + static final String BRIDGE_JS_OBJECT_NAME = "flutter_inappwebview"; + static final Map webViews = {}; } \ No newline at end of file