From 92eba92a6c16582470c66c5d6257d69faa580085 Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Thu, 20 Oct 2022 16:34:37 +0200 Subject: [PATCH] Added InAppWebView.headlessWebView property to convert an HeadlessWebView to InAppWebView widget --- CHANGELOG.md | 10 ++ .../FlutterWebViewFactory.java | 28 ++-- .../flutter_inappwebview/Util.java | 32 +++++ .../HeadlessInAppWebView.java | 66 +++++++--- .../convert_to_inappwebview.dart | 74 +++++++++++ .../headless_in_app_webview/main.dart | 2 + .../take_screenshot.dart | 5 +- .../in_app_webview/audio_playback_policy.dart | 1 + .../in_app_webview/load_url.dart | 1 + .../in_app_webview/on_received_icon.dart | 1 + .../in_app_webview/post_requests.dart | 1 + .../in_app_webview/video_playback_policy.dart | 1 + .../ios/Flutter/flutter_export_environment.sh | 5 +- example/lib/generated_plugin_registrant.dart | 19 +++ example/lib/in_app_webiew_example.screen.dart | 4 +- example/lib/main.dart | 11 ++ .../HeadlessInAppWebView.swift | 13 ++ .../InAppWebView/FlutterWebViewFactory.swift | 7 + ios/Classes/InAppWebView/InAppWebView.swift | 124 ++++++++++++++---- .../InAppWebView/WebViewChannelDelegate.swift | 40 ++++++ lib/assets/web/web_support.js | 17 ++- .../chrome_safari_browser.dart | 1 + lib/src/in_app_browser/in_app_browser.dart | 1 + .../apple/in_app_webview_controller.dart | 1 + .../headless_in_app_webview.dart | 9 ++ lib/src/in_app_webview/in_app_webview.dart | 39 +++++- .../in_app_webview_controller.dart | 2 + lib/src/in_app_webview/main.dart | 2 +- .../pull_to_refresh_controller.dart | 2 + lib/src/util.dart | 1 + .../headless_in_app_web_view_web_element.dart | 13 +- .../web/headless_inappwebview_manager.dart | 11 +- lib/src/web/in_app_web_view_web_element.dart | 99 +++++++++----- lib/src/web/web_platform.dart | 2 +- .../HeadlessInAppWebView.swift | 14 ++ .../InAppWebView/FlutterWebViewFactory.swift | 7 + macos/Classes/InAppWebView/InAppWebView.swift | 117 +++++++++++++---- .../InAppWebView/WebViewChannelDelegate.swift | 28 ++++ pubspec.yaml | 2 +- 39 files changed, 676 insertions(+), 137 deletions(-) create mode 100644 example/integration_test/headless_in_app_webview/convert_to_inappwebview.dart create mode 100644 example/lib/generated_plugin_registrant.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f9007b37..980f34f6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.0-beta.4 + +- Added `InAppWebView.headlessWebView` property to convert an `HeadlessWebView` to `InAppWebView` widget + ## 6.0.0-beta.3 - Added MacOS support @@ -46,6 +50,12 @@ - Removed `URLProtectionSpace.iosIsProxy` property - `historyUrl` and `baseUrl` of `InAppWebViewInitialData` can be `null` +## 5.5.0+4 + +- Fixed "Many crashes on iOS: Completion handler was not called" [#1221](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1221) +- Fixed "webView:didReceiveAuthenticationChallenge:completionHandler" [#1128](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1128) +- Merged "Fix missing import for Flutter 2.8.1" [#1381](https://github.com/pichillilorenzo/flutter_inappwebview/pull/1381) (thanks to [chandrabezzo](https://github.com/chandrabezzo)) + ## 5.5.0+3 - Fixed iOS `toolbarTopTintColor` InAppBrowser option diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/FlutterWebViewFactory.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/FlutterWebViewFactory.java index 86185543..da5d2b40 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/FlutterWebViewFactory.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/FlutterWebViewFactory.java @@ -2,6 +2,8 @@ package com.pichillilorenzo.flutter_inappwebview; import android.content.Context; +import com.pichillilorenzo.flutter_inappwebview.headless_in_app_webview.HeadlessInAppWebView; +import com.pichillilorenzo.flutter_inappwebview.headless_in_app_webview.HeadlessInAppWebViewManager; import com.pichillilorenzo.flutter_inappwebview.webview.in_app_webview.FlutterWebView; import com.pichillilorenzo.flutter_inappwebview.webview.PlatformWebView; import com.pichillilorenzo.flutter_inappwebview.types.WebViewImplementation; @@ -24,15 +26,25 @@ public class FlutterWebViewFactory extends PlatformViewFactory { @Override public PlatformView create(Context context, int id, Object args) { HashMap params = (HashMap) args; - - PlatformWebView flutterWebView; - WebViewImplementation implementation = WebViewImplementation.fromValue((Integer) params.get("implementation")); - switch (implementation) { - case NATIVE: - default: - flutterWebView = new FlutterWebView(plugin, context, id, params); + PlatformWebView flutterWebView = null; + + String headlessWebViewId = (String) params.get("headlessWebViewId"); + if (headlessWebViewId != null) { + HeadlessInAppWebView headlessInAppWebView = HeadlessInAppWebViewManager.webViews.get(headlessWebViewId); + if (headlessInAppWebView != null) { + flutterWebView = headlessInAppWebView.disposeAndGetFlutterWebView(); + } + } + + if (flutterWebView == null) { + WebViewImplementation implementation = WebViewImplementation.fromValue((Integer) params.get("implementation")); + switch (implementation) { + case NATIVE: + default: + flutterWebView = new FlutterWebView(plugin, context, id, params); + } + flutterWebView.makeInitialLoad(params); } - flutterWebView.makeInitialLoad(params); return flutterWebView; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java index 79acae67..82f21139 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java @@ -2,18 +2,25 @@ package com.pichillilorenzo.flutter_inappwebview; import android.content.Context; import android.content.res.AssetManager; +import android.graphics.Insets; +import android.graphics.Rect; import android.net.http.SslCertificate; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.pichillilorenzo.flutter_inappwebview.types.Size2D; import com.pichillilorenzo.flutter_inappwebview.types.SyncBaseCallbackResultImpl; import org.json.JSONArray; @@ -242,6 +249,31 @@ public class Util { return context.getResources().getDisplayMetrics().density; } + public static Size2D getFullscreenSize(Context context) { + Size2D fullscreenSize = new Size2D(-1, -1); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (wm != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + final WindowMetrics metrics = wm.getCurrentWindowMetrics(); + // Gets all excluding insets + final WindowInsets windowInsets = metrics.getWindowInsets(); + Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() + | WindowInsets.Type.displayCutout()); + int insetsWidth = insets.right + insets.left; + int insetsHeight = insets.top + insets.bottom; + final Rect bounds = metrics.getBounds(); + fullscreenSize.setWidth(bounds.width() - insetsWidth); + fullscreenSize.setHeight(bounds.height() - insetsHeight); + } else { + DisplayMetrics displayMetrics = new DisplayMetrics(); + wm.getDefaultDisplay().getMetrics(displayMetrics); + fullscreenSize.setWidth(displayMetrics.widthPixels); + fullscreenSize.setHeight(displayMetrics.heightPixels); + } + } + return fullscreenSize; + } + public static boolean isClass(String className) { try { Class.forName(className); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/headless_in_app_webview/HeadlessInAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/headless_in_app_webview/HeadlessInAppWebView.java index 7edb5d84..768d07c8 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/headless_in_app_webview/HeadlessInAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/headless_in_app_webview/HeadlessInAppWebView.java @@ -9,13 +9,12 @@ import androidx.annotation.Nullable; import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin; import com.pichillilorenzo.flutter_inappwebview.Util; -import com.pichillilorenzo.flutter_inappwebview.webview.in_app_webview.FlutterWebView; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; import com.pichillilorenzo.flutter_inappwebview.types.Size2D; +import com.pichillilorenzo.flutter_inappwebview.webview.in_app_webview.FlutterWebView; import java.util.Map; -import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; public class HeadlessInAppWebView implements Disposable { @@ -54,15 +53,16 @@ public class HeadlessInAppWebView implements Disposable { ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0); if (mainView != null && flutterWebView != null) { View view = flutterWebView.getView(); - final Map initialSize = (Map) params.get("initialSize"); - Size2D size = Size2D.fromMap(initialSize); - if (size != null) { + if (view != null) { + final Map initialSize = (Map) params.get("initialSize"); + Size2D size = Size2D.fromMap(initialSize); + if (size == null) { + size = new Size2D(-1, -1); + } setSize(size); - } else { - view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mainView.addView(view, 0); + view.setVisibility(View.INVISIBLE); } - mainView.addView(view, 0); - view.setVisibility(View.INVISIBLE); } } } @@ -71,8 +71,13 @@ public class HeadlessInAppWebView implements Disposable { public void setSize(@NonNull Size2D size) { if (flutterWebView != null && flutterWebView.webView != null) { View view = flutterWebView.getView(); - float scale = Util.getPixelDensity(view.getContext()); - view.setLayoutParams(new FrameLayout.LayoutParams((int) (size.getWidth() * scale), (int) (size.getHeight() * scale))); + if (view != null) { + float scale = Util.getPixelDensity(view.getContext()); + Size2D fullscreenSize = Util.getFullscreenSize(view.getContext()); + int width = (int) (size.getWidth() == -1 ? fullscreenSize.getWidth() : (size.getWidth() * scale)); + int height = (int) (size.getWidth() == -1 ? fullscreenSize.getHeight() : (size.getHeight() * scale)); + view.setLayoutParams(new FrameLayout.LayoutParams(width, height)); + } } } @@ -80,13 +85,41 @@ public class HeadlessInAppWebView implements Disposable { public Size2D getSize() { if (flutterWebView != null && flutterWebView.webView != null) { View view = flutterWebView.getView(); - float scale = Util.getPixelDensity(view.getContext()); - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - return new Size2D(layoutParams.width / scale, layoutParams.height / scale); + if (view != null) { + float scale = Util.getPixelDensity(view.getContext()); + Size2D fullscreenSize = Util.getFullscreenSize(view.getContext()); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + return new Size2D( + fullscreenSize.getWidth() == layoutParams.width ? layoutParams.width : (layoutParams.width / scale), + fullscreenSize.getHeight() == layoutParams.height ? layoutParams.height : (layoutParams.height / scale) + ); + } } return null; } + @Nullable + public FlutterWebView disposeAndGetFlutterWebView() { + FlutterWebView newFlutterWebView = flutterWebView; + if (flutterWebView != null) { + View view = flutterWebView.getView(); + if (view != null) { + // restore WebView layout params and visibility + view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + view.setVisibility(View.VISIBLE); + // remove from parent + ViewGroup parent = (ViewGroup) view.getParent(); + if (parent != null) { + parent.removeView(view); + } + } + // set to null to avoid to be disposed before calling "dispose()" + flutterWebView = null; + dispose(); + } + return newFlutterWebView; + } + public void dispose() { if (channelDelegate != null) { channelDelegate.dispose(); @@ -100,7 +133,10 @@ public class HeadlessInAppWebView implements Disposable { if (contentView != null) { ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0); if (mainView != null && flutterWebView != null) { - mainView.removeView(flutterWebView.getView()); + View view = flutterWebView.getView(); + if (view != null) { + mainView.removeView(flutterWebView.getView()); + } } } } diff --git a/example/integration_test/headless_in_app_webview/convert_to_inappwebview.dart b/example/integration_test/headless_in_app_webview/convert_to_inappwebview.dart new file mode 100644 index 00000000..c0f128b8 --- /dev/null +++ b/example/integration_test/headless_in_app_webview/convert_to_inappwebview.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../constants.dart'; + +void convertToInAppWebView() { + final shouldSkip = kIsWeb + ? false + : ![ + TargetPlatform.android, + TargetPlatform.iOS, + ].contains(defaultTargetPlatform); + + testWidgets('convert to InAppWebView', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + + var headlessWebView = new HeadlessInAppWebView( + initialUrlRequest: URLRequest(url: TEST_CROSS_PLATFORM_URL_1), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + ); + headlessWebView.onLoadStop = (controller, url) async { + pageLoaded.complete(); + }; + + await headlessWebView.run(); + expect(headlessWebView.isRunning(), true); + + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + final String? url = (await controller.getUrl())?.toString(); + expect(url, TEST_CROSS_PLATFORM_URL_1.toString()); + + final Completer widgetControllerCompleter = + Completer(); + final Completer loadedUrl = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + headlessWebView: headlessWebView, + onWebViewCreated: (controller) { + widgetControllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + if (url.toString() == TEST_CROSS_PLATFORM_URL_2.toString() && + !loadedUrl.isCompleted) { + loadedUrl.complete(url.toString()); + } + }, + ), + ), + ); + final InAppWebViewController widgetController = await widgetControllerCompleter.future; + + expect(headlessWebView.isRunning(), false); + + expect((await widgetController.getUrl())?.toString(), TEST_CROSS_PLATFORM_URL_1.toString()); + + await widgetController.loadUrl( + urlRequest: URLRequest(url: TEST_CROSS_PLATFORM_URL_2)); + expect(await loadedUrl.future, TEST_CROSS_PLATFORM_URL_2.toString()); + }, skip: shouldSkip); +} diff --git a/example/integration_test/headless_in_app_webview/main.dart b/example/integration_test/headless_in_app_webview/main.dart index ce4f5d3f..02e4b22e 100644 --- a/example/integration_test/headless_in_app_webview/main.dart +++ b/example/integration_test/headless_in_app_webview/main.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; +import 'convert_to_inappwebview.dart'; import 'take_screenshot.dart'; import 'custom_size.dart'; import 'run_and_dispose.dart'; @@ -11,5 +12,6 @@ void main() { takeScreenshot(); customSize(); setGetSettings(); + convertToInAppWebView(); }); } diff --git a/example/integration_test/headless_in_app_webview/take_screenshot.dart b/example/integration_test/headless_in_app_webview/take_screenshot.dart index 5ceb9c49..1711b18b 100644 --- a/example/integration_test/headless_in_app_webview/take_screenshot.dart +++ b/example/integration_test/headless_in_app_webview/take_screenshot.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -26,7 +27,9 @@ void takeScreenshot() { controllerCompleter.complete(controller); }, onLoadStop: (controller, url) async { - pageLoaded.complete(); + if (!pageLoaded.isCompleted) { + pageLoaded.complete(); + } }); await headlessWebView.run(); diff --git a/example/integration_test/in_app_webview/audio_playback_policy.dart b/example/integration_test/in_app_webview/audio_playback_policy.dart index 89b6b46c..6d4d2d5b 100644 --- a/example/integration_test/in_app_webview/audio_playback_policy.dart +++ b/example/integration_test/in_app_webview/audio_playback_policy.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/example/integration_test/in_app_webview/load_url.dart b/example/integration_test/in_app_webview/load_url.dart index 30d25133..8e76ed11 100644 --- a/example/integration_test/in_app_webview/load_url.dart +++ b/example/integration_test/in_app_webview/load_url.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; diff --git a/example/integration_test/in_app_webview/on_received_icon.dart b/example/integration_test/in_app_webview/on_received_icon.dart index b2588f19..f2bf90b8 100644 --- a/example/integration_test/in_app_webview/on_received_icon.dart +++ b/example/integration_test/in_app_webview/on_received_icon.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; diff --git a/example/integration_test/in_app_webview/post_requests.dart b/example/integration_test/in_app_webview/post_requests.dart index 27317eb5..3703cfa6 100644 --- a/example/integration_test/in_app_webview/post_requests.dart +++ b/example/integration_test/in_app_webview/post_requests.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; diff --git a/example/integration_test/in_app_webview/video_playback_policy.dart b/example/integration_test/in_app_webview/video_playback_policy.dart index ba2db5cf..2178a0ce 100644 --- a/example/integration_test/in_app_webview/video_playback_policy.dart +++ b/example/integration_test/in_app_webview/video_playback_policy.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index 9e98dd5e..fae63896 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -3,12 +3,11 @@ export "FLUTTER_ROOT=/Users/lorenzopichilli/fvm/versions/2.10.4" export "FLUTTER_APPLICATION_PATH=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example" export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/lib/main.dart" +export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" -export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/.dart_tool/package_config.json" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart new file mode 100644 index 00000000..777d4a94 --- /dev/null +++ b/example/lib/generated_plugin_registrant.dart @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// ignore_for_file: directives_ordering +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: depend_on_referenced_packages + +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:url_launcher_web/url_launcher_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(Registrar registrar) { + FlutterInAppWebViewWebPlatform.registerWith(registrar); + UrlLauncherPlugin.registerWith(registrar); + registrar.registerMessageHandler(); +} diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index ee37d50c..39a33dff 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -114,8 +114,9 @@ class _InAppWebViewExampleScreenState extends State { children: [ InAppWebView( key: webViewKey, + headlessWebView: headlessWebView, initialUrlRequest: - URLRequest(url: Uri.parse('https://flutter.dev')), + URLRequest(url: Uri.parse('https://google.com')), // initialUrlRequest: // URLRequest(url: Uri.parse(Uri.base.toString().replaceFirst("/#/", "/") + 'page.html')), // initialFile: "assets/index.html", @@ -125,6 +126,7 @@ class _InAppWebViewExampleScreenState extends State { pullToRefreshController: pullToRefreshController, onWebViewCreated: (controller) async { webViewController = controller; + print(await controller.getUrl()); }, onLoadStart: (controller, url) async { setState(() { diff --git a/example/lib/main.dart b/example/lib/main.dart index 24450208..63aa79bf 100755 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -15,6 +15,13 @@ import 'package:pointer_interceptor/pointer_interceptor.dart'; InAppLocalhostServer localhostServer = new InAppLocalhostServer(documentRoot: 'assets'); +var headlessWebView = new HeadlessInAppWebView( + initialUrlRequest: URLRequest(url: Uri.parse('https://flutter.dev')), + shouldOverrideUrlLoading: (controller, navigationAction) async { + return NavigationActionPolicy.ALLOW; + }, +); + Future main() async { WidgetsFlutterBinding.ensureInitialized(); // await Permission.camera.request(); @@ -29,6 +36,10 @@ Future main() async { await localhostServer.start(); } + headlessWebView.run(); + + await Future.delayed(Duration(seconds: 1)); + runApp(MyApp()); } diff --git a/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift b/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift index d688f50f..5759b20b 100644 --- a/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift +++ b/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift @@ -61,6 +61,19 @@ public class HeadlessInAppWebView : Disposable { return nil } + public func disposeAndGetFlutterWebView(withFrame frame: CGRect) -> FlutterWebViewController? { + let newFlutterWebView = flutterWebView + if let view = flutterWebView?.view() { + // restore WebView frame and alpha + view.frame = frame + view.alpha = 1.0 + // remove from parent + view.removeFromSuperview() + dispose() + } + return newFlutterWebView + } + public func dispose() { channelDelegate?.dispose() channelDelegate = nil diff --git a/ios/Classes/InAppWebView/FlutterWebViewFactory.swift b/ios/Classes/InAppWebView/FlutterWebViewFactory.swift index 2e202510..b3685802 100755 --- a/ios/Classes/InAppWebView/FlutterWebViewFactory.swift +++ b/ios/Classes/InAppWebView/FlutterWebViewFactory.swift @@ -23,6 +23,13 @@ public class FlutterWebViewFactory: NSObject, FlutterPlatformViewFactory { public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { let arguments = args as? NSDictionary + + if let headlessWebViewId = arguments?["headlessWebViewId"] as? String, + let headlessWebView = HeadlessInAppWebViewManager.webViews[headlessWebViewId], + let platformView = headlessWebView?.disposeAndGetFlutterWebView(withFrame: frame) { + return platformView + } + let webviewController = FlutterWebViewController(registrar: registrar!, withFrame: frame, viewIdentifier: viewId, diff --git a/ios/Classes/InAppWebView/InAppWebView.swift b/ios/Classes/InAppWebView/InAppWebView.swift index a9057d20..427aa1ca 100755 --- a/ios/Classes/InAppWebView/InAppWebView.swift +++ b/ios/Classes/InAppWebView/InAppWebView.swift @@ -1570,9 +1570,11 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")" let permissionRequest = PermissionRequest(origin: origin, resources: [type.rawValue], frame: frame) + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.PermissionRequestCallback() callback.nonNullSuccess = { (response: PermissionResponse) in if let action = response.action { + decisionHandlerCalled = true switch action { case 1: decisionHandler(.grant) @@ -1588,7 +1590,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: PermissionResponse?) in - decisionHandler(.deny) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.deny) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1610,9 +1615,11 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")" let permissionRequest = PermissionRequest(origin: origin, resources: ["deviceOrientationAndMotion"], frame: frame) + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.PermissionRequestCallback() callback.nonNullSuccess = { (response: PermissionResponse) in if let action = response.action { + decisionHandlerCalled = true switch action { case 1: decisionHandler(.grant) @@ -1628,7 +1635,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: PermissionResponse?) in - decisionHandler(.deny) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.deny) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1694,13 +1704,18 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return } + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in + decisionHandlerCalled = true decisionHandler(response) return false } callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in - decisionHandler(.allow) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1726,13 +1741,18 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, let useOnNavigationResponse = settings?.useOnNavigationResponse if useOnNavigationResponse != nil, useOnNavigationResponse! { + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.NavigationResponseCallback() callback.nonNullSuccess = { (response: WKNavigationResponsePolicy) in + decisionHandlerCalled = true decisionHandler(response) return false } callback.defaultBehaviour = { (response: WKNavigationResponsePolicy?) in - decisionHandler(.allow) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1747,7 +1767,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, } if let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { - if #available(iOS 14.5, *), !navigationResponse.canShowMIMEType { + if #available(iOS 14.5, *), !navigationResponse.canShowMIMEType, useOnNavigationResponse == nil || !useOnNavigationResponse! { decisionHandler(.download) return } else { @@ -1856,6 +1876,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return } + var completionHandlerCalled = false if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest || @@ -1869,6 +1890,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, let callback = WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() callback.nonNullSuccess = { (response: HttpAuthResponse) in if let action = response.action { + completionHandlerCalled = true switch action { case 0: InAppWebView.credentialsProposed = [] @@ -1917,7 +1939,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: HttpAuthResponse?) in - completionHandler(.performDefaultHandling, nil) + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1944,6 +1969,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in if let action = response.action { + completionHandlerCalled = true switch action { case 0: InAppWebView.credentialsProposed = [] @@ -1964,7 +1990,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: ServerTrustAuthResponse?) in - completionHandler(.performDefaultHandling, nil) + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1981,6 +2010,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback() callback.nonNullSuccess = { (response: ClientCertResponse) in if let action = response.action { + completionHandlerCalled = true switch action { case 0: completionHandler(.cancelAuthenticationChallenge, nil) @@ -2017,7 +2047,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: ClientCertResponse?) in - completionHandler(.performDefaultHandling, nil) + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -2102,9 +2135,12 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return } + var completionHandlerCalled = false + let callback = WebViewChannelDelegate.JsAlertCallback() callback.nonNullSuccess = { (response: JsAlertResponse) in if response.handledByClient { + completionHandlerCalled = true let action = response.action ?? 1 switch action { case 0: @@ -2118,14 +2154,20 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: JsAlertResponse?) in - let responseMessage = response?.message - let confirmButtonTitle = response?.confirmButtonTitle - self.createAlertDialog(message: message, responseMessage: responseMessage, - confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + self.createAlertDialog(message: message, responseMessage: responseMessage, + confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + } } callback.error = { (code: String, message: String?, details: Any?) in - print(code + ", " + (message ?? "")) - completionHandler() + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler() + } } if let channelDelegate = channelDelegate { @@ -2159,9 +2201,12 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + var completionHandlerCalled = false + let callback = WebViewChannelDelegate.JsConfirmCallback() callback.nonNullSuccess = { (response: JsConfirmResponse) in if response.handledByClient { + completionHandlerCalled = true let action = response.action ?? 1 switch action { case 0: @@ -2178,14 +2223,20 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: JsConfirmResponse?) in - let responseMessage = response?.message - let confirmButtonTitle = response?.confirmButtonTitle - let cancelButtonTitle = response?.cancelButtonTitle - self.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + self.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + } } callback.error = { (code: String, message: String?, details: Any?) in - print(code + ", " + (message ?? "")) - completionHandler(false) + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(false) + } } if let channelDelegate = channelDelegate { @@ -2230,9 +2281,13 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { + + var completionHandlerCalled = false + let callback = WebViewChannelDelegate.JsPromptCallback() callback.nonNullSuccess = { (response: JsPromptResponse) in if response.handledByClient { + completionHandlerCalled = true let action = response.action ?? 1 switch action { case 0: @@ -2249,16 +2304,22 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return true } callback.defaultBehaviour = { (response: JsPromptResponse?) in - let responseMessage = response?.message - let confirmButtonTitle = response?.confirmButtonTitle - let cancelButtonTitle = response?.cancelButtonTitle - let value = response?.value - self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, - cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + let value = response?.value + self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, + cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + } } callback.error = { (code: String, message: String?, details: Any?) in - print(code + ", " + (message ?? "")) - completionHandler(nil) + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(nil) + } } if let channelDelegate = channelDelegate { @@ -2375,13 +2436,18 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, return } + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback() callback.nonNullSuccess = { (action: Bool) in + decisionHandlerCalled = true decisionHandler(action) return false } callback.defaultBehaviour = { (action: Bool?) in - decisionHandler(false) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(false) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) diff --git a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift index 4b5e6cae..a56a1988 100644 --- a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift +++ b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -740,6 +740,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return JsAlertResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onJsAlert(url: URL?, message: String, isMainFrame: Bool, callback: JsAlertCallback) { @@ -762,6 +766,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return JsConfirmResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onJsConfirm(url: URL?, message: String, isMainFrame: Bool, callback: JsConfirmCallback) { @@ -784,6 +792,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return JsPromptResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onJsPrompt(url: URL?, message: String, defaultValue: String?, isMainFrame: Bool, callback: JsPromptCallback) { @@ -851,6 +863,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return PermissionResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onPermissionRequest(request: PermissionRequest, callback: PermissionRequestCallback) { @@ -871,6 +887,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return WKNavigationActionPolicy.cancel } } + + deinit { + self.defaultBehaviour(nil) + } } public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, callback: ShouldOverrideUrlLoadingCallback) { @@ -922,6 +942,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return HttpAuthResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge, callback: ReceivedHttpAuthRequestCallback) { @@ -939,6 +963,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return ServerTrustAuthResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge, callback: ReceivedServerTrustAuthRequestCallback) { @@ -956,6 +984,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return ClientCertResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onReceivedClientCertRequest(challenge: ClientCertChallenge, callback: ReceivedClientCertRequestCallback) { @@ -1030,6 +1062,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return WKNavigationResponsePolicy.cancel } } + + deinit { + self.defaultBehaviour(nil) + } } public func onNavigationResponse(navigationResponse: WKNavigationResponse, callback: NavigationResponseCallback) { @@ -1050,6 +1086,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return false } } + + deinit { + self.defaultBehaviour(nil) + } } public func shouldAllowDeprecatedTLS(challenge: URLAuthenticationChallenge, callback: ShouldAllowDeprecatedTLSCallback) { diff --git a/lib/assets/web/web_support.js b/lib/assets/web/web_support.js index e84e37df..98507574 100644 --- a/lib/assets/web/web_support.js +++ b/lib/assets/web/web_support.js @@ -5,6 +5,7 @@ window.flutter_inappwebview = { viewId: viewId, iframeId: iframeId, iframe: null, + iframeContainer: null, windowAutoincrementId: 0, windows: {}, isFullscreen: false, @@ -19,6 +20,7 @@ window.flutter_inappwebview = { prepare: function(settings) { webView.settings = settings; var iframe = document.getElementById(iframeId); + var iframeContainer = document.getElementById(iframeId + '-container'); document.addEventListener('fullscreenchange', function(event) { // document.fullscreenElement will point to the element that @@ -37,6 +39,7 @@ window.flutter_inappwebview = { if (iframe != null) { webView.iframe = iframe; + webView.iframeContainer = iframeContainer; iframe.addEventListener('load', function (event) { webView.windowAutoincrementId = 0; webView.windows = {}; @@ -543,20 +546,20 @@ window.flutter_inappwebview = { return false; }, getSize: function() { - var iframe = webView.iframe; + var iframeContainer = webView.iframeContainer; var width = 0.0; var height = 0.0; - if (iframe.style.width != null && iframe.style.width != '' && iframe.style.width.indexOf('px') > 0) { - width = parseFloat(iframe.style.width); + if (iframeContainer.style.width != null && iframeContainer.style.width != '' && iframeContainer.style.width.indexOf('px') > 0) { + width = parseFloat(iframeContainer.style.width); } if (width == null || width == 0.0) { - width = iframe.getBoundingClientRect().width; + width = iframeContainer.getBoundingClientRect().width; } - if (iframe.style.height != null && iframe.style.height != '' && iframe.style.height.indexOf('px') > 0) { - height = parseFloat(iframe.style.height); + if (iframeContainer.style.height != null && iframeContainer.style.height != '' && iframeContainer.style.height.indexOf('px') > 0) { + height = parseFloat(iframeContainer.style.height); } if (height == null || height == 0.0) { - height = iframe.getBoundingClientRect().height; + height = iframeContainer.getBoundingClientRect().height; } return { diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index c5082e3e..2a8706c0 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index 516645ca..82fd6d3a 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:typed_data'; import 'package:flutter/services.dart'; diff --git a/lib/src/in_app_webview/apple/in_app_webview_controller.dart b/lib/src/in_app_webview/apple/in_app_webview_controller.dart index 6bd991fe..84056e08 100644 --- a/lib/src/in_app_webview/apple/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/apple/in_app_webview_controller.dart @@ -1,4 +1,5 @@ import 'dart:core'; +import 'dart:typed_data'; import 'package:flutter/services.dart'; 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 36400aeb..6efb0419 100644 --- a/lib/src/in_app_webview/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -1,4 +1,6 @@ import 'dart:collection'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/src/util.dart'; @@ -714,3 +716,10 @@ class HeadlessInAppWebView implements WebView, Disposable { MediaCaptureState? newState, )? onMicrophoneCaptureStateChanged; } + +extension InternalHeadlessInAppWebView on HeadlessInAppWebView { + Future internalDispose() async { + _started = false; + _running = false; + } +} \ No newline at end of file diff --git a/lib/src/in_app_webview/in_app_webview.dart b/lib/src/in_app_webview/in_app_webview.dart index b4166445..ad450933 100755 --- a/lib/src/in_app_webview/in_app_webview.dart +++ b/lib/src/in_app_webview/in_app_webview.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -7,6 +8,8 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter_inappwebview/src/in_app_webview/headless_in_app_webview.dart'; +import 'package:flutter_inappwebview/src/util.dart'; import '../find_interaction/find_interaction_controller.dart'; import '../web/web_platform_manager.dart'; @@ -37,9 +40,21 @@ class InAppWebView extends StatefulWidget implements WebView { final Set>? gestureRecognizers; ///The window id of a [CreateWindowAction.windowId]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS @override final int? windowId; + ///The [HeadlessInAppWebView] to use to initialize this widget + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- Web + final HeadlessInAppWebView? headlessWebView; + const InAppWebView({ Key? key, this.windowId, @@ -148,6 +163,7 @@ class InAppWebView extends StatefulWidget implements WebView { this.onCameraCaptureStateChanged, this.onMicrophoneCaptureStateChanged, this.gestureRecognizers, + this.headlessWebView, }) : super(key: key); @override @@ -609,8 +625,11 @@ class _InAppWebViewState extends State { webViewHtmlElement.initialUrlRequest = widget.initialUrlRequest; webViewHtmlElement.initialFile = widget.initialFile; webViewHtmlElement.initialData = widget.initialData; + webViewHtmlElement.headlessWebViewId = widget.headlessWebView?.isRunning() ?? false ? widget.headlessWebView?.id : null; webViewHtmlElement.prepare(); - webViewHtmlElement.makeInitialLoad(); + if (webViewHtmlElement.headlessWebViewId == null) { + webViewHtmlElement.makeInitialLoad(); + } _onPlatformViewCreated(viewId); }, ); @@ -653,6 +672,7 @@ class _InAppWebViewState extends State { 'initialSettings': initialSettings, 'contextMenu': widget.contextMenu?.toMap() ?? {}, 'windowId': widget.windowId, + 'headlessWebViewId': widget.headlessWebView?.isRunning() ?? false ? widget.headlessWebView?.id : null, 'implementation': widget.implementation.toNativeValue(), 'initialUserScripts': widget.initialUserScripts?.map((e) => e.toMap()).toList() ?? @@ -680,6 +700,7 @@ class _InAppWebViewState extends State { 'initialSettings': initialSettings, 'contextMenu': widget.contextMenu?.toMap() ?? {}, 'windowId': widget.windowId, + 'headlessWebViewId': widget.headlessWebView?.isRunning() ?? false ? widget.headlessWebView?.id : null, 'implementation': widget.implementation.toNativeValue(), 'initialUserScripts': widget.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], @@ -702,6 +723,7 @@ class _InAppWebViewState extends State { 'initialSettings': initialSettings, 'contextMenu': widget.contextMenu?.toMap() ?? {}, 'windowId': widget.windowId, + 'headlessWebViewId': widget.headlessWebView?.isRunning() ?? false ? widget.headlessWebView?.id : null, 'implementation': widget.implementation.toNativeValue(), 'initialUserScripts': widget.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], @@ -732,10 +754,19 @@ class _InAppWebViewState extends State { } void _onPlatformViewCreated(int id) { - _controller = InAppWebViewController(id, widget); - widget.pullToRefreshController?.initMethodChannel(id); - widget.findInteractionController?.initMethodChannel(id); + final viewId = (!kIsWeb && (widget.headlessWebView?.isRunning() ?? false)) ? widget.headlessWebView?.id : id; + widget.headlessWebView?.internalDispose(); + _controller = InAppWebViewController(viewId, widget); + widget.pullToRefreshController?.initMethodChannel(viewId); + widget.findInteractionController?.initMethodChannel(viewId); if (widget.onWebViewCreated != null) { + debugLog( + className: "InAppWebView", + name: "WebView", + id: viewId?.toString(), + debugLoggingSettings: WebView.debugLoggingSettings, + method: "onWebViewCreated", + args: []); widget.onWebViewCreated!(_controller!); } } diff --git a/lib/src/in_app_webview/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart index 0698c35b..b8e1887e 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -3,6 +3,8 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:core'; import 'dart:developer' as developer; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/lib/src/in_app_webview/main.dart b/lib/src/in_app_webview/main.dart index 0e512b6d..37afa89f 100644 --- a/lib/src/in_app_webview/main.dart +++ b/lib/src/in_app_webview/main.dart @@ -7,7 +7,7 @@ export 'in_app_webview_settings.dart' InAppWebViewGroupOptions, WebViewOptions, InAppWebViewOptions; -export 'headless_in_app_webview.dart'; +export 'headless_in_app_webview.dart' hide InternalHeadlessInAppWebView; export 'android/main.dart'; export 'apple/main.dart'; export '../find_interaction/find_interaction_controller.dart'; 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 1c6d26aa..f521d535 100644 --- a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart +++ b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/services.dart'; import '../in_app_webview/webview.dart'; import '../in_app_browser/in_app_browser.dart'; diff --git a/lib/src/util.dart b/lib/src/util.dart index 7af26ea0..34852103 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'dart:developer' as developer; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; 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 index 7d6e07c5..baa4474e 100644 --- a/lib/src/web/headless_in_app_web_view_web_element.dart +++ b/lib/src/web/headless_in_app_web_view_web_element.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter/services.dart'; +import 'headless_inappwebview_manager.dart'; import 'in_app_web_view_web_element.dart'; import '../util.dart'; import '../types/disposable.dart'; @@ -59,14 +61,21 @@ class HeadlessInAppWebViewWebElement implements Disposable { } void setSize(Size size) { - webView?.iframe.style.width = size.width.toString() + "px"; - webView?.iframe.style.height = size.height.toString() + "px"; + webView?.iframeContainer.style.width = size.width.toString() + "px"; + webView?.iframeContainer.style.height = size.height.toString() + "px"; + } + + InAppWebViewWebElement? disposeAndGetFlutterWebView() { + InAppWebViewWebElement? newFlutterWebView = webView; + dispose(); + return newFlutterWebView; } @override void dispose() { _channel?.setMethodCallHandler(null); _channel = null; + HeadlessInAppWebViewManager.webViews.putIfAbsent(id, () => null); webView?.dispose(); webView = null; } diff --git a/lib/src/web/headless_inappwebview_manager.dart b/lib/src/web/headless_inappwebview_manager.dart index 173b0d4a..06f7a1af 100644 --- a/lib/src/web/headless_inappwebview_manager.dart +++ b/lib/src/web/headless_inappwebview_manager.dart @@ -9,6 +9,8 @@ import 'headless_in_app_web_view_web_element.dart'; import '../types/main.dart'; class HeadlessInAppWebViewManager { + static final Map webViews = {}; + static late MethodChannel _sharedChannel; late BinaryMessenger _messenger; @@ -50,17 +52,18 @@ class HeadlessInAppWebViewManager { var headlessWebView = HeadlessInAppWebViewWebElement( id: id, messenger: _messenger, webView: webView); WebPlatformManager.webViews.putIfAbsent(id, () => webView); + HeadlessInAppWebViewManager.webViews.putIfAbsent(id, () => headlessWebView); prepare(webView, params); headlessWebView.onWebViewCreated(); webView.makeInitialLoad(); } void prepare(InAppWebViewWebElement webView, Map params) { - webView.iframe.style.display = 'none'; + webView.iframeContainer.style.display = 'none'; Map? initialSize = params["initialSize"]?.cast(); if (initialSize != null) { - webView.iframe.style.width = initialSize["width"].toString() + 'px'; - webView.iframe.style.height = initialSize["height"].toString() + 'px'; + webView.iframeContainer.style.width = initialSize["width"].toString() + 'px'; + webView.iframeContainer.style.height = initialSize["height"].toString() + 'px'; } Map initialSettings = params["initialSettings"].cast(); @@ -74,7 +77,7 @@ class HeadlessInAppWebViewManager { webView.initialFile = params["initialFile"]; webView.initialData = InAppWebViewInitialData.fromMap( params["initialData"]?.cast()); - document.body?.append(webView.iframe); + document.body?.append(webView.iframeContainer); webView.prepare(); } } 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 687d92ec..875fe34e 100644 --- a/lib/src/web/in_app_web_view_web_element.dart +++ b/lib/src/web/in_app_web_view_web_element.dart @@ -1,8 +1,11 @@ import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/services.dart'; import 'dart:html'; import 'dart:js' as js; +import 'headless_inappwebview_manager.dart'; import 'web_platform_manager.dart'; import '../in_app_webview/in_app_webview_settings.dart'; import '../types/main.dart'; @@ -11,14 +14,16 @@ import '../types/disposable.dart'; class InAppWebViewWebElement implements Disposable { late dynamic _viewId; late BinaryMessenger _messenger; + late DivElement iframeContainer; late IFrameElement iframe; late MethodChannel? _channel; InAppWebViewSettings? initialSettings; URLRequest? initialUrlRequest; InAppWebViewInitialData? initialData; String? initialFile; + String? headlessWebViewId; - late InAppWebViewSettings settings; + InAppWebViewSettings? settings; late js.JsObject bridgeJsObject; bool isLoading = false; @@ -26,11 +31,17 @@ class InAppWebViewWebElement implements Disposable { {required dynamic viewId, required BinaryMessenger messenger}) { this._viewId = viewId; this._messenger = messenger; + iframeContainer = DivElement() + ..id = 'flutter_inappwebview-$_viewId-container' + ..style.height = '100%' + ..style.width = '100%' + ..style.border = 'none'; iframe = IFrameElement() ..id = 'flutter_inappwebview-$_viewId' ..style.height = '100%' ..style.width = '100%' ..style.border = 'none'; + iframeContainer.append(iframe); _channel = MethodChannel( 'com.pichillilorenzo/flutter_inappwebview_$_viewId', @@ -173,35 +184,59 @@ class InAppWebViewWebElement implements Disposable { } void prepare() { - settings = initialSettings ?? InAppWebViewSettings(); + if (headlessWebViewId != null) { + final headlessWebView = HeadlessInAppWebViewManager.webViews[headlessWebViewId!]; + if (headlessWebView != null && headlessWebView.webView != null) { + final webView = headlessWebView.disposeAndGetFlutterWebView(); + if (webView != null) { + webView.iframe.id = iframe.id; + iframe.remove(); + iframeContainer.append(webView.iframe); + iframe = webView.iframe; - Set sandbox = Set.from(Sandbox.values); + initialSettings = webView.initialSettings; + settings = webView.settings; + initialUrlRequest = webView.initialUrlRequest; + initialData = webView.initialData; + initialFile = webView.initialFile; - if (settings.javaScriptEnabled != null && !settings.javaScriptEnabled!) { - sandbox.remove(Sandbox.ALLOW_SCRIPTS); + bridgeJsObject['webViews'][_viewId] = bridgeJsObject + .callMethod("createFlutterInAppWebView", [_viewId, iframe.id]); + } + } } - iframe.allow = settings.iframeAllow ?? iframe.allow; - iframe.allowFullscreen = - settings.iframeAllowFullscreen ?? iframe.allowFullscreen; - iframe.referrerPolicy = - settings.iframeReferrerPolicy?.toNativeValue() ?? iframe.referrerPolicy; - iframe.name = settings.iframeName ?? iframe.name; - iframe.csp = settings.iframeCsp ?? iframe.csp; + if (headlessWebViewId == null && settings == null) { + settings = initialSettings ?? InAppWebViewSettings(); - if (settings.iframeSandbox != null && - settings.iframeSandbox != Sandbox.ALLOW_ALL) { - iframe.setAttribute("sandbox", - settings.iframeSandbox!.map((e) => e.toNativeValue()).join(" ")); - } else if (settings.iframeSandbox == Sandbox.ALLOW_ALL) { - iframe.removeAttribute("sandbox"); - } else if (sandbox != Sandbox.values) { - iframe.setAttribute( - "sandbox", sandbox.map((e) => e.toNativeValue()).join(" ")); - settings.iframeSandbox = sandbox; + Set sandbox = Set.from(Sandbox.values); + + if (settings!.javaScriptEnabled != null && !settings!.javaScriptEnabled!) { + sandbox.remove(Sandbox.ALLOW_SCRIPTS); + } + + iframe.allow = settings!.iframeAllow ?? iframe.allow; + iframe.allowFullscreen = + settings!.iframeAllowFullscreen ?? iframe.allowFullscreen; + iframe.referrerPolicy = + settings!.iframeReferrerPolicy?.toNativeValue() ?? iframe.referrerPolicy; + iframe.name = settings!.iframeName ?? iframe.name; + iframe.csp = settings!.iframeCsp ?? iframe.csp; + + if (settings!.iframeSandbox != null && + settings!.iframeSandbox != Sandbox.ALLOW_ALL) { + iframe.setAttribute("sandbox", + settings!.iframeSandbox!.map((e) => e.toNativeValue()).join(" ")); + } else if (settings!.iframeSandbox == Sandbox.ALLOW_ALL) { + iframe.removeAttribute("sandbox"); + } else if (sandbox != Sandbox.values) { + iframe.setAttribute( + "sandbox", sandbox.map((e) => e.toNativeValue()).join(" ")); + settings!.iframeSandbox = sandbox; + } } - _callMethod("prepare", [js.JsObject.jsify(settings.toMap())]); + _callMethod("prepare", [js.JsObject.jsify(settings!.toMap())]); } dynamic _callMethod(Object method, [List? args]) { @@ -405,7 +440,7 @@ class InAppWebViewWebElement implements Disposable { Set sandbox = getSandbox(); if (newSettings.javaScriptEnabled != null && - settings.javaScriptEnabled != newSettings.javaScriptEnabled) { + settings!.javaScriptEnabled != newSettings.javaScriptEnabled) { if (!newSettings.javaScriptEnabled!) { sandbox.remove(Sandbox.ALLOW_SCRIPTS); } else { @@ -413,23 +448,23 @@ class InAppWebViewWebElement implements Disposable { } } - if (settings.iframeAllow != newSettings.iframeAllow) { + if (settings!.iframeAllow != newSettings.iframeAllow) { iframe.allow = newSettings.iframeAllow; } - if (settings.iframeAllowFullscreen != newSettings.iframeAllowFullscreen) { + if (settings!.iframeAllowFullscreen != newSettings.iframeAllowFullscreen) { iframe.allowFullscreen = newSettings.iframeAllowFullscreen; } - if (settings.iframeReferrerPolicy != newSettings.iframeReferrerPolicy) { + if (settings!.iframeReferrerPolicy != newSettings.iframeReferrerPolicy) { iframe.referrerPolicy = newSettings.iframeReferrerPolicy?.toNativeValue(); } - if (settings.iframeName != newSettings.iframeName) { + if (settings!.iframeName != newSettings.iframeName) { iframe.name = newSettings.iframeName; } - if (settings.iframeCsp != newSettings.iframeCsp) { + if (settings!.iframeCsp != newSettings.iframeCsp) { iframe.csp = newSettings.iframeCsp; } - if (settings.iframeSandbox != newSettings.iframeSandbox) { + if (settings!.iframeSandbox != newSettings.iframeSandbox) { var sandbox = newSettings.iframeSandbox; if (sandbox != null && sandbox != Sandbox.ALLOW_ALL) { iframe.setAttribute( @@ -449,7 +484,7 @@ class InAppWebViewWebElement implements Disposable { } Future> getSettings() async { - return settings.toMap(); + return settings!.toMap(); } void onLoadStart(String url) async { @@ -569,7 +604,7 @@ class InAppWebViewWebElement implements Disposable { void dispose() { _channel?.setMethodCallHandler(null); _channel = null; - iframe.remove(); + iframeContainer.remove(); if (WebPlatformManager.webViews.containsKey(_viewId)) { WebPlatformManager.webViews.remove(_viewId); } diff --git a/lib/src/web/web_platform.dart b/lib/src/web/web_platform.dart index 0cbc9808..b8dae409 100644 --- a/lib/src/web/web_platform.dart +++ b/lib/src/web/web_platform.dart @@ -19,7 +19,7 @@ class FlutterInAppWebViewWebPlatform { var webView = InAppWebViewWebElement(viewId: viewId, messenger: registrar); WebPlatformManager.webViews.putIfAbsent(viewId, () => webView); - return webView.iframe; + return webView.iframeContainer; }); } diff --git a/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift index 7e60a173..e57ac3f8 100644 --- a/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift +++ b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift @@ -54,6 +54,20 @@ public class HeadlessInAppWebView : Disposable { return nil } + public func disposeAndGetFlutterWebView(withFrame frame: CGRect) -> FlutterWebViewController? { + let newFlutterWebView = flutterWebView + if let view = flutterWebView?.view() { + // restore WebView frame and alpha + view.frame = frame + view.alphaValue = 1.0 + // remove from parent + view.removeFromSuperview() + dispose() + } + return newFlutterWebView + } + + public func dispose() { channelDelegate?.dispose() channelDelegate = nil diff --git a/macos/Classes/InAppWebView/FlutterWebViewFactory.swift b/macos/Classes/InAppWebView/FlutterWebViewFactory.swift index 223a8c81..b37fb9a7 100755 --- a/macos/Classes/InAppWebView/FlutterWebViewFactory.swift +++ b/macos/Classes/InAppWebView/FlutterWebViewFactory.swift @@ -25,6 +25,13 @@ public class FlutterWebViewFactory: NSObject, FlutterPlatformViewFactory { public func create(withViewIdentifier viewId: Int64, arguments args: Any?) -> NSView { let arguments = args as? NSDictionary + + if let headlessWebViewId = arguments?["headlessWebViewId"] as? String, + let headlessWebView = HeadlessInAppWebViewManager.webViews[headlessWebViewId], + let platformView = headlessWebView?.disposeAndGetFlutterWebView(withFrame: .zero) { + return platformView.view() + } + let webviewController = FlutterWebViewController(registrar: registrar!, withFrame: .zero, viewIdentifier: viewId, diff --git a/macos/Classes/InAppWebView/InAppWebView.swift b/macos/Classes/InAppWebView/InAppWebView.swift index 162cb66b..7f015033 100755 --- a/macos/Classes/InAppWebView/InAppWebView.swift +++ b/macos/Classes/InAppWebView/InAppWebView.swift @@ -1073,9 +1073,11 @@ public class InAppWebView: WKWebView, WKUIDelegate, let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")" let permissionRequest = PermissionRequest(origin: origin, resources: [type.rawValue], frame: frame) + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.PermissionRequestCallback() callback.nonNullSuccess = { (response: PermissionResponse) in if let action = response.action { + decisionHandlerCalled = true switch action { case 1: decisionHandler(.grant) @@ -1091,7 +1093,10 @@ public class InAppWebView: WKWebView, WKUIDelegate, return true } callback.defaultBehaviour = { (response: PermissionResponse?) in - decisionHandler(.deny) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.deny) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1157,13 +1162,18 @@ public class InAppWebView: WKWebView, WKUIDelegate, return } + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in + decisionHandlerCalled = true decisionHandler(response) return false } callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in - decisionHandler(.allow) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1189,13 +1199,18 @@ public class InAppWebView: WKWebView, WKUIDelegate, let useOnNavigationResponse = settings?.useOnNavigationResponse if useOnNavigationResponse != nil, useOnNavigationResponse! { + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.NavigationResponseCallback() callback.nonNullSuccess = { (response: WKNavigationResponsePolicy) in + decisionHandlerCalled = true decisionHandler(response) return false } callback.defaultBehaviour = { (response: WKNavigationResponsePolicy?) in - decisionHandler(.allow) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1210,7 +1225,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, } if let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { - if #available(macOS 11.3, *), !navigationResponse.canShowMIMEType { + if #available(macOS 11.3, *), !navigationResponse.canShowMIMEType, useOnNavigationResponse == nil || !useOnNavigationResponse! { decisionHandler(.download) return } else { @@ -1310,6 +1325,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, return } + var completionHandlerCalled = false if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest || @@ -1323,6 +1339,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, let callback = WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() callback.nonNullSuccess = { (response: HttpAuthResponse) in if let action = response.action { + completionHandlerCalled = true switch action { case 0: InAppWebView.credentialsProposed = [] @@ -1371,7 +1388,10 @@ public class InAppWebView: WKWebView, WKUIDelegate, return true } callback.defaultBehaviour = { (response: HttpAuthResponse?) in - completionHandler(.performDefaultHandling, nil) + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1398,6 +1418,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in if let action = response.action { + completionHandlerCalled = true switch action { case 0: InAppWebView.credentialsProposed = [] @@ -1418,7 +1439,10 @@ public class InAppWebView: WKWebView, WKUIDelegate, return true } callback.defaultBehaviour = { (response: ServerTrustAuthResponse?) in - completionHandler(.performDefaultHandling, nil) + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1435,6 +1459,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback() callback.nonNullSuccess = { (response: ClientCertResponse) in if let action = response.action { + completionHandlerCalled = true switch action { case 0: completionHandler(.cancelAuthenticationChallenge, nil) @@ -1471,7 +1496,10 @@ public class InAppWebView: WKWebView, WKUIDelegate, return true } callback.defaultBehaviour = { (response: ClientCertResponse?) in - completionHandler(.performDefaultHandling, nil) + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) @@ -1573,9 +1601,12 @@ public class InAppWebView: WKWebView, WKUIDelegate, return } + var completionHandlerCalled = false + let callback = WebViewChannelDelegate.JsAlertCallback() callback.nonNullSuccess = { (response: JsAlertResponse) in if response.handledByClient { + completionHandlerCalled = true let action = response.action ?? 1 switch action { case 0: @@ -1589,14 +1620,20 @@ public class InAppWebView: WKWebView, WKUIDelegate, return true } callback.defaultBehaviour = { (response: JsAlertResponse?) in - let responseMessage = response?.message - let confirmButtonTitle = response?.confirmButtonTitle - self.createAlertDialog(message: message, responseMessage: responseMessage, - confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + self.createAlertDialog(message: message, responseMessage: responseMessage, + confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + } } callback.error = { (code: String, message: String?, details: Any?) in - print(code + ", " + (message ?? "")) - completionHandler() + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler() + } } if let channelDelegate = channelDelegate { @@ -1622,9 +1659,12 @@ public class InAppWebView: WKWebView, WKUIDelegate, public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + var completionHandlerCalled = false + let callback = WebViewChannelDelegate.JsConfirmCallback() callback.nonNullSuccess = { (response: JsConfirmResponse) in if response.handledByClient { + completionHandlerCalled = true let action = response.action ?? 1 switch action { case 0: @@ -1641,14 +1681,20 @@ public class InAppWebView: WKWebView, WKUIDelegate, return true } callback.defaultBehaviour = { (response: JsConfirmResponse?) in - let responseMessage = response?.message - let confirmButtonTitle = response?.confirmButtonTitle - let cancelButtonTitle = response?.cancelButtonTitle - self.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + self.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + } } callback.error = { (code: String, message: String?, details: Any?) in - print(code + ", " + (message ?? "")) - completionHandler(false) + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(false) + } } if let channelDelegate = channelDelegate { @@ -1678,9 +1724,13 @@ public class InAppWebView: WKWebView, WKUIDelegate, public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { + + var completionHandlerCalled = false + let callback = WebViewChannelDelegate.JsPromptCallback() callback.nonNullSuccess = { (response: JsPromptResponse) in if response.handledByClient { + completionHandlerCalled = true let action = response.action ?? 1 switch action { case 0: @@ -1697,16 +1747,22 @@ public class InAppWebView: WKWebView, WKUIDelegate, return true } callback.defaultBehaviour = { (response: JsPromptResponse?) in - let responseMessage = response?.message - let confirmButtonTitle = response?.confirmButtonTitle - let cancelButtonTitle = response?.cancelButtonTitle - let value = response?.value - self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, - cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + let value = response?.value + self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, + cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + } } callback.error = { (code: String, message: String?, details: Any?) in - print(code + ", " + (message ?? "")) - completionHandler(nil) + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(nil) + } } if let channelDelegate = channelDelegate { @@ -1768,13 +1824,18 @@ public class InAppWebView: WKWebView, WKUIDelegate, return } + var decisionHandlerCalled = false let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback() callback.nonNullSuccess = { (action: Bool) in + decisionHandlerCalled = true decisionHandler(action) return false } callback.defaultBehaviour = { (action: Bool?) in - decisionHandler(false) + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(false) + } } callback.error = { [weak callback] (code: String, message: String?, details: Any?) in print(code + ", " + (message ?? "")) diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift index 311a569a..59ac3dd3 100644 --- a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift +++ b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -834,6 +834,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return PermissionResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onPermissionRequest(request: PermissionRequest, callback: PermissionRequestCallback) { @@ -854,6 +858,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return WKNavigationActionPolicy.cancel } } + + deinit { + self.defaultBehaviour(nil) + } } public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, callback: ShouldOverrideUrlLoadingCallback) { @@ -905,6 +913,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return HttpAuthResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge, callback: ReceivedHttpAuthRequestCallback) { @@ -922,6 +934,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return ServerTrustAuthResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge, callback: ReceivedServerTrustAuthRequestCallback) { @@ -939,6 +955,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return ClientCertResponse.fromMap(map: obj as? [String:Any?]) } } + + deinit { + self.defaultBehaviour(nil) + } } public func onReceivedClientCertRequest(challenge: ClientCertChallenge, callback: ReceivedClientCertRequestCallback) { @@ -1013,6 +1033,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return WKNavigationResponsePolicy.cancel } } + + deinit { + self.defaultBehaviour(nil) + } } public func onNavigationResponse(navigationResponse: WKNavigationResponse, callback: NavigationResponseCallback) { @@ -1033,6 +1057,10 @@ public class WebViewChannelDelegate : ChannelDelegate { return false } } + + deinit { + self.defaultBehaviour(nil) + } } public func shouldAllowDeprecatedTLS(challenge: URLAuthenticationChallenge, callback: ShouldAllowDeprecatedTLSCallback) { diff --git a/pubspec.yaml b/pubspec.yaml index 467200f6..47f88698 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview description: A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. -version: 6.0.0-beta.3 +version: 6.0.0-beta.4 homepage: https://inappwebview.dev/ repository: https://github.com/pichillilorenzo/flutter_inappwebview issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues