LLorenzo Pichillifix #1924
e143e289创建于 2023年12月23日历史提交
//

//  InAppWebView.swift

//  flutter_inappwebview

//

//  Created by Lorenzo on 21/10/18.

//



import Flutter

import Foundation

import WebKit



public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate,

                            WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate,

                            WKDownloadDelegate,

                            PullToRefreshDelegate,

                            Disposable {

    static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_"



    var id: Any? // viewId

    var plugin: SwiftFlutterPlugin?

    var windowId: Int64?

    var windowCreated = false

    var windowBeforeCreatedCallbacks: [() -> ()] = []

    var inAppBrowserDelegate: InAppBrowserDelegate?

    var channelDelegate: WebViewChannelDelegate?

    var settings: InAppWebViewSettings?

    var pullToRefreshControl: PullToRefreshControl?

    var findInteractionController: FindInteractionController?

    var webMessageChannels: [String: WebMessageChannel] = [:]

    var webMessageListeners: [WebMessageListener] = []

    var currentOriginalUrl: URL?

    var inFullscreen = false

    var preventGestureDelay = false

    

    private static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate]

    private static var credentialsProposed: [URLCredential] = []

    

    var lastScrollX: CGFloat = 0

    var lastScrollY: CGFloat = 0

    

    // Used to manage pauseTimers() and resumeTimers()

    var isPausedTimers = false

    var isPausedTimersCompletionHandler: (() -> Void)?



    var contextMenu: [String: Any]?

    var initialUserScripts: [UserScript] = []

    

    // https://github.com/mozilla-mobile/firefox-ios/blob/50531a7e9e4d459fb11d4fcb7d4322e08103501f/Client/Frontend/Browser/ContextMenuHelper.swift

    fileprivate var nativeHighlightLongPressRecognizer: UILongPressGestureRecognizer?

    fileprivate var nativeLoupeGesture: UILongPressGestureRecognizer?

    var longPressRecognizer: UILongPressGestureRecognizer!

    var recognizerForDisablingContextMenuOnLinks: UILongPressGestureRecognizer!

    var lastLongPressTouchPoint: CGPoint?

    

    var panGestureRecognizer: UIPanGestureRecognizer!

    

    var lastTouchPoint: CGPoint?

    var lastTouchPointTimestamp = Int64(Date().timeIntervalSince1970 * 1000)

    

    var contextMenuIsShowing = false

    // flag used for the workaround to trigger onCreateContextMenu event as the same on Android

    var onCreateContextMenuEventTriggeredWhenMenuDisabled = false

    

    var customIMPs: [IMP] = []

    

    var callAsyncJavaScriptBelowIOS14Results: [String:((Any?) -> Void)] = [:]

    

    var oldZoomScale = Float(1.0)

    

    fileprivate var interceptOnlyAsyncAjaxRequestsPluginScript: PluginScript?

    

    init(id: Any?, plugin: SwiftFlutterPlugin?, frame: CGRect, configuration: WKWebViewConfiguration,

         contextMenu: [String: Any]?, userScripts: [UserScript] = []) {

        super.init(frame: frame, configuration: configuration)

        self.id = id

        self.plugin = plugin

        if let id = id, let registrar = plugin?.registrar {

            let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: id),

                                               binaryMessenger: registrar.messenger())

            self.channelDelegate = WebViewChannelDelegate(webView: self, channel: channel)

        }

        self.contextMenu = contextMenu

        self.initialUserScripts = userScripts

        uiDelegate = self

        navigationDelegate = self

        scrollView.delegate = self

        longPressRecognizer = UILongPressGestureRecognizer()

        longPressRecognizer.delegate = self

        longPressRecognizer.addTarget(self, action: #selector(longPressGestureDetected))

        recognizerForDisablingContextMenuOnLinks = UILongPressGestureRecognizer()

        recognizerForDisablingContextMenuOnLinks.delegate = self

        recognizerForDisablingContextMenuOnLinks.addTarget(self, action: #selector(longPressGestureDetected))

        recognizerForDisablingContextMenuOnLinks?.minimumPressDuration = 0.45

        panGestureRecognizer = UIPanGestureRecognizer()

        panGestureRecognizer.delegate = self

        panGestureRecognizer.addTarget(self, action: #selector(endDraggingDetected))

    }

    

    override public var frame: CGRect {

        get {

            return super.frame

        }

        set {

            super.frame = newValue

            

            self.scrollView.contentInset = .zero

            if #available(iOS 11, *) {

                // Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will

                // always be 0.

                if (scrollView.adjustedContentInset != UIEdgeInsets.zero) {

                    let insetToAdjust = self.scrollView.adjustedContentInset

                    scrollView.contentInset = UIEdgeInsets(top: -insetToAdjust.top, left: -insetToAdjust.left,

                                                                bottom: -insetToAdjust.bottom, right: -insetToAdjust.right)

                }

            }

        }

    }

    

    required public init(coder aDecoder: NSCoder) {

        super.init(coder: aDecoder)!

    }



    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

        return true

    }

    

    // BVC KVO events for all changes on the webview will call this.

    // It is called frequently during a page load (particularly on progress changes and URL changes).

    // As of iOS 12, WKContentView gesture setup is async, but it has been called by the time

    // the webview is ready to load an URL. After this has happened, we can override the gesture.

    func replaceGestureHandlerIfNeeded() {

        DispatchQueue.main.async {

            if self.gestureRecognizerWithDescriptionFragment("InAppWebView") == nil {

                self.replaceWebViewLongPress()

            }

        }

    }

    

    private func replaceWebViewLongPress() {

        // WebKit installs gesture handlers async. If `replaceWebViewLongPress` is called after a wkwebview in most cases a small delay is sufficient

        // See also https://bugs.webkit.org/show_bug.cgi?id=193366

        nativeHighlightLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_highlightLongPressRecognized:")

        nativeLoupeGesture = gestureRecognizerWithDescriptionFragment("action=loupeGesture:")



        if let nativeLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_longPressRecognized:") {

            nativeLongPressRecognizer.removeTarget(nil, action: nil)

            nativeLongPressRecognizer.addTarget(self, action: #selector(self.longPressGestureDetected))

        }

    }

    

    private func gestureRecognizerWithDescriptionFragment(_ descriptionFragment: String) -> UILongPressGestureRecognizer? {

        let result = self.scrollView.subviews.compactMap({ $0.gestureRecognizers }).joined().first(where: {

            return (($0 as? UILongPressGestureRecognizer) != nil) && $0.description.contains(descriptionFragment)

        })

        return result as? UILongPressGestureRecognizer

    }

    

    @objc func longPressGestureDetected(_ sender: UIGestureRecognizer) {

        if sender.state == .cancelled {

            return

        }



        guard sender.state == .began else {

            return

        }

        

        if sender == recognizerForDisablingContextMenuOnLinks,

           let settings = settings, !settings.disableLongPressContextMenuOnLinks {

            return

        }

        

        if sender == longPressRecognizer {

            // To prevent the tapped link from proceeding with navigation, "cancel" the native WKWebView

            // `_highlightLongPressRecognizer`. This preserves the original behavior as seen here:

            // https://github.com/WebKit/webkit/blob/d591647baf54b4b300ca5501c21a68455429e182/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L1600-L1614

            if let nativeHighlightLongPressRecognizer = nativeHighlightLongPressRecognizer,

                nativeHighlightLongPressRecognizer.isEnabled {

                nativeHighlightLongPressRecognizer.isEnabled = false

                nativeHighlightLongPressRecognizer.isEnabled = true

            }

        }



        //Finding actual touch location in webView

        var touchLocation = sender.location(in: self)

        touchLocation.x -= scrollView.contentInset.left

        touchLocation.y -= scrollView.contentInset.top

        touchLocation.x /= scrollView.zoomScale

        touchLocation.y /= scrollView.zoomScale



        lastLongPressTouchPoint = touchLocation



        evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(touchLocation.x),\(touchLocation.y))", completionHandler: {(value, error) in

            if error != nil {

                print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")")

            } else if let value = value as? [String: Any?] {

                let hitTestResult = HitTestResult.fromMap(map: value)!

                self.nativeLoupeGesture = self.gestureRecognizerWithDescriptionFragment("action=loupeGesture:")

                

                if sender == self.recognizerForDisablingContextMenuOnLinks,

                   hitTestResult.type.rawValue > HitTestResultType.unknownType.rawValue,

                   hitTestResult.type.rawValue < HitTestResultType.editTextType.rawValue {

                    self.nativeLoupeGesture?.isEnabled = false

                    self.nativeLoupeGesture?.isEnabled = true

                } else {

                    self.channelDelegate?.onLongPressHitTestResult(hitTestResult: hitTestResult)

                }

            } else if sender == self.longPressRecognizer {

                self.channelDelegate?.onLongPressHitTestResult(hitTestResult: HitTestResult(type: .unknownType, extra: nil))

            }

        })

    }

    

    public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        lastTouchPoint = point

        lastTouchPointTimestamp = Int64(Date().timeIntervalSince1970 * 1000)

        SharedLastTouchPointTimestamp[self] = lastTouchPointTimestamp

        

        // re-build context menu items for the current webview

        UIMenuController.shared.menuItems = []

        if let menu = self.contextMenu {

            if let menuItems = menu["menuItems"] as? [[String : Any]] {

                for menuItem in menuItems {

                    let id = menuItem["id"]!

                    let title = menuItem["title"] as! String

                    let targetMethodName = "onContextMenuActionItemClicked-" + String(self.hash) + "-" +

                                            (id is Int64 ? String(id as! Int64) : id as! String)

                    if !self.responds(to: Selector(targetMethodName)) {

                        let customAction: () -> Void = {

                            self.channelDelegate?.onContextMenuActionItemClicked(id: id, title: title)

                            if #available(iOS 16.0, *) {

                                if #unavailable(iOS 16.4) {

                                    self.onHideContextMenu()

                                }

                            }

                        }

                        let castedCustomAction: AnyObject = unsafeBitCast(customAction as @convention(block) () -> Void, to: AnyObject.self)

                        let swizzledImplementation = imp_implementationWithBlock(castedCustomAction)

                        class_addMethod(InAppWebView.self, Selector(targetMethodName), swizzledImplementation, nil)

                        self.customIMPs.append(swizzledImplementation)

                    }

                    let item = UIMenuItem(title: title, action: Selector(targetMethodName))

                    UIMenuController.shared.menuItems!.append(item)

                }

            }

        }

        

        // https://github.com/pichillilorenzo/flutter_inappwebview/pull/1665

        if preventGestureDelay, let gestures = superview?.superview?.gestureRecognizers {

            for gesture in gestures {

                if NSStringFromClass(type(of: gesture)) == "DelayingGestureRecognizer" {

                    gesture.isEnabled = false

                }

            }

        }

        

        return super.hitTest(point, with: event)

    }

    

    @available(iOS 13.0, *)

    public override func buildMenu(with builder: UIMenuBuilder) {

        if #available(iOS 16.0, *) {

            if let menu = contextMenu {

                let contextMenuSettings = ContextMenuSettings()

                if let contextMenuSettingsMap = menu["settings"] as? [String: Any?] {

                    let _ = contextMenuSettings.parse(settings: contextMenuSettingsMap)

                    if contextMenuSettings.hideDefaultSystemContextMenuItems {

                        builder.remove(menu: .lookup)

                    }

                }

            }

            

            if #unavailable(iOS 16.4), settings?.disableContextMenu == false {

                contextMenuIsShowing = false

                DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {

                    self.onCreateContextMenu()

                }

            }

        }

        super.buildMenu(with: builder)

    }

    

    @available(iOS 16.4, *)

    public func webView(_ webView: WKWebView, willPresentEditMenuWithAnimator animator: UIEditMenuInteractionAnimating) {

        onCreateContextMenu()

    }

    

    @available(iOS 16.4, *)

    public func webView(_ webView: WKWebView, willDismissEditMenuWithAnimator animator: UIEditMenuInteractionAnimating) {

        onHideContextMenu()

    }

    

    public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {

        var needCheck = sender is UIMenuController

        if #available(iOS 13.0, *) {

            needCheck = sender is UIMenuElement || sender is UIMenuController

        }

        

        if needCheck {

            if settings?.disableContextMenu == true {

                if !onCreateContextMenuEventTriggeredWhenMenuDisabled {

                    // workaround to trigger onCreateContextMenu event as the same on Android

                    onCreateContextMenu()

                    onCreateContextMenuEventTriggeredWhenMenuDisabled = true

                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {

                        self.onCreateContextMenuEventTriggeredWhenMenuDisabled = false

                    }

                }

                return false

            }

            

            if let menu = contextMenu {

                let contextMenuSettings = ContextMenuSettings()

                if let contextMenuSettingsMap = menu["settings"] as? [String: Any?] {

                    let _ = contextMenuSettings.parse(settings: contextMenuSettingsMap)

                    if !action.description.starts(with: "onContextMenuActionItemClicked-") && contextMenuSettings.hideDefaultSystemContextMenuItems {

                        return false

                    }

                }

            }

            

            if contextMenuIsShowing, !action.description.starts(with: "onContextMenuActionItemClicked-") {

                let id = action.description.compactMap({ $0.asciiValue?.description }).joined()

                channelDelegate?.onContextMenuActionItemClicked(id: id, title: action.description)

                if #available(iOS 16.0, *) {

                    if #unavailable(iOS 16.4) {

                        onHideContextMenu()

                    }

                }

            }

        }

        return super.canPerformAction(action, withSender: sender)

    }

    

    // For some reasons, using the scrollViewDidEndDragging event, in some rare cases, could block

    // the scroll gesture

    @objc func endDraggingDetected() {

        // detect end dragging

        if panGestureRecognizer.state == .ended {

            // fix for pull-to-refresh jittering when the touch drag event is held

            if let pullToRefreshControl = pullToRefreshControl,

               pullToRefreshControl.shouldCallOnRefresh {

                pullToRefreshControl.onRefresh()

            }

        }

    }



    public func prepare() {

        scrollView.addGestureRecognizer(self.longPressRecognizer)

        scrollView.addGestureRecognizer(self.recognizerForDisablingContextMenuOnLinks)

        scrollView.addGestureRecognizer(self.panGestureRecognizer)

        scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [.new, .old], context: nil)

        scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.zoomScale), options: [.new, .old], context: nil)

        scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: [.new, .old], context: nil)

        

        addObserver(self,

                    forKeyPath: #keyPath(WKWebView.estimatedProgress),

                    options: .new,

                    context: nil)

        

        addObserver(self,

                    forKeyPath: #keyPath(WKWebView.url),

                    options: [.new, .old],

                    context: nil)

        

        addObserver(self,

            forKeyPath: #keyPath(WKWebView.title),

            options: [.new, .old],

            context: nil)

        

        if #available(iOS 15.0, *) {

            addObserver(self,

                forKeyPath: #keyPath(WKWebView.cameraCaptureState),

                options: [.new, .old],

                context: nil)

            

            addObserver(self,

                forKeyPath: #keyPath(WKWebView.microphoneCaptureState),

                options: [.new, .old],

                context: nil)

        }

        

        if #unavailable(iOS 16.0) {

            NotificationCenter.default.addObserver(

                self,

                selector: #selector(onCreateContextMenu),

                name: UIMenuController.willShowMenuNotification,

                object: nil)

            

            NotificationCenter.default.addObserver(

                self,

                selector: #selector(onHideContextMenu),

                name: UIMenuController.didHideMenuNotification,

                object: nil)

        }

        

        // TODO: Still not working on iOS 16.0!

//        if #available(iOS 16.0, *) {

//            addObserver(self,

//                        forKeyPath: #keyPath(WKWebView.fullscreenState),

//                        options: .new,

//                context: nil)

//        } else {

            // listen for videos playing in fullscreen

            NotificationCenter.default.addObserver(self,

                                                   selector: #selector(onEnterFullscreen(_:)),

                                                   name: UIWindow.didBecomeVisibleNotification,

                                                   object: window)



            // listen for videos stopping to play in fullscreen

            NotificationCenter.default.addObserver(self,

                                                   selector: #selector(onExitFullscreen(_:)),

                                                   name: UIWindow.didBecomeHiddenNotification,

                                                   object: window)

//        }

        

        if let settings = settings {

            if settings.transparentBackground {

                isOpaque = false

                backgroundColor = UIColor.clear

                scrollView.backgroundColor = UIColor.clear

            }

            

            // prevent webView from bouncing

            if settings.disallowOverScroll {

                if responds(to: #selector(getter: scrollView)) {

                    scrollView.bounces = false

                }

                else {

                    for subview: UIView in subviews {

                        if subview is UIScrollView {

                            (subview as! UIScrollView).bounces = false

                        }

                    }

                }

            }

            

            if #available(iOS 11.0, *) {

                accessibilityIgnoresInvertColors = settings.accessibilityIgnoresInvertColors

                scrollView.contentInsetAdjustmentBehavior =

                    UIScrollView.ContentInsetAdjustmentBehavior.init(rawValue: settings.contentInsetAdjustmentBehavior)!

            }

            

            allowsBackForwardNavigationGestures = settings.allowsBackForwardNavigationGestures

            if #available(iOS 9.0, *) {

                allowsLinkPreview = settings.allowsLinkPreview

                if !settings.userAgent.isEmpty {

                    customUserAgent = settings.userAgent

                }

            }

            

            if #available(iOS 13.0, *) {

                scrollView.automaticallyAdjustsScrollIndicatorInsets = settings.automaticallyAdjustsScrollIndicatorInsets

            }

            

            scrollView.showsVerticalScrollIndicator = !settings.disableVerticalScroll

            scrollView.showsHorizontalScrollIndicator = !settings.disableHorizontalScroll

            scrollView.showsVerticalScrollIndicator = settings.verticalScrollBarEnabled

            scrollView.showsHorizontalScrollIndicator = settings.horizontalScrollBarEnabled

            scrollView.isScrollEnabled = !(settings.disableVerticalScroll && settings.disableHorizontalScroll)

            scrollView.isDirectionalLockEnabled = settings.isDirectionalLockEnabled



            scrollView.decelerationRate = Util.getDecelerationRate(type: settings.decelerationRate)

            scrollView.alwaysBounceVertical = settings.alwaysBounceVertical

            scrollView.alwaysBounceHorizontal = settings.alwaysBounceHorizontal

            scrollView.scrollsToTop = settings.scrollsToTop

            scrollView.isPagingEnabled = settings.isPagingEnabled

            scrollView.maximumZoomScale = CGFloat(settings.maximumZoomScale)

            scrollView.minimumZoomScale = CGFloat(settings.minimumZoomScale)

            

            if #available(iOS 14.0, *) {

                mediaType = settings.mediaType

                pageZoom = CGFloat(settings.pageZoom)

            }

            

            if #available(iOS 15.0, *) {

                if let underPageBackgroundColor = settings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty {

                    self.underPageBackgroundColor = UIColor(hexString: underPageBackgroundColor)

                }

            }

            

            if #available(iOS 15.5, *) {

                if let minViewportInset = settings.minimumViewportInset, let maxViewportInset = settings.maximumViewportInset {

                    setMinimumViewportInset(minViewportInset, maximumViewportInset: maxViewportInset)

                }

            }

            

            if #available(iOS 16.0, *) {

                isFindInteractionEnabled = settings.isFindInteractionEnabled

            }

            

            if #available(iOS 16.4, *) {

                isInspectable = settings.isInspectable

            }

            

            if settings.clearCache {

                clearCache()

            }

        }

        

        prepareAndAddUserScripts()

        

        if windowId != nil {

            // The new created window webview has the same WKWebViewConfiguration variable reference.

            // So, we cannot set another WKWebViewConfiguration for it unfortunately!

            // This is a limitation of the official WebKit API.

            return

        }

        

        configuration.preferences = WKPreferences()

        if let settings = settings {

            if #available(iOS 9.0, *) {

                configuration.allowsAirPlayForMediaPlayback = settings.allowsAirPlayForMediaPlayback

                configuration.allowsPictureInPictureMediaPlayback = settings.allowsPictureInPictureMediaPlayback

            }

            

            configuration.preferences.javaScriptCanOpenWindowsAutomatically = settings.javaScriptCanOpenWindowsAutomatically

            configuration.preferences.minimumFontSize = CGFloat(settings.minimumFontSize)

            

            if #available(iOS 13.0, *) {

                configuration.preferences.isFraudulentWebsiteWarningEnabled = settings.isFraudulentWebsiteWarningEnabled

                configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: settings.preferredContentMode)!

            }

            

            configuration.preferences.javaScriptEnabled = settings.javaScriptEnabled

            if #available(iOS 14.0, *) {

                configuration.defaultWebpagePreferences.allowsContentJavaScript = settings.javaScriptEnabled

            }

            

            if #available(iOS 15.0, *) {

                configuration.preferences.isTextInteractionEnabled = settings.isTextInteractionEnabled

            }

            if #available(iOS 15.4, *) {

                configuration.preferences.isSiteSpecificQuirksModeEnabled = settings.isSiteSpecificQuirksModeEnabled

                configuration.preferences.isElementFullscreenEnabled = settings.isElementFullscreenEnabled

            }

            if #available(iOS 16.4, *) {

                configuration.preferences.shouldPrintBackgrounds = settings.shouldPrintBackgrounds

            }

        }

    }

    

    public func prepareAndAddUserScripts() -> Void {

        if windowId != nil {

            // The new created window webview has the same WKWebViewConfiguration variable reference.

            // So, we cannot set another WKWebViewConfiguration for it unfortunately!

            // This is a limitation of the official WebKit API.

            return

        }

        configuration.userContentController = WKUserContentController()

        configuration.userContentController.initialize()

        

        if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {

            return

        }

        

        configuration.userContentController.addPluginScript(PROMISE_POLYFILL_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(CONSOLE_LOG_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(PRINT_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT)

        configuration.userContentController.addPluginScript(ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT)

        if let settings = settings {

            interceptOnlyAsyncAjaxRequestsPluginScript = createInterceptOnlyAsyncAjaxRequestsPluginScript(onlyAsync: settings.interceptOnlyAsyncAjaxRequests)

            if settings.useShouldInterceptAjaxRequest {

                if let interceptOnlyAsyncAjaxRequestsPluginScript = interceptOnlyAsyncAjaxRequestsPluginScript {

                    configuration.userContentController.addPluginScript(interceptOnlyAsyncAjaxRequestsPluginScript)

                }

                configuration.userContentController.addPluginScript(INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT)

            }

            if settings.useShouldInterceptFetchRequest {

                configuration.userContentController.addPluginScript(INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT)

            }

            if settings.useOnLoadResource {

                configuration.userContentController.addPluginScript(ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT)

            }

            if !settings.supportZoom {

                configuration.userContentController.addPluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT)

            } else if settings.enableViewportScale {

                configuration.userContentController.addPluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT)

            }

        }

        configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received")

        configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received")

        configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived")

        configuration.userContentController.add(self, name: "onWebMessagePortMessageReceived")

        configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived")

        configuration.userContentController.add(self, name: "onWebMessageListenerPostMessageReceived")

        configuration.userContentController.addUserOnlyScripts(initialUserScripts)

        configuration.userContentController.sync(scriptMessageHandler: self)

    }

    

    public static func preWKWebViewConfiguration(settings: InAppWebViewSettings?) -> WKWebViewConfiguration {

        let configuration = WKWebViewConfiguration()

        

        configuration.processPool = WKProcessPoolManager.sharedProcessPool

        

        if let settings = settings {

            configuration.allowsInlineMediaPlayback = settings.allowsInlineMediaPlayback

            configuration.suppressesIncrementalRendering = settings.suppressesIncrementalRendering

            configuration.selectionGranularity = WKSelectionGranularity.init(rawValue: settings.selectionGranularity)!

            

            if settings.allowUniversalAccessFromFileURLs {

                configuration.setValue(settings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs")

            }

            

            if settings.allowFileAccessFromFileURLs {

                configuration.preferences.setValue(settings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs")

            }

            

            if #available(iOS 9.0, *) {

                if settings.incognito {

                    configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()

                } else if settings.cacheEnabled {

                    configuration.websiteDataStore = WKWebsiteDataStore.default()

                }

                if !settings.applicationNameForUserAgent.isEmpty {

                    if let applicationNameForUserAgent = configuration.applicationNameForUserAgent {

                        configuration.applicationNameForUserAgent = applicationNameForUserAgent + " " + settings.applicationNameForUserAgent

                    }

                }

            }

            

            if #available(iOS 10.0, *) {

                configuration.ignoresViewportScaleLimits = settings.ignoresViewportScaleLimits

                

                var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0)

                for type in settings.dataDetectorTypes {

                    let dataDetectorType = Util.getDataDetectorType(type: type)

                    dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue)

                }

                configuration.dataDetectorTypes = dataDetectorTypes

                

                configuration.mediaTypesRequiringUserActionForPlayback = settings.mediaPlaybackRequiresUserGesture ? .all : []

            } else {

                // Fallback on earlier versions

                configuration.mediaPlaybackRequiresUserAction = settings.mediaPlaybackRequiresUserGesture

            }

            

            if #available(iOS 11.0, *) {

                for scheme in settings.resourceCustomSchemes {

                    configuration.setURLSchemeHandler(CustomSchemeHandler(), forURLScheme: scheme)

                }

                if settings.sharedCookiesEnabled {

                    // More info to sending cookies with WKWebView

                    // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303

                    // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies

                    // See also https://forums.developer.apple.com/thread/97194

                    // check if websiteDataStore has not been initialized before

                    if(!settings.incognito && !settings.cacheEnabled) {

                        configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()

                    }

                    for cookie in HTTPCookieStorage.shared.cookies ?? [] {

                        configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil)

                    }

                }

            }

            

            if #available(iOS 14.0, *) {

                configuration.limitsNavigationsToAppBoundDomains = settings.limitsNavigationsToAppBoundDomains

            }

            

            if #available(iOS 15.0, *) {

                configuration.upgradeKnownHostsToHTTPS = settings.upgradeKnownHostsToHTTPS

            }

        }

        

        return configuration

    }

    

    @objc func onCreateContextMenu() {

        let mapSorted = SharedLastTouchPointTimestamp.sorted { $0.value > $1.value }

        if (mapSorted.first?.key != self) {

            return

        }

        

        contextMenuIsShowing = true

        

        let hitTestResult = HitTestResult(type: .unknownType, extra: nil)

        

        if let lastLongPressTouhLocation = lastLongPressTouchPoint {

            if configuration.preferences.javaScriptEnabled {

                self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastLongPressTouhLocation.x),\(lastLongPressTouhLocation.y))", completionHandler: {(value, error) in

                    if error != nil {

                        print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")")

                    } else if let value = value as? [String: Any?] {

                        self.channelDelegate?.onCreateContextMenu(hitTestResult: HitTestResult.fromMap(map: value) ?? hitTestResult)

                    } else {

                        self.channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult)

                    }

                })

            } else {

                channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult)

            }

        } else {

            channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult)

        }

    }

    

    @objc func onHideContextMenu() {

        if contextMenuIsShowing == false {

            return

        }

        contextMenuIsShowing = false

        channelDelegate?.onHideContextMenu()

    }

    

    override public func observeValue(forKeyPath keyPath: String?, of object: Any?,

                               change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

        if keyPath == #keyPath(WKWebView.estimatedProgress) {

            initializeWindowIdJS()

            let progress = Int(estimatedProgress * 100)

            channelDelegate?.onProgressChanged(progress: progress)

            inAppBrowserDelegate?.didChangeProgress(progress: estimatedProgress)

        } else if keyPath == #keyPath(WKWebView.url) && change?[.newKey] is URL {

            initializeWindowIdJS()

            let newUrl = change?[NSKeyValueChangeKey.newKey] as? URL

            channelDelegate?.onUpdateVisitedHistory(url: newUrl?.absoluteString, isReload: nil)

            inAppBrowserDelegate?.didUpdateVisitedHistory(url: newUrl)

        } else if keyPath == #keyPath(WKWebView.title) && change?[.newKey] is String {

            let newTitle = change?[.newKey] as? String

            channelDelegate?.onTitleChanged(title: newTitle)

            inAppBrowserDelegate?.didChangeTitle(title: newTitle)

        } else if keyPath == #keyPath(UIScrollView.contentOffset) {

            let newContentOffset = change?[.newKey] as? CGPoint

            let oldContentOffset = change?[.oldKey] as? CGPoint

            let startedByUser = scrollView.isDragging || scrollView.isDecelerating

            if newContentOffset != oldContentOffset {

                DispatchQueue.main.async {

                    self.onScrollChanged(startedByUser: startedByUser, oldContentOffset: oldContentOffset)

                }

            }

        } else if keyPath == #keyPath(UIScrollView.contentSize) {

            if let newContentSize = change?[.newKey] as? CGSize,

               let oldContentSize = change?[.oldKey] as? CGSize,

               newContentSize != oldContentSize {

                DispatchQueue.main.async {

                    self.onContentSizeChanged(oldContentSize: oldContentSize)

                }

            }

        }

        else if #available(iOS 15.0, *) {

            if keyPath == #keyPath(WKWebView.cameraCaptureState) || keyPath == #keyPath(WKWebView.microphoneCaptureState) {

                var oldState: WKMediaCaptureState? = nil

                if let oldValue = change?[.oldKey] as? Int {

                    oldState = WKMediaCaptureState.init(rawValue: oldValue)

                }

                var newState: WKMediaCaptureState? = nil

                if let newValue = change?[.newKey] as? Int {

                    newState = WKMediaCaptureState.init(rawValue: newValue)

                }

                if oldState != newState {

                    if keyPath == #keyPath(WKWebView.cameraCaptureState) {

                        channelDelegate?.onCameraCaptureStateChanged(oldState: oldState, newState: newState)

                    } else {

                        channelDelegate?.onMicrophoneCaptureStateChanged(oldState: oldState, newState: newState)

                    }

                }

            }

        } else if #available(iOS 16.0, *) {

            // TODO: Still not working on iOS 16.0!

//            if keyPath == #keyPath(WKWebView.fullscreenState) {

//                if fullscreenState == .enteringFullscreen {

//                    channelDelegate?.onEnterFullscreen()

//                } else if fullscreenState == .exitingFullscreen {

//                    channelDelegate?.onExitFullscreen()

//                }

//            }

        }

        replaceGestureHandlerIfNeeded()

    }

    

    public func initializeWindowIdJS() {

        if let windowId = windowId {

            if #available(iOS 14.0, *) {

                let contentWorlds = configuration.userContentController.getContentWorlds(with: windowId)

                for contentWorld in contentWorlds {

                    let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId))

                    evaluateJavascript(source: source, contentWorld: contentWorld)

                }

            } else {

                let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId))

                evaluateJavascript(source: source)

            }

        }

    }

    

    public func goBackOrForward(steps: Int) {

        if canGoBackOrForward(steps: steps) {

            if (steps > 0) {

                let index = steps - 1

                go(to: self.backForwardList.forwardList[index])

            }

            else if (steps < 0){

                let backListLength = self.backForwardList.backList.count

                let index = backListLength + steps

                go(to: self.backForwardList.backList[index])

            }

        }

    }

    

    public func canGoBackOrForward(steps: Int) -> Bool {

        let currentIndex = self.backForwardList.backList.count

        return (steps >= 0)

            ? steps <= self.backForwardList.forwardList.count

            : currentIndex + steps >= 0

    }

    

    @available(iOS 11.0, *)

    public func takeScreenshot (with: [String: Any?]?, completionHandler: @escaping (_ screenshot: Data?) -> Void) {

        var snapshotConfiguration: WKSnapshotConfiguration? = nil

        if let with = with {

            snapshotConfiguration = WKSnapshotConfiguration()

            if let rect = with["rect"] as? [String: Double] {

                snapshotConfiguration!.rect = CGRect.fromMap(map: rect)

            }

            if let snapshotWidth = with["snapshotWidth"] as? Double {

                snapshotConfiguration!.snapshotWidth = NSNumber(value: snapshotWidth)

            }

            if #available(iOS 13.0, *), let afterScreenUpdates = with["afterScreenUpdates"] as? Bool {

                snapshotConfiguration!.afterScreenUpdates = afterScreenUpdates

            }

        }

        takeSnapshot(with: snapshotConfiguration, completionHandler: {(image, error) -> Void in

            var imageData: Data? = nil

            if let screenshot = image {

                if let with = with {

                    switch with["compressFormat"] as! String {

                    case "JPEG":

                        let quality = Float(with["quality"] as! Int) / 100

                        imageData = screenshot.jpegData(compressionQuality: CGFloat(quality))

                        break

                    case "PNG":

                        imageData = screenshot.pngData()

                        break

                    default:

                        imageData = screenshot.pngData()

                    }

                }

                else {

                    imageData = screenshot.pngData()

                }

            }

            completionHandler(imageData)

        })

    }

    

    @available(iOS 14.0, *)

    public func createPdf (configuration: [String: Any?]?, completionHandler: @escaping (_ pdf: Data?) -> Void) {

        let pdfConfiguration: WKPDFConfiguration = .init()

        if let configuration = configuration {

            if let rect = configuration["rect"] as? [String: Double] {

                pdfConfiguration.rect = CGRect.fromMap(map: rect)

            }

        }

        createPDF(configuration: pdfConfiguration) { (result) in

            switch (result) {

            case .success(let data):

                completionHandler(data)

                return

            case .failure(let error):

                print(error.localizedDescription)

                completionHandler(nil)

                return

            }

        }

    }

    

    @available(iOS 14.0, *)

    public func createWebArchiveData (dataCompletionHandler: @escaping (_ webArchiveData: Data?) -> Void) {

        createWebArchiveData(completionHandler: { (result) in

            switch (result) {

            case .success(let data):

                dataCompletionHandler(data)

                return

            case .failure(let error):

                print(error.localizedDescription)

                dataCompletionHandler(nil)

                return

            }

        })

    }

    

    @available(iOS 14.0, *)

    public func saveWebArchive (filePath: String, autoname: Bool, completionHandler: @escaping (_ path: String?) -> Void) {

        createWebArchiveData(dataCompletionHandler: { (webArchiveData) in

            if let webArchiveData = webArchiveData {

                var localUrl = URL(fileURLWithPath: filePath)

                if autoname {

                    if let url = self.url {

                        // tries to mimic Android saveWebArchive method

                        let invalidCharacters = CharacterSet(charactersIn: "\\/:*?\"<>|")

                                    .union(.newlines)

                                    .union(.illegalCharacters)

                                    .union(.controlCharacters)

                                

                        let currentPageUrlFileName = url.path

                            .components(separatedBy: invalidCharacters)

                            .joined(separator: "")

                        

                        let fullPath = filePath + "/" + currentPageUrlFileName + ".webarchive"

                        localUrl = URL(fileURLWithPath: fullPath)

                    } else {

                        completionHandler(nil)

                        return

                    }

                }

                do {

                    try webArchiveData.write(to: localUrl)

                    completionHandler(localUrl.path)

                } catch {

                    // Catch any errors

                    print(error.localizedDescription)

                    completionHandler(nil)

                }

            } else {

                completionHandler(nil)

            }

        })

    }

    

    public func loadUrl(urlRequest: URLRequest, allowingReadAccessTo: URL?) {

        let url = urlRequest.url!

        

        if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, url.scheme == "file", allowingReadAccessTo.scheme == "file" {

            loadFileURL(url, allowingReadAccessTo: allowingReadAccessTo)

        } else {

            load(urlRequest)

        }

    }

    

    public func postUrl(url: URL, postData: Data) {

        var request = URLRequest(url: url)

        

        request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

        request.httpMethod = "POST"

        request.httpBody = postData

        load(request)

    }

    

    public func loadData(data: String, mimeType: String, encoding: String, baseUrl: URL, allowingReadAccessTo: URL?) {

        if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, baseUrl.scheme == "file", allowingReadAccessTo.scheme == "file" {

            loadFileURL(baseUrl, allowingReadAccessTo: allowingReadAccessTo)

        }

        

        if #available(iOS 9.0, *) {

            load(data.data(using: .utf8)!, mimeType: mimeType, characterEncodingName: encoding, baseURL: baseUrl)

        } else {

            loadHTMLString(data, baseURL: baseUrl)

        }

    }

    

    public func loadFile(assetFilePath: String) throws {

        if let plugin = plugin {

            let assetURL = try Util.getUrlAsset(plugin: plugin, assetFilePath: assetFilePath)

            let urlRequest = URLRequest(url: assetURL)

            loadUrl(urlRequest: urlRequest, allowingReadAccessTo: nil)

        }

    }

    

    func setSettings(newSettings: InAppWebViewSettings, newSettingsMap: [String: Any]) {

        

        // MUST be the first! In this way, all the settings that uses evaluateJavaScript can be applied/blocked!

        if #available(iOS 13.0, *) {

            if newSettingsMap["applePayAPIEnabled"] != nil && settings?.applePayAPIEnabled != newSettings.applePayAPIEnabled {

                if let settings = settings {

                    settings.applePayAPIEnabled = newSettings.applePayAPIEnabled

                }

                if !newSettings.applePayAPIEnabled {

                    // re-add WKUserScripts for the next page load

                    prepareAndAddUserScripts()

                } else {

                    configuration.userContentController.removeAllUserScripts()

                }

            }

        }

        

        if newSettingsMap["transparentBackground"] != nil && settings?.transparentBackground != newSettings.transparentBackground {

            if newSettings.transparentBackground {

                isOpaque = false

                backgroundColor = UIColor.clear

                scrollView.backgroundColor = UIColor.clear

            } else {

                isOpaque = true

                backgroundColor = nil

                scrollView.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1)

            }

        }

        

        if newSettingsMap["disallowOverScroll"] != nil && settings?.disallowOverScroll != newSettings.disallowOverScroll {

            if responds(to: #selector(getter: scrollView)) {

                scrollView.bounces = !newSettings.disallowOverScroll

            }

            else {

                for subview: UIView in subviews {

                    if subview is UIScrollView {

                        (subview as! UIScrollView).bounces = !newSettings.disallowOverScroll

                    }

                }

            }

        }

        

        if #available(iOS 9.0, *) {

            if (newSettingsMap["incognito"] != nil && settings?.incognito != newSettings.incognito && newSettings.incognito) {

                configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()

            } else if (newSettingsMap["cacheEnabled"] != nil && settings?.cacheEnabled != newSettings.cacheEnabled && newSettings.cacheEnabled) {

                configuration.websiteDataStore = WKWebsiteDataStore.default()

            }

        }

        

        if #available(iOS 11.0, *) {

            if (newSettingsMap["sharedCookiesEnabled"] != nil && settings?.sharedCookiesEnabled != newSettings.sharedCookiesEnabled && newSettings.sharedCookiesEnabled) {

                if(!newSettings.incognito && !newSettings.cacheEnabled) {

                    configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()

                }

                for cookie in HTTPCookieStorage.shared.cookies ?? [] {

                    configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil)

                }

            }

            if newSettingsMap["accessibilityIgnoresInvertColors"] != nil && settings?.accessibilityIgnoresInvertColors != newSettings.accessibilityIgnoresInvertColors {

                accessibilityIgnoresInvertColors = newSettings.accessibilityIgnoresInvertColors

            }

            if newSettingsMap["contentInsetAdjustmentBehavior"] != nil && settings?.contentInsetAdjustmentBehavior != newSettings.contentInsetAdjustmentBehavior {

                scrollView.contentInsetAdjustmentBehavior =

                    UIScrollView.ContentInsetAdjustmentBehavior.init(rawValue: newSettings.contentInsetAdjustmentBehavior)!

            }

        }

        

        if newSettingsMap["enableViewportScale"] != nil && settings?.enableViewportScale != newSettings.enableViewportScale {

            if !newSettings.enableViewportScale {

                if configuration.userContentController.userScripts.contains(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) {

                    configuration.userContentController.removePluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT)

                    evaluateJavaScript(NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE)

                }

            } else {

                evaluateJavaScript(ENABLE_VIEWPORT_SCALE_JS_SOURCE)

                configuration.userContentController.addUserScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT)

            }

        }

        

        if newSettingsMap["supportZoom"] != nil && settings?.supportZoom != newSettings.supportZoom {

            if newSettings.supportZoom {

                if configuration.userContentController.userScripts.contains(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) {

                    configuration.userContentController.removePluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT)

                    evaluateJavaScript(SUPPORT_ZOOM_JS_SOURCE)

                }

            } else {

                evaluateJavaScript(NOT_SUPPORT_ZOOM_JS_SOURCE)

                configuration.userContentController.addUserScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT)

            }

        }

        

        if newSettingsMap["useOnLoadResource"] != nil && settings?.useOnLoadResource != newSettings.useOnLoadResource {

            if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled {

                enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE,

                                            enable: newSettings.useOnLoadResource,

                                            pluginScript: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT)

            } else {

                newSettings.useOnLoadResource = false

            }

        }

        

        if newSettingsMap["useShouldInterceptAjaxRequest"] != nil && settings?.useShouldInterceptAjaxRequest != newSettings.useShouldInterceptAjaxRequest {

            if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled {

                enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE,

                                            enable: newSettings.useShouldInterceptAjaxRequest,

                                            pluginScript: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT)

            } else {

                newSettings.useShouldInterceptAjaxRequest = false

            }

        }

        

        if newSettingsMap["interceptOnlyAsyncAjaxRequests"] != nil && settings?.interceptOnlyAsyncAjaxRequests != newSettings.interceptOnlyAsyncAjaxRequests {

            if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled,

               let interceptOnlyAsyncAjaxRequestsPluginScript = interceptOnlyAsyncAjaxRequestsPluginScript {

                enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE,

                                            enable: newSettings.interceptOnlyAsyncAjaxRequests,

                                            pluginScript: interceptOnlyAsyncAjaxRequestsPluginScript)

            }

        }

        

        if newSettingsMap["useShouldInterceptFetchRequest"] != nil && settings?.useShouldInterceptFetchRequest != newSettings.useShouldInterceptFetchRequest {

            if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled {

                enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE,

                                            enable: newSettings.useShouldInterceptFetchRequest,

                                            pluginScript: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT)

            } else {

                newSettings.useShouldInterceptFetchRequest = false

            }

        }

        

        if newSettingsMap["mediaPlaybackRequiresUserGesture"] != nil && settings?.mediaPlaybackRequiresUserGesture != newSettings.mediaPlaybackRequiresUserGesture {

            if #available(iOS 10.0, *) {

                configuration.mediaTypesRequiringUserActionForPlayback = (newSettings.mediaPlaybackRequiresUserGesture) ? .all : []

            } else {

                // Fallback on earlier versions

                configuration.mediaPlaybackRequiresUserAction = newSettings.mediaPlaybackRequiresUserGesture

            }

        }

        

        if newSettingsMap["allowsInlineMediaPlayback"] != nil && settings?.allowsInlineMediaPlayback != newSettings.allowsInlineMediaPlayback {

            configuration.allowsInlineMediaPlayback = newSettings.allowsInlineMediaPlayback

        }

        

        if newSettingsMap["suppressesIncrementalRendering"] != nil && settings?.suppressesIncrementalRendering != newSettings.suppressesIncrementalRendering {

            configuration.suppressesIncrementalRendering = newSettings.suppressesIncrementalRendering

        }

        

        if newSettingsMap["allowsBackForwardNavigationGestures"] != nil && settings?.allowsBackForwardNavigationGestures != newSettings.allowsBackForwardNavigationGestures {

            allowsBackForwardNavigationGestures = newSettings.allowsBackForwardNavigationGestures

        }

        

        if newSettingsMap["javaScriptCanOpenWindowsAutomatically"] != nil && settings?.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically {

            configuration.preferences.javaScriptCanOpenWindowsAutomatically = newSettings.javaScriptCanOpenWindowsAutomatically

        }

        

        if newSettingsMap["minimumFontSize"] != nil && settings?.minimumFontSize != newSettings.minimumFontSize {

            configuration.preferences.minimumFontSize = CGFloat(newSettings.minimumFontSize)

        }

        

        if newSettingsMap["selectionGranularity"] != nil && settings?.selectionGranularity != newSettings.selectionGranularity {

            configuration.selectionGranularity = WKSelectionGranularity.init(rawValue: newSettings.selectionGranularity)!

        }

        

        if #available(iOS 10.0, *) {

            if newSettingsMap["ignoresViewportScaleLimits"] != nil && settings?.ignoresViewportScaleLimits != newSettings.ignoresViewportScaleLimits {

                configuration.ignoresViewportScaleLimits = newSettings.ignoresViewportScaleLimits

            }

            

            if newSettingsMap["dataDetectorTypes"] != nil && settings?.dataDetectorTypes != newSettings.dataDetectorTypes {

                var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0)

                for type in newSettings.dataDetectorTypes {

                    let dataDetectorType = Util.getDataDetectorType(type: type)

                    dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue)

                }

                configuration.dataDetectorTypes = dataDetectorTypes

            }

        }

        

        if #available(iOS 13.0, *) {

            if newSettingsMap["isFraudulentWebsiteWarningEnabled"] != nil && settings?.isFraudulentWebsiteWarningEnabled != newSettings.isFraudulentWebsiteWarningEnabled {

                configuration.preferences.isFraudulentWebsiteWarningEnabled = newSettings.isFraudulentWebsiteWarningEnabled

            }

            if newSettingsMap["preferredContentMode"] != nil && settings?.preferredContentMode != newSettings.preferredContentMode {

                configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: newSettings.preferredContentMode)!

            }

            if newSettingsMap["automaticallyAdjustsScrollIndicatorInsets"] != nil && settings?.automaticallyAdjustsScrollIndicatorInsets != newSettings.automaticallyAdjustsScrollIndicatorInsets {

                scrollView.automaticallyAdjustsScrollIndicatorInsets = newSettings.automaticallyAdjustsScrollIndicatorInsets

            }

        }

        

        if newSettingsMap["disableVerticalScroll"] != nil && settings?.disableVerticalScroll != newSettings.disableVerticalScroll {

            scrollView.showsVerticalScrollIndicator = !newSettings.disableVerticalScroll

        }

        if newSettingsMap["disableHorizontalScroll"] != nil && settings?.disableHorizontalScroll != newSettings.disableHorizontalScroll {

            scrollView.showsHorizontalScrollIndicator = !newSettings.disableHorizontalScroll

        }

        

        if newSettingsMap["verticalScrollBarEnabled"] != nil && settings?.verticalScrollBarEnabled != newSettings.verticalScrollBarEnabled {

            scrollView.showsVerticalScrollIndicator = newSettings.verticalScrollBarEnabled

        }

        if newSettingsMap["horizontalScrollBarEnabled"] != nil && settings?.horizontalScrollBarEnabled != newSettings.horizontalScrollBarEnabled {

            scrollView.showsHorizontalScrollIndicator = newSettings.horizontalScrollBarEnabled

        }

        

        if newSettingsMap["isDirectionalLockEnabled"] != nil && settings?.isDirectionalLockEnabled != newSettings.isDirectionalLockEnabled {

            scrollView.isDirectionalLockEnabled = newSettings.isDirectionalLockEnabled

        }

        

        if newSettingsMap["decelerationRate"] != nil && settings?.decelerationRate != newSettings.decelerationRate {

            scrollView.decelerationRate = Util.getDecelerationRate(type: newSettings.decelerationRate)

        }

        if newSettingsMap["alwaysBounceVertical"] != nil && settings?.alwaysBounceVertical != newSettings.alwaysBounceVertical {

            scrollView.alwaysBounceVertical = newSettings.alwaysBounceVertical

        }

        if newSettingsMap["alwaysBounceHorizontal"] != nil && settings?.alwaysBounceHorizontal != newSettings.alwaysBounceHorizontal {

            scrollView.alwaysBounceHorizontal = newSettings.alwaysBounceHorizontal

        }

        if newSettingsMap["scrollsToTop"] != nil && settings?.scrollsToTop != newSettings.scrollsToTop {

            scrollView.scrollsToTop = newSettings.scrollsToTop

        }

        if newSettingsMap["isPagingEnabled"] != nil && settings?.isPagingEnabled != newSettings.isPagingEnabled {

            scrollView.scrollsToTop = newSettings.isPagingEnabled

        }

        if newSettingsMap["maximumZoomScale"] != nil && settings?.maximumZoomScale != newSettings.maximumZoomScale {

            scrollView.maximumZoomScale = CGFloat(newSettings.maximumZoomScale)

        }

        if newSettingsMap["minimumZoomScale"] != nil && settings?.minimumZoomScale != newSettings.minimumZoomScale {

            scrollView.minimumZoomScale = CGFloat(newSettings.minimumZoomScale)

        }

        

        if #available(iOS 9.0, *) {

            if newSettingsMap["allowsLinkPreview"] != nil && settings?.allowsLinkPreview != newSettings.allowsLinkPreview {

                allowsLinkPreview = newSettings.allowsLinkPreview

            }

            if newSettingsMap["allowsAirPlayForMediaPlayback"] != nil && settings?.allowsAirPlayForMediaPlayback != newSettings.allowsAirPlayForMediaPlayback {

                configuration.allowsAirPlayForMediaPlayback = newSettings.allowsAirPlayForMediaPlayback

            }

            if newSettingsMap["allowsPictureInPictureMediaPlayback"] != nil && settings?.allowsPictureInPictureMediaPlayback != newSettings.allowsPictureInPictureMediaPlayback {

                configuration.allowsPictureInPictureMediaPlayback = newSettings.allowsPictureInPictureMediaPlayback

            }

            if newSettingsMap["applicationNameForUserAgent"] != nil && settings?.applicationNameForUserAgent != newSettings.applicationNameForUserAgent && newSettings.applicationNameForUserAgent != "" {

                configuration.applicationNameForUserAgent = newSettings.applicationNameForUserAgent

            }

            if newSettingsMap["userAgent"] != nil && settings?.userAgent != newSettings.userAgent && newSettings.userAgent != "" {

                customUserAgent = newSettings.userAgent

            }

        }

        

        if newSettingsMap["allowUniversalAccessFromFileURLs"] != nil && settings?.allowUniversalAccessFromFileURLs != newSettings.allowUniversalAccessFromFileURLs {

            configuration.setValue(newSettings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs")

        }

        

        if newSettingsMap["allowFileAccessFromFileURLs"] != nil && settings?.allowFileAccessFromFileURLs != newSettings.allowFileAccessFromFileURLs {

            configuration.preferences.setValue(newSettings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs")

        }

        

        if newSettingsMap["clearCache"] != nil && newSettings.clearCache {

            clearCache()

        }

        

        if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled {

            configuration.preferences.javaScriptEnabled = newSettings.javaScriptEnabled

        }

        

        if #available(iOS 14.0, *) {

            if settings?.mediaType != newSettings.mediaType {

                mediaType = newSettings.mediaType

            }

            

            if newSettingsMap["pageZoom"] != nil && settings?.pageZoom != newSettings.pageZoom {

                pageZoom = CGFloat(newSettings.pageZoom)

            }

            

            if newSettingsMap["limitsNavigationsToAppBoundDomains"] != nil && settings?.limitsNavigationsToAppBoundDomains != newSettings.limitsNavigationsToAppBoundDomains {

                configuration.limitsNavigationsToAppBoundDomains = newSettings.limitsNavigationsToAppBoundDomains

            }

            

            if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled {

                configuration.defaultWebpagePreferences.allowsContentJavaScript = newSettings.javaScriptEnabled

            }

        }

        

        if #available(iOS 11.0, *), newSettingsMap["contentBlockers"] != nil {

            configuration.userContentController.removeAllContentRuleLists()

            let contentBlockers = newSettings.contentBlockers

            if contentBlockers.count > 0 {

                do {

                    let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: [])

                    let blockRules = String(data: jsonData, encoding: .utf8)

                    WKContentRuleListStore.default().compileContentRuleList(

                        forIdentifier: "ContentBlockingRules",

                        encodedContentRuleList: blockRules) { (contentRuleList, error) in

                            if let error = error {

                                print(error.localizedDescription)

                                return

                            }

                            self.configuration.userContentController.add(contentRuleList!)

                    }

                } catch {

                    print(error.localizedDescription)

                }

            }

        }

        

        if #available(iOS 15.0, *) {

            if newSettingsMap["upgradeKnownHostsToHTTPS"] != nil && settings?.upgradeKnownHostsToHTTPS != newSettings.upgradeKnownHostsToHTTPS {

                configuration.upgradeKnownHostsToHTTPS = newSettings.upgradeKnownHostsToHTTPS

            }

            if newSettingsMap["isTextInteractionEnabled"] != nil && settings?.isTextInteractionEnabled != newSettings.isTextInteractionEnabled {

                configuration.preferences.isTextInteractionEnabled = newSettings.isTextInteractionEnabled

            }

            if newSettingsMap["underPageBackgroundColor"] != nil, settings?.underPageBackgroundColor != newSettings.underPageBackgroundColor,

               let underPageBackgroundColor = newSettings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty {

                self.underPageBackgroundColor = UIColor(hexString: underPageBackgroundColor)

            }

        }

        if #available(iOS 15.4, *) {

            if newSettingsMap["isSiteSpecificQuirksModeEnabled"] != nil, settings?.isSiteSpecificQuirksModeEnabled != newSettings.isSiteSpecificQuirksModeEnabled {

                configuration.preferences.isSiteSpecificQuirksModeEnabled = newSettings.isSiteSpecificQuirksModeEnabled

            }

        }

        if #available(iOS 15.5, *) {

            if ((newSettingsMap["minimumViewportInset"] != nil && settings?.minimumViewportInset != newSettings.minimumViewportInset) ||

               (newSettingsMap["maximumViewportInset"] != nil && settings?.maximumViewportInset != newSettings.maximumViewportInset)),

               let minViewportInset = newSettings.minimumViewportInset, let maxViewportInset = newSettings.maximumViewportInset {

                setMinimumViewportInset(minViewportInset, maximumViewportInset: maxViewportInset)

            }

        }

        if #available(iOS 16.0, *) {

            if newSettingsMap["isFindInteractionEnabled"] != nil, settings?.isFindInteractionEnabled != newSettings.isFindInteractionEnabled {

                isFindInteractionEnabled = newSettings.isFindInteractionEnabled

            }

        }

        if #available(iOS 16.4, *) {

            if newSettingsMap["isInspectable"] != nil, settings?.isInspectable != newSettings.isInspectable {

                isInspectable = newSettings.isInspectable

            }

            if newSettingsMap["shouldPrintBackgrounds"] != nil, settings?.shouldPrintBackgrounds != newSettings.shouldPrintBackgrounds {

                configuration.preferences.shouldPrintBackgrounds = newSettings.shouldPrintBackgrounds

            }

        }

        

        scrollView.isScrollEnabled = !(newSettings.disableVerticalScroll && newSettings.disableHorizontalScroll)

        

        self.settings = newSettings

    }

    

    func getSettings() -> [String: Any?]? {

        if (self.settings == nil) {

            return nil

        }

        return self.settings!.getRealSettings(obj: self)

    }

    

    public func enablePluginScriptAtRuntime(flagVariable: String, enable: Bool, pluginScript: PluginScript) {

        evaluateJavascript(source: flagVariable) { (alreadyLoaded) in

            if let alreadyLoaded = alreadyLoaded as? Bool, alreadyLoaded {

                let enableSource = "\(flagVariable) = \(enable);"

                if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds {

                    for contentWorld in self.configuration.userContentController.contentWorlds {

                        self.evaluateJavaScript(enableSource, frame: nil, contentWorld: contentWorld, completionHandler: nil)

                    }

                } else {

                    self.evaluateJavaScript(enableSource, completionHandler: nil)

                }

                if !enable {

                    self.configuration.userContentController.removePluginScripts(with: pluginScript.groupName!)

                }

            }

            else if enable {

                if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds {

                    for contentWorld in self.configuration.userContentController.contentWorlds {

                        self.evaluateJavaScript(pluginScript.source, frame: nil, contentWorld: contentWorld, completionHandler: nil)

                        self.configuration.userContentController.addPluginScript(pluginScript)

                    }

                } else {

                    self.evaluateJavaScript(pluginScript.source, completionHandler: nil)

                    self.configuration.userContentController.addPluginScript(pluginScript)

                }

                self.configuration.userContentController.sync(scriptMessageHandler: self)

            }

        }

    }

    

    @available(*, deprecated, message: "Use InAppWebViewManager.clearAllCache instead.")

    public func clearCache() {

        if #available(iOS 9.0, *) {

            let date = NSDate(timeIntervalSince1970: 0)

            WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: date as Date, completionHandler:{ })

        } else {

            var libraryPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.libraryDirectory, FileManager.SearchPathDomainMask.userDomainMask, false).first!

            libraryPath += "/Cookies"

            

            do {

                try FileManager.default.removeItem(atPath: libraryPath)

            } catch {

                print("can't clear cache")

            }

            URLCache.shared.removeAllCachedResponses()

        }

    }

    

    public func injectDeferredObject(source: String, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) {

        var jsToInject = source

        if let wrapper = jsWrapper {

            let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: [])

            let sourceArrayString = String(data: jsonData!, encoding: .utf8)

            let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2))

            jsToInject = String(format: wrapper, sourceString!)

        }

        

        evaluateJavaScript(jsToInject) { (value, error) in

            guard let completionHandler = completionHandler else {

                return

            }

            

            if let error = error {

                let userInfo = (error as NSError).userInfo

                let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ??

                                   userInfo["NSLocalizedDescription"] as? String ??

                                   error.localizedDescription

                self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3)

            }

            

            if value == nil {

                completionHandler(nil)

                return

            }

            

            completionHandler(value)

        }

    }

    

    @available(iOS 14.0, *)

    public func injectDeferredObject(source: String, contentWorld: WKContentWorld, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) {

        var jsToInject = source

        if let wrapper = jsWrapper {

            let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: [])

            let sourceArrayString = String(data: jsonData!, encoding: .utf8)

            let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2))

            jsToInject = String(format: wrapper, sourceString!)

        }

        

        jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: jsToInject, contentWorld: contentWorld)

        

        evaluateJavaScript(jsToInject, frame: nil, contentWorld: contentWorld) { (evalResult) in

            guard let completionHandler = completionHandler else {

                return

            }

            

            switch (evalResult) {

            case .success(let value):

                completionHandler(value)

                return

            case .failure(let error):

                let userInfo = (error as NSError).userInfo

                let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ??

                                   userInfo["NSLocalizedDescription"] as? String ??

                                   error.localizedDescription

                self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3)

                break

            }

            

            completionHandler(nil)

        }

    }

    

    public override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {

        if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {

            if let completionHandler = completionHandler {

                completionHandler(nil, nil)

            }

            return

        }

        super.evaluateJavaScript(javaScriptString, completionHandler: completionHandler)

    }

    

    @available(iOS 14.0, *)

    public func evaluateJavaScript(_ javaScript: String, frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result<Any, Error>) -> Void)? = nil) {

        if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {

            return

        }

        super.evaluateJavaScript(javaScript, in: frame, in: contentWorld, completionHandler: completionHandler)

    }

    

    public func evaluateJavascript(source: String, completionHandler: ((Any?) -> Void)? = nil) {

        injectDeferredObject(source: source, withWrapper: nil, completionHandler: completionHandler)

    }

    

    @available(iOS 14.0, *)

    public func evaluateJavascript(source: String, contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) {

        injectDeferredObject(source: source, contentWorld: contentWorld, withWrapper: nil, completionHandler: completionHandler)

    }

    

    @available(iOS 14.0, *)

    public func callAsyncJavaScript(_ functionBody: String, arguments: [String : Any] = [:], frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result<Any, Error>) -> Void)? = nil) {

        if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {

            return

        }

        super.callAsyncJavaScript(functionBody, arguments: arguments, in: frame, in: contentWorld, completionHandler: completionHandler)

    }

    

    @available(iOS 14.0, *)

    public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) {

        let jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: functionBody, contentWorld: contentWorld)

        

        callAsyncJavaScript(jsToInject, arguments: arguments, frame: nil, contentWorld: contentWorld) { (evalResult) in

            guard let completionHandler = completionHandler else {

                return

            }

            

            var body: [String: Any?] = [

                "value": nil,

                "error": nil

            ]

            

            switch (evalResult) {

            case .success(let value):

                body["value"] = value

                break

            case .failure(let error):

                let userInfo = (error as NSError).userInfo

                body["error"] = userInfo["WKJavaScriptExceptionMessage"] ??

                                userInfo["NSLocalizedDescription"] as? String ??

                                error.localizedDescription

                self.channelDelegate?.onConsoleMessage(message: String(describing: body["error"]), messageLevel: 3)

                break

            }

            

            completionHandler(body)

        }

    }

    

    @available(iOS 10.3, *)

    public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], completionHandler: ((Any?) -> Void)? = nil) {

        if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {

            completionHandler?(nil)

        }

        

        var jsToInject = functionBody

        

        let resultUuid = NSUUID().uuidString

        if let completionHandler = completionHandler {

            callAsyncJavaScriptBelowIOS14Results[resultUuid] = completionHandler

        }

        

        var functionArgumentNamesList: [String] = []

        var functionArgumentValuesList: [String] = []

        let keys = arguments.keys

        keys.forEach { (key) in

            functionArgumentNamesList.append(key)

            functionArgumentValuesList.append("obj.\(key)")

        }

        

        let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ")

        let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ")

        

        jsToInject = CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS

            .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, with: functionArgumentNames)

            .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, with: functionArgumentValues)

            .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, with: Util.JSONStringify(value: arguments))

            .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_BODY, with: jsToInject)

            .replacingOccurrences(of: PluginScriptsUtil.VAR_RESULT_UUID, with: resultUuid)

        

        evaluateJavaScript(jsToInject) { (value, error) in

            if let error = error {

                let userInfo = (error as NSError).userInfo

                let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ??

                                   userInfo["NSLocalizedDescription"] as? String ??

                                   error.localizedDescription

                self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3)

                completionHandler?(nil)

                self.callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid)

            }

        }

    }

    

    public func injectJavascriptFileFromUrl(urlFile: String, scriptHtmlTagAttributes: [String:Any?]?) {

        var scriptAttributes = ""

        if let scriptHtmlTagAttributes = scriptHtmlTagAttributes {

            if let typeAttr = scriptHtmlTagAttributes["type"] as? String {

                scriptAttributes += " script.type = '\(typeAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let idAttr = scriptHtmlTagAttributes["id"] as? String {

                let scriptIdEscaped = idAttr.replacingOccurrences(of: "\'", with: "\\'")

                scriptAttributes += " script.id = '\(scriptIdEscaped)'; "

                scriptAttributes += """

                script.onload = function() {

                    if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) {

                        window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptLoaded', '\(scriptIdEscaped)');

                    }

                };

                """

                scriptAttributes += """

                script.onerror = function() {

                    if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) {

                        window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptError', '\(scriptIdEscaped)');

                    }

                };

                """

            }

            if let asyncAttr = scriptHtmlTagAttributes["async"] as? Bool, asyncAttr {

                scriptAttributes += " script.async = true; "

            }

            if let deferAttr = scriptHtmlTagAttributes["defer"] as? Bool, deferAttr {

                scriptAttributes += " script.defer = true; "

            }

            if let crossOriginAttr = scriptHtmlTagAttributes["crossOrigin"] as? String {

                scriptAttributes += " script.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let integrityAttr = scriptHtmlTagAttributes["integrity"] as? String {

                scriptAttributes += " script.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let noModuleAttr = scriptHtmlTagAttributes["noModule"] as? Bool, noModuleAttr {

                scriptAttributes += " script.noModule = true; "

            }

            if let nonceAttr = scriptHtmlTagAttributes["nonce"] as? String {

                scriptAttributes += " script.nonce = '\(nonceAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let referrerPolicyAttr = scriptHtmlTagAttributes["referrerPolicy"] as? String {

                scriptAttributes += " script.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

        }

        let jsWrapper = "(function(d) { var script = d.createElement('script'); \(scriptAttributes) script.src = %@; d.body.appendChild(script); })(document);"

        injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil)

    }

    

    public func injectCSSCode(source: String) {

        let jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %@; d.head.appendChild(style); })(document);"

        injectDeferredObject(source: source, withWrapper: jsWrapper, completionHandler: nil)

    }

    

    public func injectCSSFileFromUrl(urlFile: String, cssLinkHtmlTagAttributes: [String:Any?]?) {

        var cssLinkAttributes = ""

        var alternateStylesheet = ""

        if let cssLinkHtmlTagAttributes = cssLinkHtmlTagAttributes {

            if let idAttr = cssLinkHtmlTagAttributes["id"] as? String {

                cssLinkAttributes += " link.id = '\(idAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let mediaAttr = cssLinkHtmlTagAttributes["media"] as? String {

                cssLinkAttributes += " link.media = '\(mediaAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let crossOriginAttr = cssLinkHtmlTagAttributes["crossOrigin"] as? String {

                cssLinkAttributes += " link.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let integrityAttr = cssLinkHtmlTagAttributes["integrity"] as? String {

                cssLinkAttributes += " link.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let referrerPolicyAttr = cssLinkHtmlTagAttributes["referrerPolicy"] as? String {

                cssLinkAttributes += " link.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

            if let disabledAttr = cssLinkHtmlTagAttributes["disabled"] as? Bool, disabledAttr {

                cssLinkAttributes += " link.disabled = true; "

            }

            if let alternateAttr = cssLinkHtmlTagAttributes["alternate"] as? Bool, alternateAttr {

                alternateStylesheet = "alternate "

            }

            if let titleAttr = cssLinkHtmlTagAttributes["title"] as? String {

                cssLinkAttributes += " link.title = '\(titleAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "

            }

        }

        let jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='\(alternateStylesheet)stylesheet', link.type='text/css'; \(cssLinkAttributes) link.href = %@; d.head.appendChild(link); })(document);"

        injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil)

    }

    

    public func getCopyBackForwardList() -> [String: Any] {

        let currentList = backForwardList

        let currentIndex = currentList.backList.count

        var completeList = currentList.backList

        if currentList.currentItem != nil {

            completeList.append(currentList.currentItem!)

        }

        completeList.append(contentsOf: currentList.forwardList)

        

        var history: [[String: String]] = []

        

        for historyItem in completeList {

            var historyItemMap: [String: String] = [:]

            historyItemMap["originalUrl"] = historyItem.initialURL.absoluteString

            historyItemMap["title"] = historyItem.title

            historyItemMap["url"] = historyItem.url.absoluteString

            history.append(historyItemMap)

        }

        

        var result: [String: Any] = [:]

        result["list"] = history

        result["currentIndex"] = currentIndex

        

        return result;

    }



    @available(iOS 15.0, *)

    public func webView(_ webView: WKWebView,

                        requestMediaCapturePermissionFor origin: WKSecurityOrigin,

                        initiatedByFrame frame: WKFrameInfo,

                        type: WKMediaCaptureType,

                        decisionHandler: @escaping (WKPermissionDecision) -> Void) {

        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)

                        break

                    case 2:

                        decisionHandler(.prompt)

                        break

                    default:

                        decisionHandler(.deny)

                }

                return false

            }

            return true

        }

        callback.defaultBehaviour = { (response: PermissionResponse?) in

            if !decisionHandlerCalled {

                decisionHandlerCalled = true

                decisionHandler(.deny)

            }

        }

        callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

            print(code + ", " + (message ?? ""))

            callback?.defaultBehaviour(nil)

        }

        

        if let channelDelegate = channelDelegate {

            channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback)

        } else {

            callback.defaultBehaviour(nil)

        }

    }

    

    @available(iOS 15.0, *)

    public func webView(_ webView: WKWebView,

                        requestDeviceOrientationAndMotionPermissionFor origin: WKSecurityOrigin,

                        initiatedByFrame frame: WKFrameInfo,

                        decisionHandler: @escaping (WKPermissionDecision) -> Void) {

        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)

                        break

                    case 2:

                        decisionHandler(.prompt)

                        break

                    default:

                        decisionHandler(.deny)

                }

                return false

            }

            return true

        }

        callback.defaultBehaviour = { (response: PermissionResponse?) in

            if !decisionHandlerCalled {

                decisionHandlerCalled = true

                decisionHandler(.deny)

            }

        }

        callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

            print(code + ", " + (message ?? ""))

            callback?.defaultBehaviour(nil)

        }

        

        if let channelDelegate = channelDelegate {

            channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback)

        } else {

            callback.defaultBehaviour(nil)

        }

    }

    

    @available(iOS 13.0, *)

    public func webView(_ webView: WKWebView,

                 decidePolicyFor navigationAction: WKNavigationAction,

                 preferences: WKWebpagePreferences,

                 decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {

        self.webView(webView, decidePolicyFor: navigationAction, decisionHandler: {(navigationActionPolicy) -> Void in

            decisionHandler(navigationActionPolicy, preferences)

        })

    }

    

    @available(iOS 14.5, *)

    public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {

        if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart {

            let downloadStartRequest = DownloadStartRequest(url: url.absoluteString,

                                                            userAgent: nil,

                                                            contentDisposition: nil,

                                                            mimeType: response.mimeType,

                                                            contentLength: response.expectedContentLength,

                                                            suggestedFilename: suggestedFilename,

                                                            textEncodingName: response.textEncodingName)

            channelDelegate?.onDownloadStartRequest(request: downloadStartRequest)

        }

        download.delegate = nil

        // cancel the download

        completionHandler(nil)

    }

    

    @available(iOS 14.5, *)

    public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {

        let response = navigationResponse.response

        if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart {

            let downloadStartRequest = DownloadStartRequest(url: url.absoluteString,

                                                            userAgent: nil,

                                                            contentDisposition: nil,

                                                            mimeType: response.mimeType,

                                                            contentLength: response.expectedContentLength,

                                                            suggestedFilename: response.suggestedFilename,

                                                            textEncodingName: response.textEncodingName)

            channelDelegate?.onDownloadStartRequest(request: downloadStartRequest)

        }

        download.delegate = nil

    }

    

    public func webView(_ webView: WKWebView,

                 decidePolicyFor navigationAction: WKNavigationAction,

                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        var decisionHandlerCalled = false

        let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback()

        callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in

            decisionHandlerCalled = true

            decisionHandler(response)

            return false

        }

        callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in

            if !decisionHandlerCalled {

                decisionHandlerCalled = true

                decisionHandler(.allow)

            }

        }

        callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

            print(code + ", " + (message ?? ""))

            callback?.defaultBehaviour(nil)

        }



        let runCallback = {

            if let useShouldOverrideUrlLoading = self.settings?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading, let channelDelegate = self.channelDelegate {

                channelDelegate.shouldOverrideUrlLoading(navigationAction: navigationAction, callback: callback)

            } else {

                callback.defaultBehaviour(nil)

            }

        }

        

        if windowId != nil, !windowCreated {

            windowBeforeCreatedCallbacks.append(runCallback)

        } else {

            runCallback()

        }

    }

    

    public func webView(_ webView: WKWebView,

                 decidePolicyFor navigationResponse: WKNavigationResponse,

                 decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {

        if let response = navigationResponse.response as? HTTPURLResponse, response.statusCode >= 400 {

            let request = WebResourceRequest.init(fromWKNavigationResponse: navigationResponse)

            let errorResponse = WebResourceResponse.init(fromWKNavigationResponse: navigationResponse)

            channelDelegate?.onReceivedHttpError(request: request, errorResponse: errorResponse)

        }

        

        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

                if !decisionHandlerCalled {

                    decisionHandlerCalled = true

                    decisionHandler(.allow)

                }

            }

            callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

                print(code + ", " + (message ?? ""))

                callback?.defaultBehaviour(nil)

            }

            

            if let channelDelegate = channelDelegate {

                channelDelegate.onNavigationResponse(navigationResponse: navigationResponse, callback: callback)

            } else {

                callback.defaultBehaviour(nil)

            }

        }

        

        if let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart {

            if #available(iOS 14.5, *), !navigationResponse.canShowMIMEType, useOnNavigationResponse == nil || !useOnNavigationResponse! {

                decisionHandler(.download)

                return

            } else {

                let mimeType = navigationResponse.response.mimeType

                if let url = navigationResponse.response.url, navigationResponse.isForMainFrame {

                    if url.scheme != "file", mimeType != nil, !mimeType!.starts(with: "text/") {

                        let downloadStartRequest = DownloadStartRequest(url: url.absoluteString,

                                                                        userAgent: nil,

                                                                        contentDisposition: nil,

                                                                        mimeType: mimeType,

                                                                        contentLength: navigationResponse.response.expectedContentLength,

                                                                        suggestedFilename: navigationResponse.response.suggestedFilename,

                                                                        textEncodingName: navigationResponse.response.textEncodingName)

                        channelDelegate?.onDownloadStartRequest(request: downloadStartRequest)

                        if useOnNavigationResponse == nil || !useOnNavigationResponse! {

                            decisionHandler(.cancel)

                        }

                        return

                    }

                }

            }

        }

        

        if useOnNavigationResponse == nil || !useOnNavigationResponse! {

            decisionHandler(.allow)

        }

    }

    

    public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {

        currentOriginalUrl = url

        lastTouchPoint = nil

        

        disposeWebMessageChannels()

        initializeWindowIdJS()

        

        if #available(iOS 14.0, *) {

            configuration.userContentController.resetContentWorlds(windowId: windowId)

        }

        

        channelDelegate?.onLoadStart(url: url?.absoluteString)

        

        inAppBrowserDelegate?.didStartNavigation(url: url)

    }

    

    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

        initializeWindowIdJS()

        

        InAppWebView.credentialsProposed = []

        evaluateJavaScript(PLATFORM_READY_JS_SOURCE, completionHandler: nil)

        

        // sometimes scrollView.contentSize doesn't fit all the frame.size available

        // so, we call setNeedsLayout to redraw the layout

        let webViewFrameSize = frame.size

        let scrollViewSize = scrollView.contentSize

        if (scrollViewSize.width < webViewFrameSize.width || scrollViewSize.height < webViewFrameSize.height) {

            setNeedsLayout()

        }



        channelDelegate?.onLoadStop(url: url?.absoluteString)

        

        inAppBrowserDelegate?.didFinishNavigation(url: url)

    }

    

    public func webView(_ view: WKWebView,

                 didFailProvisionalNavigation navigation: WKNavigation!,

                 withError error: Error) {

        webView(view, didFail: navigation, withError: error)

    }

    

    public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {

        InAppWebView.credentialsProposed = []

        

        var urlError: URL = url ?? URL(string: "about:blank")!

        var errorCode = -1

        var errorDescription = "domain=\(error._domain), code=\(error._code), \(error.localizedDescription)"

        

        if let info = error as? URLError {

            if let failingURL = info.failingURL {

                urlError = failingURL

            }

            errorCode = info.code.rawValue

            errorDescription = info.localizedDescription

        }

        else if let info = error._userInfo as? [String: Any] {

            if let failingUrl = info[NSURLErrorFailingURLErrorKey] as? URL {

                urlError = failingUrl

            }

            if let failingUrlString = info[NSURLErrorFailingURLStringErrorKey] as? String,

               let failingUrl = URL(string: failingUrlString) {

                urlError = failingUrl

            }

        }

        

        let webResourceRequest = WebResourceRequest(url: urlError, headers: nil)

        let webResourceError = WebResourceError(type: errorCode, errorDescription: errorDescription)

        

        channelDelegate?.onReceivedError(request: webResourceRequest, error: webResourceError)

        

        inAppBrowserDelegate?.didFailNavigation(url: url, error: error)

    }

    

    public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        var completionHandlerCalled = false

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic ||

            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault ||

            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest ||

            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNegotiate ||

            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM {

            let host = challenge.protectionSpace.host

            let prot = challenge.protectionSpace.protocol

            let realm = challenge.protectionSpace.realm

            let port = challenge.protectionSpace.port

            

            let callback = WebViewChannelDelegate.ReceivedHttpAuthRequestCallback()

            callback.nonNullSuccess = { (response: HttpAuthResponse) in

                if let action = response.action {

                    completionHandlerCalled = true

                    switch action {

                        case 0:

                            InAppWebView.credentialsProposed = []

                            // used .performDefaultHandling to maintain consistency with Android

                            // because .cancelAuthenticationChallenge will call webView(_:didFail:withError:)

                            completionHandler(.performDefaultHandling, nil)

                            //completionHandler(.cancelAuthenticationChallenge, nil)

                            break

                        case 1:

                            let username = response.username

                            let password = response.password

                            let permanentPersistence = response.permanentPersistence

                            let persistence = (permanentPersistence) ? URLCredential.Persistence.permanent : URLCredential.Persistence.forSession

                            let credential = URLCredential(user: username, password: password, persistence: persistence)

                            completionHandler(.useCredential, credential)

                            break

                        case 2:

                            if InAppWebView.credentialsProposed.count == 0 {

                                for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials {

                                    if protectionSpace.host == host && protectionSpace.realm == realm &&

                                    protectionSpace.protocol == prot && protectionSpace.port == port {

                                        for credential in credentials {

                                            InAppWebView.credentialsProposed.append(credential.value)

                                        }

                                        break

                                    }

                                }

                            }

                            if InAppWebView.credentialsProposed.count == 0, let credential = challenge.proposedCredential {

                                InAppWebView.credentialsProposed.append(credential)

                            }

                            

                            if let credential = InAppWebView.credentialsProposed.popLast() {

                                completionHandler(.useCredential, credential)

                            }

                            else {

                                completionHandler(.performDefaultHandling, nil)

                            }

                            break

                        default:

                            InAppWebView.credentialsProposed = []

                            completionHandler(.performDefaultHandling, nil)

                    }

                    return false

                }

                return true

            }

            callback.defaultBehaviour = { (response: HttpAuthResponse?) in

                if !completionHandlerCalled {

                    completionHandlerCalled = true

                    completionHandler(.performDefaultHandling, nil)

                }

            }

            callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

                print(code + ", " + (message ?? ""))

                callback?.defaultBehaviour(nil)

            }

            

            let runCallback = {

                if let channelDelegate = self.channelDelegate {

                    channelDelegate.onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge(fromChallenge: challenge), callback: callback)

                } else {

                    callback.defaultBehaviour(nil)

                }

            }

            

            if windowId != nil, !windowCreated {

                windowBeforeCreatedCallbacks.append(runCallback)

            } else {

                runCallback()

            }

        }

        else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {

            guard let serverTrust = challenge.protectionSpace.serverTrust else {

                completionHandler(.performDefaultHandling, nil)

                return

            }

            

            if let scheme = challenge.protectionSpace.protocol, scheme == "https" {

                // workaround for ProtectionSpace SSL Certificate

                // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678

                DispatchQueue.global(qos: .background).async {

                    if let sslCertificate = challenge.protectionSpace.sslCertificate {

                        DispatchQueue.main.async {

                            InAppWebView.sslCertificatesMap[challenge.protectionSpace.host] = sslCertificate

                        }

                    }

                }

            }

            

            let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback()

            callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in

                if let action = response.action {

                    completionHandlerCalled = true

                    switch action {

                        case 0:

                            InAppWebView.credentialsProposed = []

                            completionHandler(.cancelAuthenticationChallenge, nil)

                            break

                        case 1:

                            // workaround for https://github.com/pichillilorenzo/flutter_inappwebview/issues/1924

                            DispatchQueue.global(qos: .background).async {

                                let exceptions = SecTrustCopyExceptions(serverTrust)

                                SecTrustSetExceptions(serverTrust, exceptions)

                                let credential = URLCredential(trust: serverTrust)

                                completionHandler(.useCredential, credential)

                            }

                            break

                        default:

                            InAppWebView.credentialsProposed = []

                            completionHandler(.performDefaultHandling, nil)

                    }

                    return false

                }

                return true

            }

            callback.defaultBehaviour = { (response: ServerTrustAuthResponse?) in

                if !completionHandlerCalled {

                    completionHandlerCalled = true

                    completionHandler(.performDefaultHandling, nil)

                }

            }

            callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

                print(code + ", " + (message ?? ""))

                callback?.defaultBehaviour(nil)

            }

            

            let runCallback = {

                if let channelDelegate = self.channelDelegate {

                    channelDelegate.onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge(fromChallenge: challenge), callback: callback)

                } else {

                    callback.defaultBehaviour(nil)

                }

            }

            

            if windowId != nil, !windowCreated {

                windowBeforeCreatedCallbacks.append(runCallback)

            } else {

                runCallback()

            }

        }

        else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {

            let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback()

            callback.nonNullSuccess = { (response: ClientCertResponse) in

                if let action = response.action {

                    completionHandlerCalled = true

                    switch action {

                        case 0:

                            completionHandler(.cancelAuthenticationChallenge, nil)

                            break

                        case 1:

                            let certificatePath = response.certificatePath

                            let certificatePassword = response.certificatePassword ?? "";

                            

                            var path: String = certificatePath

                            do {

                                if let plugin = self.plugin {

                                    path = try Util.getAbsPathAsset(plugin: plugin, assetFilePath: certificatePath)

                                }

                            } catch {}

                            

                            if let PKCS12Data = NSData(contentsOfFile: path),

                               let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) {

                                let urlCredential: URLCredential = URLCredential(

                                    identity: identityAndTrust.identityRef,

                                    certificates: identityAndTrust.certArray as? [AnyObject],

                                    persistence: URLCredential.Persistence.forSession);

                                completionHandler(.useCredential, urlCredential)

                            } else {

                                completionHandler(.performDefaultHandling, nil)

                            }

                            

                            break

                        case 2:

                            completionHandler(.cancelAuthenticationChallenge, nil)

                            break

                        default:

                            completionHandler(.performDefaultHandling, nil)

                    }

                    return false

                }

                return true

            }

            callback.defaultBehaviour = { (response: ClientCertResponse?) in

                if !completionHandlerCalled {

                    completionHandlerCalled = true

                    completionHandler(.performDefaultHandling, nil)

                }

            }

            callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

                print(code + ", " + (message ?? ""))

                callback?.defaultBehaviour(nil)

            }

            

            let runCallback = {

                if let channelDelegate = self.channelDelegate {

                    channelDelegate.onReceivedClientCertRequest(challenge: ClientCertChallenge(fromChallenge: challenge), callback: callback)

                } else {

                    callback.defaultBehaviour(nil)

                }

            }

            

            if windowId != nil, !windowCreated {

                windowBeforeCreatedCallbacks.append(runCallback)

            } else {

                runCallback()

            }

        }

        else {

            completionHandler(.performDefaultHandling, nil)

        }

    }

    

    struct IdentityAndTrust {

        var identityRef: SecIdentity

        var trust: SecTrust

        var certArray: AnyObject

    }



    func extractIdentity(PKCS12Data: NSData, password: String) -> IdentityAndTrust? {

        var identityAndTrust: IdentityAndTrust?

        var securityError: OSStatus = errSecSuccess



        var importResult: CFArray?

        securityError = SecPKCS12Import(

            PKCS12Data as NSData,

            [kSecImportExportPassphrase as String: password] as NSDictionary,

            &importResult

        )



        if securityError == errSecSuccess {

            let certItems: CFArray = importResult! as CFArray

            let certItemsArray: Array = certItems as Array

            let dict: AnyObject? = certItemsArray.first

            if let certEntry: Dictionary = dict as? Dictionary<String, AnyObject> {

                // grab the identity

                let identityPointer: AnyObject? = certEntry["identity"]

                let secIdentityRef:SecIdentity = (identityPointer as! SecIdentity?)!

                // grab the trust

                let trustPointer: AnyObject? = certEntry["trust"]

                let trustRef:SecTrust = trustPointer as! SecTrust

                // grab the cert

                let chainPointer: AnyObject? = certEntry["chain"]

                identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray:  chainPointer!)

            }

        } else {

            print("Security Error: " + securityError.description)

            if #available(iOS 11.3, *) {

                print(SecCopyErrorMessageString(securityError, nil) ?? "")

            }

        }

        return identityAndTrust

    }

    

    func createAlertDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, completionHandler: @escaping () -> Void) {

        let title = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message

        let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "")

        let alertController = UIAlertController(title: title, message: nil,

                                                preferredStyle: UIAlertController.Style.alert);

        

        alertController.addAction(UIAlertAction(title: okButton, style: UIAlertAction.Style.default) {

            _ in completionHandler()}

        )

        

        guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else {

            completionHandler()

            return

        }

        presentingViewController.present(alertController, animated: true, completion: {})

    }

    

    public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String,

                 initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {

        

        if (isPausedTimers) {

            isPausedTimersCompletionHandler = completionHandler

            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:

                        completionHandler()

                        break

                    default:

                        completionHandler()

                }

                return false

            }

            return true

        }

        callback.defaultBehaviour = { [weak self] (response: JsAlertResponse?) in

            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

            if !completionHandlerCalled {

                completionHandlerCalled = true

                print(code + ", " + (message ?? ""))

                completionHandler()

            }

        }

        

        if let channelDelegate = channelDelegate {

            channelDelegate.onJsAlert(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback)

        } else {

            callback.defaultBehaviour(nil)

        }

    }

    

    func createConfirmDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, completionHandler: @escaping (Bool) -> Void) {

        let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message

        let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "")

        let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "")

        

        let confirmController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert)

        

        confirmController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in

            completionHandler(true)

        }))

        

        confirmController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in

            completionHandler(false)

        }))

        

        guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else {

            completionHandler(false)

            return

        }

        presentingViewController.present(confirmController, animated: true, completion: nil)

    }

    

    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:

                        completionHandler(true)

                        break

                    case 1:

                        completionHandler(false)

                        break

                    default:

                        completionHandler(false)

                }

                return false

            }

            return true

        }

        callback.defaultBehaviour = { [weak self] (response: JsConfirmResponse?) in

            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

            if !completionHandlerCalled {

                completionHandlerCalled = true

                print(code + ", " + (message ?? ""))

                completionHandler(false)

            }

        }

        

        if let channelDelegate = channelDelegate {

            channelDelegate.onJsConfirm(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback)

        } else {

            callback.defaultBehaviour(nil)

        }

    }



    func createPromptDialog(message: String, defaultValue: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, value: String?, completionHandler: @escaping (String?) -> Void) {

        let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message

        let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "")

        let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "")

        

        let promptController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert)

        

        promptController.addTextField { (textField) in

            textField.text = defaultValue

        }

        

        promptController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in

            if let v = value {

                completionHandler(v)

            }

            else if let text = promptController.textFields?.first?.text {

                completionHandler(text)

            } else {

                completionHandler("")

            }

        }))

        

        promptController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in

            completionHandler(nil)

        }))

        

        guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else {

            completionHandler(nil)

            return

        }

        presentingViewController.present(promptController, animated: true, completion: nil)

    }

    

    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:

                        completionHandler(response.value)

                        break

                    case 1:

                        completionHandler(nil)

                        break

                    default:

                        completionHandler(nil)

                }

                return false

            }

            return true

        }

        callback.defaultBehaviour = { [weak self] (response: JsPromptResponse?) in

            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

            if !completionHandlerCalled {

                completionHandlerCalled = true

                print(code + ", " + (message ?? ""))

                completionHandler(nil)

            }

        }

        

        if let channelDelegate = channelDelegate {

            channelDelegate.onJsPrompt(url: frame.request.url, message: message, defaultValue: defaultValue, isMainFrame: frame.isMainFrame, callback: callback)

        } else {

            callback.defaultBehaviour(nil)

        }

    }

    

    /// UIScrollViewDelegate is somehow bugged:

    /// if InAppWebView implements the UIScrollViewDelegate protocol and implement the scrollViewDidScroll event,

    /// then, when the user scrolls the content, the webview content is not rendered (just white space).

    /// Calling setNeedsLayout() resolves this problem, but, for some reason, the bounce effect is canceled.

    ///

    /// So, to track the same event, without implementing the scrollViewDidScroll event, we create

    /// an observer that observes the scrollView.contentOffset property.

    /// This way, we don't need to call setNeedsLayout() and all works fine.

    public func onScrollChanged(startedByUser: Bool, oldContentOffset: CGPoint?) {

        let disableVerticalScroll = settings?.disableVerticalScroll ?? false

        let disableHorizontalScroll = settings?.disableHorizontalScroll ?? false

        if startedByUser {

            if disableVerticalScroll && disableHorizontalScroll {

                scrollView.contentOffset = CGPoint(x: lastScrollX, y: lastScrollY)

            }

            else if disableVerticalScroll {

                if scrollView.contentOffset.y >= 0 || scrollView.contentOffset.y < 0 {

                    scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: lastScrollY)

                }

            }

            else if disableHorizontalScroll {

                if scrollView.contentOffset.x >= 0 || scrollView.contentOffset.x < 0 {

                    scrollView.contentOffset = CGPoint(x: lastScrollX, y: scrollView.contentOffset.y)

                }

            }

        }

        if (!disableVerticalScroll && !disableHorizontalScroll) ||

            (disableVerticalScroll && scrollView.contentOffset.x != oldContentOffset?.x) ||

            (disableHorizontalScroll && scrollView.contentOffset.y != oldContentOffset?.y) {

            let x = Int(scrollView.contentOffset.x / scrollView.contentScaleFactor)

            let y = Int(scrollView.contentOffset.y / scrollView.contentScaleFactor)

            channelDelegate?.onScrollChanged(x: x, y: y)

        }

        lastScrollX = scrollView.contentOffset.x

        lastScrollY = scrollView.contentOffset.y

        

        let overScrolledHorizontally = lastScrollX < 0 || lastScrollX > (scrollView.contentSize.width - scrollView.frame.size.width)

        let overScrolledVertically = lastScrollY < 0 || lastScrollY > (scrollView.contentSize.height - scrollView.frame.size.height)

        if overScrolledHorizontally || overScrolledVertically {

            let x = Int(lastScrollX / scrollView.contentScaleFactor)

            let y = Int(lastScrollY / scrollView.contentScaleFactor)

            channelDelegate?.onOverScrolled(x: x, y: y,

                           clampedX: overScrolledHorizontally,

                           clampedY: overScrolledVertically)

        }

    }

    

    public func onContentSizeChanged(oldContentSize: CGSize) {

        channelDelegate?.onContentSizeChanged(oldContentSize: oldContentSize,

                                              newContentSize: scrollView.contentSize)

    }

    

    public func scrollViewDidZoom(_ scrollView: UIScrollView) {

        let newScale = Float(scrollView.zoomScale)

        if newScale != oldZoomScale {

            channelDelegate?.onZoomScaleChanged(newScale: newScale, oldScale: oldZoomScale)

            oldZoomScale = newScale

        }

    }

    

    public func webView(_ webView: WKWebView,

                        createWebViewWith configuration: WKWebViewConfiguration,

                  for navigationAction: WKNavigationAction,

                  windowFeatures: WKWindowFeatures) -> WKWebView? {

        

        var windowId: Int64 = 0

        let inAppWebViewManager = plugin?.inAppWebViewManager

        if let inAppWebViewManager = inAppWebViewManager {

            inAppWebViewManager.windowAutoincrementId += 1

            windowId = inAppWebViewManager.windowAutoincrementId

        }

        

        let windowWebView = InAppWebView(id: nil, plugin: nil, frame: self.bounds, configuration: configuration, contextMenu: nil)

        windowWebView.windowId = windowId



        let webViewTransport = WebViewTransport(

            webView: windowWebView,

            request: navigationAction.request

        )



        inAppWebViewManager?.windowWebViews[windowId] = webViewTransport

        

        let createWindowAction = CreateWindowAction(navigationAction: navigationAction, windowId: windowId, windowFeatures: windowFeatures, isDialog: nil)

        

        let callback = WebViewChannelDelegate.CreateWindowCallback()

        callback.nonNullSuccess = { (handledByClient: Bool) in

            return !handledByClient

        }

        callback.defaultBehaviour = { [weak self] (handledByClient: Bool?) in

            if inAppWebViewManager?.windowWebViews[windowId] != nil {

                inAppWebViewManager?.windowWebViews.removeValue(forKey: windowId)

            }

            self?.loadUrl(urlRequest: navigationAction.request, allowingReadAccessTo: nil)

        }

        callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

            print(code + ", " + (message ?? ""))

            callback?.defaultBehaviour(nil)

        }

        

        if let channelDelegate = channelDelegate {

            channelDelegate.onCreateWindow(createWindowAction: createWindowAction, callback: callback)

        } else {

            callback.defaultBehaviour(nil)

        }

        

        return windowWebView

    }

    

    public func webView(_ webView: WKWebView,

                        authenticationChallenge challenge: URLAuthenticationChallenge,

                        shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void) {

        var decisionHandlerCalled = false

        let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback()

        callback.nonNullSuccess = { (action: Bool) in

            decisionHandlerCalled = true

            decisionHandler(action)

            return false

        }

        callback.defaultBehaviour = { (action: Bool?) in

            if !decisionHandlerCalled {

                decisionHandlerCalled = true

                decisionHandler(false)

            }

        }

        callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

            print(code + ", " + (message ?? ""))

            callback?.defaultBehaviour(nil)

        }

        

        let runCallback = {

            if let channelDelegate = self.channelDelegate {

                channelDelegate.shouldAllowDeprecatedTLS(challenge: challenge, callback: callback)

            } else {

                callback.defaultBehaviour(nil)

            }

        }

        

        if windowId != nil, !windowCreated {

            windowBeforeCreatedCallbacks.append(runCallback)

        } else {

            runCallback()

        }

    }

    

    public func webViewDidClose(_ webView: WKWebView) {

        channelDelegate?.onCloseWindow()

    }

    

    public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {

        channelDelegate?.onWebContentProcessDidTerminate()

    }

    

    public func webView(_ webView: WKWebView,

                        didCommit navigation: WKNavigation!) {

        channelDelegate?.onPageCommitVisible(url: url?.absoluteString)

    }

    

    public func webView(_ webView: WKWebView,

                        didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {

        channelDelegate?.onDidReceiveServerRedirectForProvisionalNavigation()

    }

    

//    @available(iOS 13.0, *)

//    public func webView(_ webView: WKWebView,

//                        contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo,

//                        completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {

//        print("contextMenuConfigurationForElement")

//        let actionProvider: UIContextMenuActionProvider = { _ in

//            let editMenu = UIMenu(title: "Edit...", children: [

//                UIAction(title: "Copy") { action in

//

//                },

//                UIAction(title: "Duplicate") { action in

//

//                }

//            ])

//            return UIMenu(title: "Title", children: [

//                UIAction(title: "Share") { action in

//

//                },

//                editMenu

//            ])

//        }

//        let contextMenuConfiguration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider)

//        //completionHandler(contextMenuConfiguration)

//        completionHandler(nil)

////        onContextMenuConfigurationForElement(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in

////            if result is FlutterError {

////                print((result as! FlutterError).message ?? "")

////            }

////            else if (result as? NSObject) == FlutterMethodNotImplemented {

////                completionHandler(nil)

////            }

////            else {

////                var response: [String: Any]

////                if let r = result {

////                    response = r as! [String: Any]

////                    var action = response["action"] as? Int

////                    action = action != nil ? action : 0;

////                    switch action {

////                        case 0:

////                            break

////                        case 1:

////                            break

////                        default:

////                            completionHandler(nil)

////                    }

////                    return;

////                }

////                completionHandler(nil)

////            }

////        }*/)

//    }

////

//    @available(iOS 13.0, *)

//    public func webView(_ webView: WKWebView,

//                        contextMenuDidEndForElement elementInfo: WKContextMenuElementInfo) {

//        print("contextMenuDidEndForElement")

//        print(elementInfo)

//        //onContextMenuDidEndForElement(linkURL: elementInfo.linkURL?.absoluteString)

//    }

//

//    @available(iOS 13.0, *)

//    public func webView(_ webView: WKWebView,

//                        contextMenuForElement elementInfo: WKContextMenuElementInfo,

//                        willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {

//        print("willCommitWithAnimator")

//        print(elementInfo)

////        onWillCommitWithAnimator(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in

////            if result is FlutterError {

////                print((result as! FlutterError).message ?? "")

////            }

////            else if (result as? NSObject) == FlutterMethodNotImplemented {

////

////            }

////            else {

////                var response: [String: Any]

////                if let r = result {

////                    response = r as! [String: Any]

////                    var action = response["action"] as? Int

////                    action = action != nil ? action : 0;

//////                    switch action {

//////                        case 0:

//////                            break

//////                        case 1:

//////                            break

//////                        default:

//////

//////                    }

////                    return;

////                }

////

////            }

////        }*/)

//    }

//

//    @available(iOS 13.0, *)

//    public func webView(_ webView: WKWebView,

//                        contextMenuWillPresentForElement elementInfo: WKContextMenuElementInfo) {

//        print("contextMenuWillPresentForElement")

//        print(elementInfo.linkURL)

//        //onContextMenuWillPresentForElement(linkURL: elementInfo.linkURL?.absoluteString)

//    }

    

    

    // https://stackoverflow.com/a/42840541/4637638

    public func isVideoPlayerWindow(_ notificationObject: AnyObject?) -> Bool {

        let nonVideoClasses = ["_UIAlertControllerShimPresenterWindow",

                               "UITextEffectsWindow",

                               "UIRemoteKeyboardWindow",

                               "PGHostedWindow"]

        

        var isVideo = true

        if let obj = notificationObject {

            for nonVideoClass in nonVideoClasses {

                if let clazz = NSClassFromString(nonVideoClass) {

                    isVideo = isVideo && !(obj.isKind(of: clazz))

                }

            }

        }

        return isVideo

    }

    

    @objc func onEnterFullscreen(_ notification: Notification) {

        // TODO: Still not working on iOS 16.0!

//        if #available(iOS 16.0, *) {

//            channelDelegate?.onEnterFullscreen()

//            inFullscreen = true

//        }

//        else

        if (isVideoPlayerWindow(notification.object as AnyObject?)) {

            channelDelegate?.onEnterFullscreen()

            inFullscreen = true

        }

    }

    

    @objc func onExitFullscreen(_ notification: Notification) {

        // TODO: Still not working on iOS 16.0!

//        if #available(iOS 16.0, *) {

//            channelDelegate?.onExitFullscreen()

//            inFullscreen = false

//        }

//        else

        if (isVideoPlayerWindow(notification.object as AnyObject?)) {

            channelDelegate?.onExitFullscreen()

            inFullscreen = false

        }

    }

    

//    public func onContextMenuConfigurationForElement(linkURL: String?, result: FlutterResult?) {

//        let arguments: [String: Any?] = ["linkURL": linkURL]

//        channel?.invokeMethod("onContextMenuConfigurationForElement", arguments: arguments, result: result)

//    }

//

//    public func onContextMenuDidEndForElement(linkURL: String?) {

//        let arguments: [String: Any?] = ["linkURL": linkURL]

//        channel?.invokeMethod("onContextMenuDidEndForElement", arguments: arguments)

//    }

//

//    public func onWillCommitWithAnimator(linkURL: String?, result: FlutterResult?) {

//        let arguments: [String: Any?] = ["linkURL": linkURL]

//        channel?.invokeMethod("onWillCommitWithAnimator", arguments: arguments, result: result)

//    }

//

//    public func onContextMenuWillPresentForElement(linkURL: String?) {

//        let arguments: [String: Any?] = ["linkURL": linkURL]

//        channel?.invokeMethod("onContextMenuWillPresentForElement", arguments: arguments)

//    }

    

    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

        guard let body = message.body as? [String: Any?] else {

            return

        }

        

        if ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"].contains(message.name) {

            var messageLevel = 1

            switch (message.name) {

                case "consoleLog":

                    messageLevel = 1

                    break

                case "consoleDebug":

                    // on Android, console.debug is TIP

                    messageLevel = 0

                    break

                case "consoleError":

                    messageLevel = 3

                    break

                case "consoleInfo":

                    // on Android, console.info is LOG

                    messageLevel = 1

                    break

                case "consoleWarn":

                    messageLevel = 2

                    break

                default:

                    messageLevel = 1

                    break

            }

            let consoleMessage = body["message"] as? String ?? ""

            

            let _windowId = body["_windowId"] as? Int64

            var webView = self

            if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] {

                webView = webViewTransport.webView

            }

            webView.channelDelegate?.onConsoleMessage(message: consoleMessage, messageLevel: messageLevel)

        } else if message.name == "callHandler", let handlerName = body["handlerName"] as? String {

            if handlerName == "onPrintRequest" {

                let settings = PrintJobSettings()

                settings.handledByClient = true

                if let printJobId = printCurrentPage(settings: settings) {

                    let callback = WebViewChannelDelegate.PrintRequestCallback()

                    callback.nonNullSuccess = { (handledByClient: Bool) in

                        return !handledByClient

                    }

                    callback.defaultBehaviour = { [weak self] (handledByClient: Bool?) in

                        if let printJob = self?.plugin?.printJobManager?.jobs[printJobId] {

                            printJob?.disposeNoDismiss()

                        }

                    }

                    callback.error = { [weak callback] (code: String, message: String?, details: Any?) in

                        print(code + ", " + (message ?? ""))

                        callback?.defaultBehaviour(nil)

                    }

                    channelDelegate?.onPrintRequest(url: url, printJobId: printJobId, callback: callback)

                }

                return

            }

            

            let _callHandlerID = body["_callHandlerID"] as? Int64 ?? 0

            let args = body["args"] as? String ?? ""

            

            let _windowId = body["_windowId"] as? Int64

            var webView = self

            if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] {

                webView = webViewTransport.webView

            }

            

            let callback = WebViewChannelDelegate.CallJsHandlerCallback()

            callback.defaultBehaviour = { [weak self] (response: Any?) in

                var json = "null"

                if let r = response as? String {

                    json = r

                }

                

                self?.evaluateJavaScript("""

if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) {

    window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)].resolve(\(json));

    delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)];

}

""", completionHandler: nil)

            }

            callback.error = { [weak self] (code: String, message: String?, details: Any?) in

                let errorMessage = code + (message != nil ? ", " + (message ?? "") : "")

                print(errorMessage)

                

                self?.evaluateJavaScript("""

if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) {

    window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)].reject(new Error('\(errorMessage.replacingOccurrences(of: "\'", with: "\\'"))'));

    delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)];

}

""", completionHandler: nil)

            }

            

            if let channelDelegate = webView.channelDelegate {

                channelDelegate.onCallJsHandler(handlerName: handlerName, args: args, callback: callback)

            }

        } else if message.name == "onFindResultReceived",

                  let findResult = body["findResult"] as? [String: Any],

                  let activeMatchOrdinal = findResult["activeMatchOrdinal"] as? Int,

                  let numberOfMatches = findResult["numberOfMatches"] as? Int,

                  let isDoneCounting = findResult["isDoneCounting"] as? Bool {

            

            let _windowId = body["_windowId"] as? Int64

            var webView = self

            if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] {

                webView = webViewTransport.webView

            }

            webView.findInteractionController?.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting)

            webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting)

        } else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received",

                  let resultUuid = body["resultUuid"] as? String,

                  let result = callAsyncJavaScriptBelowIOS14Results[resultUuid] {

            result([

                    "value": body["value"],

                    "error": body["error"]

            ])

            callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid)

        } else if message.name == "onWebMessagePortMessageReceived",

                  let webMessageChannelId = body["webMessageChannelId"] as? String,

                  let index = body["index"] as? Int64 {

            var webMessage: WebMessage? = nil

            if let webMessageMap = body["message"] as? [String : Any?] {

                webMessage = WebMessage.fromMap(map: webMessageMap)

            }

            

            if let webMessageChannel = webMessageChannels[webMessageChannelId] {

                webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage)

            }

        } else if message.name == "onWebMessageListenerPostMessageReceived", let jsObjectName = body["jsObjectName"] as? String {

            var webMessage: WebMessage? = nil

            if let webMessageMap = body["message"] as? [String : Any?] {

                webMessage = WebMessage.fromMap(map: webMessageMap)

            }

            

            if let webMessageListener = webMessageListeners.first(where: ({($0.jsObjectName == jsObjectName)})) {

                let isMainFrame = message.frameInfo.isMainFrame

                

                var scheme: String? = nil

                var host: String? = nil

                var port: Int? = nil

                if #available(iOS 9.0, *) {

                    let sourceOrigin = message.frameInfo.securityOrigin

                    scheme = sourceOrigin.protocol

                    host = sourceOrigin.host

                    port = sourceOrigin.port

                } else if let url = message.frameInfo.request.url {

                    scheme = url.scheme

                    host = url.host

                    port = url.port

                }

                

                if !webMessageListener.isOriginAllowed(scheme: scheme, host: host, port: port) {

                    return

                }

                

                var sourceOrigin: URL? = nil

                if let scheme = scheme, !scheme.isEmpty, let host = host, !host.isEmpty {

                    sourceOrigin = URL(string: "\(scheme)://\(host)\(port != nil && port != 0 ? ":" + String(port!) : "")")

                }

                webMessageListener.channelDelegate?.onPostMessage(message: webMessage, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame)

            }

        }

    }

    

    public func scrollTo(x: Int, y: Int, animated: Bool) {

        scrollView.setContentOffset(CGPoint(x: x, y: y), animated: animated)

    }

    

    public func scrollBy(x: Int, y: Int, animated: Bool) {

        let newX = CGFloat(x) + scrollView.contentOffset.x

        let newY = CGFloat(y) + scrollView.contentOffset.y

        scrollView.setContentOffset(CGPoint(x: newX, y: newY), animated: animated)

    }

    

    

    public func pauseTimers() {

        if !isPausedTimers {

            isPausedTimers = true

            let script = "alert();";

            self.evaluateJavaScript(script, completionHandler: nil)

        }

    }

    

    public func resumeTimers() {

        if isPausedTimers {

            if let completionHandler = isPausedTimersCompletionHandler {

                self.isPausedTimersCompletionHandler = nil

                completionHandler()

            }

            isPausedTimers = false

        }

    }

    

    public func printCurrentPage(settings: PrintJobSettings? = nil,

                                 completionHandler: UIPrintInteractionController.CompletionHandler? = nil) -> String? {

        var printJobId: String? = nil

        if let settings = settings, settings.handledByClient {

            printJobId = NSUUID().uuidString

        }

        

        let printController = UIPrintInteractionController.shared

        let printFormatter = self.viewPrintFormatter()

        if let settings = settings {

            if let margins = settings.margins {

                printFormatter.perPageContentInsets = margins

            }

            if let maximumContentHeight = settings.maximumContentHeight {

                printFormatter.maximumContentHeight = maximumContentHeight

            }

            if let maximumContentWidth = settings.maximumContentWidth {

                printFormatter.maximumContentWidth = maximumContentWidth

            }

        }

        printController.printFormatter = printFormatter

        

        printController.printInfo = UIPrintInfo(dictionary: nil)

        if let printInfo = printController.printInfo {

            printInfo.jobName = settings?.jobName ?? (title ?? url?.absoluteString ?? "") + " Document"

            if let settings = settings {

                if let orientationValue = settings.orientation,

                    let orientation = UIPrintInfo.Orientation.init(rawValue: orientationValue) {

                    printInfo.orientation = orientation

                }

                if let duplexModeValue = settings.duplexMode,

                    let duplexMode = UIPrintInfo.Duplex.init(rawValue: duplexModeValue) {

                    printInfo.duplex = duplexMode

                }

                if let outputTypeValue = settings.outputType,

                    let outputType = UIPrintInfo.OutputType.init(rawValue: outputTypeValue) {

                    printInfo.outputType = outputType

                }

            }

        }

        

        // initialize print renderer and set its formatter

        let printRenderer = CustomUIPrintPageRenderer(numberOfPage: settings?.numberOfPages,

                                                      forceRenderingQuality: settings?.forceRenderingQuality)

        printRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)

        if let settings = settings {

            if let footerHeight = settings.footerHeight {

                printRenderer.footerHeight = footerHeight

            }

            if let headerHeight = settings.headerHeight {

                printRenderer.headerHeight = headerHeight

            }

        }

        printController.printPageRenderer = printRenderer

        

        if let settings = settings {

            printController.showsNumberOfCopies = settings.showsNumberOfCopies

            printController.showsPaperSelectionForLoadedPapers = settings.showsPaperSelectionForLoadedPapers

            if #available(iOS 15.0, *) {

                printController.showsPaperOrientation = settings.showsPaperOrientation

            }

        }

        

        let animated = settings?.animated ?? true

        if let id = printJobId, let plugin = plugin {

            let printJob = PrintJobController(plugin: plugin, id: id, job: printController, settings: settings)

            plugin.printJobManager?.jobs[id] = printJob

            printJob.present(animated: animated, completionHandler: completionHandler)

        } else {

            printController.present(animated: animated, completionHandler: completionHandler)

        }

        

        return printJobId

    }

    

    public func getContentHeight() -> Int64 {

        return Int64(scrollView.contentSize.height)

    }

    

    public func getContentWidth() -> Int64 {

        return Int64(scrollView.contentSize.width)

    }

    

    public func zoomBy(zoomFactor: Float, animated: Bool) {

        let currentZoomScale = scrollView.zoomScale

        scrollView.setZoomScale(currentZoomScale * CGFloat(zoomFactor), animated: animated)

    }

    

    public func getOriginalUrl() -> URL? {

        return currentOriginalUrl

    }

    

    public func getZoomScale() -> Float {

        return Float(scrollView.zoomScale)

    }

    

    public func getSelectedText(completionHandler: @escaping (Any?, Error?) -> Void) {

        if configuration.preferences.javaScriptEnabled {

            evaluateJavaScript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, completionHandler: completionHandler)

        } else {

            completionHandler(nil, nil)

        }

    }

    

    public func getHitTestResult(completionHandler: @escaping (HitTestResult) -> Void) {

        if configuration.preferences.javaScriptEnabled, let lastTouchLocation = lastTouchPoint {

            self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastTouchLocation.x),\(lastTouchLocation.y))", completionHandler: {(value, error) in

                if error != nil {

                    print("getHitTestResult error: \(error?.localizedDescription ?? "")")

                    completionHandler(HitTestResult(type: .unknownType, extra: nil))

                } else if let value = value as? [String: Any?] {

                    let hitTestResult = HitTestResult.fromMap(map: value)!

                    completionHandler(hitTestResult)

                } else {

                    completionHandler(HitTestResult(type: .unknownType, extra: nil))

                }

            })

        } else {

            completionHandler(HitTestResult(type: .unknownType, extra: nil))

        }

    }

    

    public func requestFocusNodeHref(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) {

        if configuration.preferences.javaScriptEnabled {

            // add some delay to make it sure _lastAnchorOrImageTouched is updated

            DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {

                self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched", completionHandler: {(value, error) in

                    let lastAnchorOrImageTouched = value as? [String: Any?]

                    completionHandler(lastAnchorOrImageTouched, error)

                })

            }

        } else {

            completionHandler(nil, nil)

        }

    }

    

    public func requestImageRef(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) {

        if configuration.preferences.javaScriptEnabled {

            // add some delay to make it sure _lastImageTouched is updated

            DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {

                self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched", completionHandler: {(value, error) in

                    let lastImageTouched = value as? [String: Any?]

                    completionHandler(lastImageTouched, error)

                })

            }

        } else {

            completionHandler(nil, nil)

        }

    }

    

    public func clearFocus() {

        self.scrollView.subviews.first?.resignFirstResponder()

    }

    

    public func getCertificate() -> SslCertificate? {

        guard let scheme = url?.scheme,

              scheme == "https",

              let host = url?.host,

              let sslCertificate = InAppWebView.sslCertificatesMap[host] else {

            return nil

        }

        return sslCertificate

    }

    

    public func isSecureContext(completionHandler: @escaping (_ isSecureContext: Bool) -> Void) {

        evaluateJavascript(source: "window.isSecureContext") { (isSecureContext) in

            if let isSecureContext = isSecureContext {

                completionHandler(isSecureContext as? Bool ?? false)

                return

            }

            completionHandler(false)

        }

    }

    

    public func canScrollVertically() -> Bool {

        return scrollView.contentSize.height > self.frame.height

    }

    

    public func canScrollHorizontally() -> Bool {

        return scrollView.contentSize.width > self.frame.width

    }

    

    public func enablePullToRefresh() {

        if let pullToRefreshControl = pullToRefreshControl {

            if #available(iOS 10.0, *) {

                scrollView.refreshControl = pullToRefreshControl

            } else {

                scrollView.addSubview(pullToRefreshControl)

            }

        }

    }

    

    public func disablePullToRefresh() {

        pullToRefreshControl?.removeFromSuperview()

        if #available(iOS 10.0, *) {

            scrollView.refreshControl = nil

        }

    }

    

    public func isPullToRefreshEnabled() -> Bool {

        if #available(iOS 10.0, *) {

            return scrollView.refreshControl != nil

        } else {

            return pullToRefreshControl?.superview != nil

        }

    }

    

    public func createWebMessageChannel(completionHandler: ((WebMessageChannel?) -> Void)? = nil) -> WebMessageChannel? {

        guard let plugin = plugin else {

            completionHandler?(nil)

            return nil

        }

        let id = NSUUID().uuidString

        let webMessageChannel = WebMessageChannel(plugin: plugin, id: id)

        webMessageChannel.initJsInstance(webView: self, completionHandler: completionHandler)

        webMessageChannels[id] = webMessageChannel

        

        return webMessageChannel

    }

    

    public func postWebMessage(message: WebMessage, targetOrigin: String, completionHandler: ((Any?) -> Void)? = nil) throws {

        var portsString = "null"

        if let ports = message.ports {

            var portArrayString: [String] = []

            for port in ports {

                if port.isStarted {

                    throw NSError(domain: "Port is already started", code: 0)

                }

                if port.isClosed || port.isTransferred {

                    throw NSError(domain: "Port is already closed or transferred", code: 0)

                }

                port.isTransferred = true

                portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)")

            }

            portsString = "[" + portArrayString.joined(separator: ", ") + "]"

        }

        

        let url = URL(string: targetOrigin)?.absoluteString ?? "*"

        let source = """

        (function() {

            window.postMessage(\(message.jsData), '\(url)', \(portsString));

        })();

        """

        evaluateJavascript(source: source, completionHandler: completionHandler)

        message.dispose()

    }

    

    public func addWebMessageListener(webMessageListener: WebMessageListener) throws {

        if webMessageListeners.map({ ($0.jsObjectName) }).contains(webMessageListener.jsObjectName) {

            throw NSError(domain: "jsObjectName \(webMessageListener.jsObjectName) was already added.", code: 0)

        }

        try webMessageListener.assertOriginRulesValid()

        webMessageListener.initJsInstance(webView: self)

        webMessageListeners.append(webMessageListener)

    }

    

    public func disposeWebMessageChannels() {

        for webMessageChannel in webMessageChannels.values {

            webMessageChannel.dispose()

        }

        webMessageChannels.removeAll()

    }

    

    // https://stackoverflow.com/a/58001395/4637638

    public override var inputAccessoryView: UIView? {

        return settings?.disableInputAccessoryView ?? false ? nil : super.inputAccessoryView

    }

    

    public func runWindowBeforeCreatedCallbacks() {

        let callbacks = windowBeforeCreatedCallbacks

        callbacks.forEach { (callback) in

            callback()

        }

        windowBeforeCreatedCallbacks.removeAll()

    }

    

    public func dispose() {

        channelDelegate?.dispose()

        channelDelegate = nil

        runWindowBeforeCreatedCallbacks()

        removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))

        removeObserver(self, forKeyPath: #keyPath(WKWebView.url))

        removeObserver(self, forKeyPath: #keyPath(WKWebView.title))

        if #available(iOS 15.0, *) {

            removeObserver(self, forKeyPath: #keyPath(WKWebView.cameraCaptureState))

            removeObserver(self, forKeyPath: #keyPath(WKWebView.microphoneCaptureState))

        }

        // TODO: Still not working on iOS 16.0!

//        if #available(iOS 16.0, *) {

//            removeObserver(self, forKeyPath: #keyPath(WKWebView.fullscreenState))

//        }

        scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset))

        scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.zoomScale))

        scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize))

        resumeTimers()

        stopLoading()

        disposeWebMessageChannels()

        for webMessageListener in webMessageListeners {

            webMessageListener.dispose()

        }

        webMessageListeners.removeAll()

        interceptOnlyAsyncAjaxRequestsPluginScript = nil

        if windowId == nil {

            configuration.userContentController.removeAllPluginScriptMessageHandlers()

            configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received")

            configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived")

            configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived")

            configuration.userContentController.removeAllUserScripts()

            if #available(iOS 11.0, *) {

                configuration.userContentController.removeAllContentRuleLists()

            }

        } else if let wId = windowId, plugin?.inAppWebViewManager?.windowWebViews[wId] != nil {

            plugin?.inAppWebViewManager?.windowWebViews.removeValue(forKey: wId)

        }

        configuration.userContentController.dispose(windowId: windowId)

        NotificationCenter.default.removeObserver(self)

        for imp in customIMPs {

            imp_removeBlock(imp)

        }

        longPressRecognizer.removeTarget(self, action: #selector(longPressGestureDetected))

        longPressRecognizer.delegate = nil

        scrollView.removeGestureRecognizer(longPressRecognizer)

        recognizerForDisablingContextMenuOnLinks.removeTarget(self, action: #selector(longPressGestureDetected))

        recognizerForDisablingContextMenuOnLinks.delegate = nil

        scrollView.removeGestureRecognizer(recognizerForDisablingContextMenuOnLinks)

        panGestureRecognizer.removeTarget(self, action: #selector(endDraggingDetected))

        panGestureRecognizer.delegate = nil

        scrollView.removeGestureRecognizer(panGestureRecognizer)

        disablePullToRefresh()

        pullToRefreshControl?.dispose()

        pullToRefreshControl = nil

        findInteractionController?.dispose()

        findInteractionController = nil

        uiDelegate = nil

        navigationDelegate = nil

        scrollView.delegate = nil

        isPausedTimersCompletionHandler = nil

        SharedLastTouchPointTimestamp.removeValue(forKey: self)

        callAsyncJavaScriptBelowIOS14Results.removeAll()

        plugin = nil

    }

    

    deinit {

        debugPrint("InAppWebView - dealloc")

    }

}