diff --git a/.metadata b/.metadata index 14879c61..b0199f3a 100644 --- a/.metadata +++ b/.metadata @@ -1,10 +1,39 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: c860cba910319332564e1e9d470a17074c1f2dfd + revision: 18a827f3933c19f51862dde3fa472197683249d6 channel: stable project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: android + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: ios + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: macos + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: web + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/lib/in_app_browser_example.screen.dart b/example/lib/in_app_browser_example.screen.dart index 37e87165..33c3a3a5 100755 --- a/example/lib/in_app_browser_example.screen.dart +++ b/example/lib/in_app_browser_example.screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -18,9 +19,7 @@ class MyInAppBrowser extends InAppBrowser { } @override - Future onLoadStart(url) async { - - } + Future onLoadStart(url) async {} @override Future onLoadStop(url) async { @@ -61,26 +60,30 @@ class InAppBrowserExampleScreen extends StatefulWidget { } class _InAppBrowserExampleScreenState extends State { - late PullToRefreshController pullToRefreshController; + PullToRefreshController? pullToRefreshController; @override void initState() { super.initState(); - pullToRefreshController = PullToRefreshController( - settings: PullToRefreshSettings( - color: Colors.black, - ), - onRefresh: () async { - if (Platform.isAndroid) { - widget.browser.webViewController.reload(); - } else if (Platform.isIOS) { - widget.browser.webViewController.loadUrl( - urlRequest: URLRequest( - url: await widget.browser.webViewController.getUrl())); - } - }, - ); + pullToRefreshController = kIsWeb || + ![TargetPlatform.iOS, TargetPlatform.android] + .contains(defaultTargetPlatform) + ? null + : PullToRefreshController( + settings: PullToRefreshSettings( + color: Colors.black, + ), + onRefresh: () async { + if (Platform.isAndroid) { + widget.browser.webViewController.reload(); + } else if (Platform.isIOS) { + widget.browser.webViewController.loadUrl( + urlRequest: URLRequest( + url: await widget.browser.webViewController.getUrl())); + } + }, + ); widget.browser.pullToRefreshController = pullToRefreshController; } @@ -103,6 +106,7 @@ class _InAppBrowserExampleScreenState extends State { URLRequest(url: Uri.parse("https://flutter.dev")), settings: InAppBrowserClassSettings( browserSettings: InAppBrowserSettings( + hidden: false, toolbarTopBackgroundColor: Colors.blue, presentationStyle: ModalPresentationStyle.POPOVER ), diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index 10586b29..ee37d50c 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -63,7 +63,7 @@ class _InAppWebViewExampleScreenState extends State { contextMenuItemClicked.title); }); - pullToRefreshController = kIsWeb + pullToRefreshController = kIsWeb || ![TargetPlatform.iOS, TargetPlatform.android].contains(defaultTargetPlatform) ? null : PullToRefreshController( settings: PullToRefreshSettings( diff --git a/example/lib/main.dart b/example/lib/main.dart index 8c7a1612..24450208 100755 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -33,26 +33,65 @@ Future main() async { } PointerInterceptor myDrawer({required BuildContext context}) { - final children = [ + var children = [ ListTile( title: Text('InAppWebView'), onTap: () { Navigator.pushReplacementNamed(context, '/'); }, - ) + ), + ListTile( + title: Text('InAppBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/InAppBrowser'); + }, + ), + ListTile( + title: Text('ChromeSafariBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/ChromeSafariBrowser'); + }, + ), + ListTile( + title: Text('WebAuthenticationSession'), + onTap: () { + Navigator.pushReplacementNamed(context, '/WebAuthenticationSession'); + }, + ), + ListTile( + title: Text('HeadlessInAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/HeadlessInAppWebView'); + }, + ), ]; - if (!kIsWeb) { - children.addAll([ + if (kIsWeb) { + children = [ + ListTile( + title: Text('InAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ) + ]; + } else if (defaultTargetPlatform == TargetPlatform.macOS) { + children = [ + // ListTile( + // title: Text('InAppWebView'), + // onTap: () { + // Navigator.pushReplacementNamed(context, '/'); + // }, + // ), + // ListTile( + // title: Text('InAppBrowser'), + // onTap: () { + // Navigator.pushReplacementNamed(context, '/InAppBrowser'); + // }, + // ), ListTile( title: Text('InAppBrowser'), onTap: () { - Navigator.pushReplacementNamed(context, '/InAppBrowser'); - }, - ), - ListTile( - title: Text('ChromeSafariBrowser'), - onTap: () { - Navigator.pushReplacementNamed(context, '/ChromeSafariBrowser'); + Navigator.pushReplacementNamed(context, '/'); }, ), ListTile( @@ -67,7 +106,7 @@ PointerInterceptor myDrawer({required BuildContext context}) { Navigator.pushReplacementNamed(context, '/HeadlessInAppWebView'); }, ), - ]); + ]; } return PointerInterceptor( child: Drawer( @@ -110,6 +149,15 @@ class _MyAppState extends State { '/': (context) => InAppWebViewExampleScreen(), }); } + if (defaultTargetPlatform == TargetPlatform.macOS) { + return MaterialApp(initialRoute: '/', routes: { + // '/': (context) => InAppWebViewExampleScreen(), + // '/InAppBrowser': (context) => InAppBrowserExampleScreen(), + '/': (context) => InAppBrowserExampleScreen(), + '/HeadlessInAppWebView': (context) => HeadlessInAppWebViewExampleScreen(), + '/WebAuthenticationSession': (context) => WebAuthenticationSessionExampleScreen(), + }); + } return MaterialApp(initialRoute: '/', routes: { '/': (context) => InAppWebViewExampleScreen(), '/InAppBrowser': (context) => InAppBrowserExampleScreen(), diff --git a/example/lib/web_authentication_session_example.screen.dart b/example/lib/web_authentication_session_example.screen.dart index aaaec20b..541e5cc1 100755 --- a/example/lib/web_authentication_session_example.screen.dart +++ b/example/lib/web_authentication_session_example.screen.dart @@ -48,7 +48,8 @@ class _WebAuthenticationSessionExampleScreenState onPressed: () async { if (session == null && !kIsWeb && - defaultTargetPlatform == TargetPlatform.iOS && + [TargetPlatform.iOS, TargetPlatform.macOS] + .contains(defaultTargetPlatform) && await WebAuthenticationSession.isAvailable()) { session = await WebAuthenticationSession.create( url: Uri.parse( diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..5d8a06dd --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_inappwebview +import path_provider_macos +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 00000000..dade8dfa --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f9c8aaaf --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 635B80423A5381B66E47F216 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3C8BEF4D338BF1EDD832305 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_inappwebview_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_inappwebview_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7D4269DF6E938B573DA3AB1B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A3C8BEF4D338BF1EDD832305 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AC8274DF424C630859617712 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + B44881B5FC807BDF77BD81E9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 635B80423A5381B66E47F216 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + DE8EF0F1212CA8BCD731BA0F /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_inappwebview_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A3C8BEF4D338BF1EDD832305 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + DE8EF0F1212CA8BCD731BA0F /* Pods */ = { + isa = PBXGroup; + children = ( + AC8274DF424C630859617712 /* Pods-Runner.debug.xcconfig */, + 7D4269DF6E938B573DA3AB1B /* Pods-Runner.release.xcconfig */, + B44881B5FC807BDF77BD81E9 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 2233BC408EAD04A8B5721F92 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 7E9999C83038C5D12ED26B20 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_inappwebview_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2233BC408EAD04A8B5721F92 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7E9999C83038C5D12ED26B20 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..958f17d6 --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..2165dd02 --- /dev/null +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_inappwebview_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.pichillilorenzo.flutterInappwebviewExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.pichillilorenzo. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..f7ba7c9b --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.print + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..8ed2e121 --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.print + + + diff --git a/ios/Classes/InAppBrowser/InAppBrowserManager.swift b/ios/Classes/InAppBrowser/InAppBrowserManager.swift index 8d77a76a..233686be 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserManager.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserManager.swift @@ -110,7 +110,7 @@ public class InAppBrowserManager: ChannelDelegate { navController.tmpWindow = tmpWindow var animated = true - if let browserOptions = webViewController.browserSettings, browserOptions.hidden { + if let browserSettings = webViewController.browserSettings, browserSettings.hidden { tmpWindow.isHidden = true UIApplication.shared.delegate?.window??.makeKeyAndVisible() animated = false diff --git a/ios/Classes/InAppBrowser/InAppBrowserSettings.swift b/ios/Classes/InAppBrowser/InAppBrowserSettings.swift index ea265651..5bd51f8f 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserSettings.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserSettings.swift @@ -36,7 +36,7 @@ public class InAppBrowserSettings: ISettings { var realOptions: [String: Any?] = toMap() if let inAppBrowserWebViewController = obj { realOptions["hideUrlBar"] = inAppBrowserWebViewController.searchBar.isHidden - realOptions["hideUrlBar"] = inAppBrowserWebViewController.progressBar.isHidden + realOptions["progressBar"] = inAppBrowserWebViewController.progressBar.isHidden realOptions["closeButtonCaption"] = inAppBrowserWebViewController.closeButton.title realOptions["closeButtonColor"] = inAppBrowserWebViewController.closeButton.tintColor?.hexString if let navController = inAppBrowserWebViewController.navigationController { diff --git a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift index 2abbc95d..536eb049 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -254,7 +254,7 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega navigationController?.navigationBar.barTintColor = UIColor(hexString: barTintColor) } if let tintColor = browserOptions.toolbarTopTintColor, !tintColor.isEmpty { - navigationController?.navigationBar.barTintColor = UIColor(hexString: tintColor) + navigationController?.navigationBar.tintColor = UIColor(hexString: tintColor) } navigationController?.navigationBar.isTranslucent = browserOptions.toolbarTopTranslucent } diff --git a/lib/src/in_app_webview/in_app_webview.dart b/lib/src/in_app_webview/in_app_webview.dart index fc0ff3b4..f60ea6ec 100755 --- a/lib/src/in_app_webview/in_app_webview.dart +++ b/lib/src/in_app_webview/in_app_webview.dart @@ -688,7 +688,7 @@ class _InAppWebViewState extends State { creationParamsCodec: const StandardMessageCodec(), ); } - } else if (defaultTargetPlatform == TargetPlatform.iOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS/* || defaultTargetPlatform == TargetPlatform.macOS*/) { return UiKitView( viewType: 'com.pichillilorenzo/flutter_inappwebview', onPlatformViewCreated: _onPlatformViewCreated, diff --git a/lib/src/types/print_job_color_mode.dart b/lib/src/types/print_job_color_mode.dart index adbf583f..7edcf5a9 100644 --- a/lib/src/types/print_job_color_mode.dart +++ b/lib/src/types/print_job_color_mode.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; import '../print_job/main.dart'; @@ -9,11 +10,17 @@ part 'print_job_color_mode.g.dart'; class PrintJobColorMode_ { // ignore: unused_field final int _value; + // ignore: unused_field + final dynamic _nativeValue = null; const PrintJobColorMode_._internal(this._value); ///Monochrome color scheme, for example one color is used. + @EnumSupportedPlatforms( + platforms: [EnumAndroidPlatform(value: 1), EnumMacOSPlatform(value: "Gray")]) static const MONOCHROME = const PrintJobColorMode_._internal(1); ///Color color scheme, for example many colors are used. + @EnumSupportedPlatforms( + platforms: [EnumAndroidPlatform(value: 1), EnumMacOSPlatform(value: "RGB")]) static const COLOR = const PrintJobColorMode_._internal(2); } diff --git a/lib/src/types/print_job_color_mode.g.dart b/lib/src/types/print_job_color_mode.g.dart index e6a7212e..83fb130e 100644 --- a/lib/src/types/print_job_color_mode.g.dart +++ b/lib/src/types/print_job_color_mode.g.dart @@ -9,7 +9,7 @@ part of 'print_job_color_mode.dart'; ///Class representing how the printed content of a [PrintJobController] should be laid out. class PrintJobColorMode { final int _value; - final int _nativeValue; + final dynamic _nativeValue; const PrintJobColorMode._internal(this._value, this._nativeValue); // ignore: unused_element factory PrintJobColorMode._internalMultiPlatform( @@ -17,10 +17,38 @@ class PrintJobColorMode { PrintJobColorMode._internal(value, nativeValue()); ///Monochrome color scheme, for example one color is used. - static const MONOCHROME = PrintJobColorMode._internal(1, 1); + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- MacOS + static final MONOCHROME = PrintJobColorMode._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 1; + case TargetPlatform.macOS: + return 'Gray'; + default: + break; + } + return null; + }); ///Color color scheme, for example many colors are used. - static const COLOR = PrintJobColorMode._internal(2, 2); + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- MacOS + static final COLOR = PrintJobColorMode._internalMultiPlatform(2, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 1; + case TargetPlatform.macOS: + return 'RGB'; + default: + break; + } + return null; + }); ///Set of all values of [PrintJobColorMode]. static final Set values = [ @@ -42,7 +70,7 @@ class PrintJobColorMode { } ///Gets a possible [PrintJobColorMode] instance from a native value. - static PrintJobColorMode? fromNativeValue(int? value) { + static PrintJobColorMode? fromNativeValue(dynamic value) { if (value != null) { try { return PrintJobColorMode.values @@ -57,8 +85,8 @@ class PrintJobColorMode { ///Gets [int] value. int toValue() => _value; - ///Gets [int] native value. - int toNativeValue() => _nativeValue; + ///Gets [dynamic] native value. + dynamic toNativeValue() => _nativeValue; @override int get hashCode => _value.hashCode; diff --git a/lib/src/types/print_job_duplex_mode.dart b/lib/src/types/print_job_duplex_mode.dart index 08994d93..724c157b 100644 --- a/lib/src/types/print_job_duplex_mode.dart +++ b/lib/src/types/print_job_duplex_mode.dart @@ -17,18 +17,18 @@ class PrintJobDuplexMode_ { ///No double-sided (duplex) printing; single-sided printing only. @EnumSupportedPlatforms( - platforms: [EnumAndroidPlatform(value: 1), EnumIOSPlatform(value: 0)]) + platforms: [EnumAndroidPlatform(value: 1), EnumIOSPlatform(value: 0), EnumMacOSPlatform(value: 1)]) static const NONE = PrintJobDuplexMode_._internal('NONE'); ///Duplex printing that flips the back page along the long edge of the paper. ///Pages are turned sideways along the long edge - like a book. @EnumSupportedPlatforms( - platforms: [EnumAndroidPlatform(value: 2), EnumIOSPlatform(value: 1)]) + platforms: [EnumAndroidPlatform(value: 2), EnumIOSPlatform(value: 1), EnumMacOSPlatform(value: 2)]) static const LONG_EDGE = PrintJobDuplexMode_._internal('LONG_EDGE'); ///Duplex print that flips the back page along the short edge of the paper. ///Pages are turned upwards along the short edge - like a notepad. @EnumSupportedPlatforms( - platforms: [EnumAndroidPlatform(value: 4), EnumIOSPlatform(value: 2)]) + platforms: [EnumAndroidPlatform(value: 4), EnumIOSPlatform(value: 2), EnumMacOSPlatform(value: 3)]) static const SHORT_EDGE = PrintJobDuplexMode_._internal('SHORT_EDGE'); } diff --git a/lib/src/types/print_job_duplex_mode.g.dart b/lib/src/types/print_job_duplex_mode.g.dart index 043899a5..6202c46b 100644 --- a/lib/src/types/print_job_duplex_mode.g.dart +++ b/lib/src/types/print_job_duplex_mode.g.dart @@ -21,12 +21,15 @@ class PrintJobDuplexMode { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static final NONE = PrintJobDuplexMode._internalMultiPlatform('NONE', () { switch (defaultTargetPlatform) { case TargetPlatform.android: return 1; case TargetPlatform.iOS: return 0; + case TargetPlatform.macOS: + return 1; default: break; } @@ -39,6 +42,7 @@ class PrintJobDuplexMode { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static final LONG_EDGE = PrintJobDuplexMode._internalMultiPlatform('LONG_EDGE', () { switch (defaultTargetPlatform) { @@ -46,6 +50,8 @@ class PrintJobDuplexMode { return 2; case TargetPlatform.iOS: return 1; + case TargetPlatform.macOS: + return 2; default: break; } @@ -58,6 +64,7 @@ class PrintJobDuplexMode { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static final SHORT_EDGE = PrintJobDuplexMode._internalMultiPlatform('SHORT_EDGE', () { switch (defaultTargetPlatform) { @@ -65,6 +72,8 @@ class PrintJobDuplexMode { return 4; case TargetPlatform.iOS: return 2; + case TargetPlatform.macOS: + return 3; default: break; } diff --git a/lib/src/types/print_job_orientation.dart b/lib/src/types/print_job_orientation.dart index 0a754d05..0694737d 100644 --- a/lib/src/types/print_job_orientation.dart +++ b/lib/src/types/print_job_orientation.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; import '../print_job/main.dart'; @@ -12,8 +13,24 @@ class PrintJobOrientation_ { const PrintJobOrientation_._internal(this._value); ///Pages are printed in portrait orientation. + @EnumSupportedPlatforms(platforms: [ + EnumIOSPlatform( + value: 0 + ), + EnumMacOSPlatform( + value: 0 + ) + ]) static const PORTRAIT = const PrintJobOrientation_._internal(0); ///Pages are printed in landscape orientation. + @EnumSupportedPlatforms(platforms: [ + EnumIOSPlatform( + value: 1 + ), + EnumMacOSPlatform( + value: 1 + ) + ]) static const LANDSCAPE = const PrintJobOrientation_._internal(1); } diff --git a/lib/src/types/print_job_orientation.g.dart b/lib/src/types/print_job_orientation.g.dart index c7207c28..3c900d75 100644 --- a/lib/src/types/print_job_orientation.g.dart +++ b/lib/src/types/print_job_orientation.g.dart @@ -17,10 +17,38 @@ class PrintJobOrientation { PrintJobOrientation._internal(value, nativeValue()); ///Pages are printed in portrait orientation. - static const PORTRAIT = PrintJobOrientation._internal(0, 0); + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS + static final PORTRAIT = PrintJobOrientation._internalMultiPlatform(0, () { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 0; + case TargetPlatform.macOS: + return 0; + default: + break; + } + return null; + }); ///Pages are printed in landscape orientation. - static const LANDSCAPE = PrintJobOrientation._internal(1, 1); + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS + static final LANDSCAPE = PrintJobOrientation._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 1; + case TargetPlatform.macOS: + return 1; + default: + break; + } + return null; + }); ///Set of all values of [PrintJobOrientation]. static final Set values = [ diff --git a/lib/src/types/ssl_certificate.dart b/lib/src/types/ssl_certificate.dart index c045617c..ada6b5af 100644 --- a/lib/src/types/ssl_certificate.dart +++ b/lib/src/types/ssl_certificate.dart @@ -85,10 +85,12 @@ class SslCertificate_ { SslCertificateDName.fromMap(map["issuedBy"]?.cast()), issuedTo: SslCertificateDName.fromMap(map["issuedTo"]?.cast()), - validNotAfterDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]), - validNotBeforeDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]), + validNotAfterDate: map["validNotAfterDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]) + : null, + validNotBeforeDate: map["validNotBeforeDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]) + : null, x509Certificate: x509Certificate, ); } diff --git a/lib/src/types/ssl_certificate.g.dart b/lib/src/types/ssl_certificate.g.dart index 4d62ab25..0702d486 100644 --- a/lib/src/types/ssl_certificate.g.dart +++ b/lib/src/types/ssl_certificate.g.dart @@ -75,10 +75,12 @@ class SslCertificate { map["issuedBy"]?.cast()), issuedTo: SslCertificateDName.fromMap( map["issuedTo"]?.cast()), - validNotAfterDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]), - validNotBeforeDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]), + validNotAfterDate: map["validNotAfterDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]) + : null, + validNotBeforeDate: map["validNotBeforeDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]) + : null, x509Certificate: x509Certificate); } diff --git a/macos/Classes/CredentialDatabase.swift b/macos/Classes/CredentialDatabase.swift new file mode 100755 index 00000000..ff555380 --- /dev/null +++ b/macos/Classes/CredentialDatabase.swift @@ -0,0 +1,201 @@ +// +// CredentialDatabase.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 29/10/2019. +// + +import Foundation +import FlutterMacOS + +public class CredentialDatabase: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_credential_database" + static var registrar: FlutterPluginRegistrar? + static var credentialStore: URLCredentialStorage? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: CredentialDatabase.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + CredentialDatabase.registrar = registrar + CredentialDatabase.credentialStore = URLCredentialStorage.shared + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "getAllAuthCredentials": + var allCredentials: [[String: Any?]] = [] + guard let credentialStore = CredentialDatabase.credentialStore else { + result(allCredentials) + return + } + for (protectionSpace, credentials) in credentialStore.allCredentials { + var crendentials: [[String: Any?]] = [] + for c in credentials { + let credential: [String: Any?] = c.value.toMap() + crendentials.append(credential) + } + if crendentials.count > 0 { + let dict: [String : Any] = [ + "protectionSpace": protectionSpace.toMap(), + "credentials": crendentials + ] + allCredentials.append(dict) + } + } + result(allCredentials) + break + case "getHttpAuthCredentials": + var crendentials: [[String: Any?]] = [] + guard let credentialStore = CredentialDatabase.credentialStore else { + result(crendentials) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + + for (protectionSpace, credentials) in credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + crendentials.append(c.value.toMap()) + } + break + } + } + result(crendentials) + break + case "setHttpAuthCredential": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + let credential = URLCredential(user: username, password: password, persistence: .permanent) + credentialStore.set(credential, + for: URLProtectionSpace(host: host, port: urlPort, protocol: urlProtocol, + realm: realm, authenticationMethod: NSURLAuthenticationMethodHTTPBasic)) + result(true) + break + case "removeHttpAuthCredential": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + + var credential: URLCredential? = nil; + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + if c.value.user == username, c.value.password == password { + credential = c.value + protectionSpaceCredential = protectionSpace + break + } + } + } + if credential != nil { + break + } + } + + if let c = credential, let protectionSpace = protectionSpaceCredential { + credentialStore.remove(c, for: protectionSpace) + } + + result(true) + break + case "removeHttpAuthCredentials": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + + var credentialsToRemove: [URLCredential] = []; + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + protectionSpaceCredential = protectionSpace + for c in credentials { + if let _ = c.value.user, let _ = c.value.password { + credentialsToRemove.append(c.value) + } + } + break + } + } + + if let protectionSpace = protectionSpaceCredential { + for credential in credentialsToRemove { + credentialStore.remove(credential, for: protectionSpace) + } + } + + result(true) + break + case "clearAllAuthCredentials": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + for (protectionSpace, credentials) in credentialStore.allCredentials { + for credential in credentials { + credentialStore.remove(credential.value, for: protectionSpace) + } + } + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public override func dispose() { + super.dispose() + CredentialDatabase.registrar = nil + CredentialDatabase.credentialStore = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift new file mode 100644 index 00000000..7e60a173 --- /dev/null +++ b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift @@ -0,0 +1,68 @@ +// +// HeadlessInAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation +import FlutterMacOS + +public class HeadlessInAppWebView : Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_headless_inappwebview_" + var id: String + var channelDelegate: HeadlessWebViewChannelDelegate? + var flutterWebView: FlutterWebViewController? + + public init(id: String, flutterWebView: FlutterWebViewController) { + self.id = id + self.flutterWebView = flutterWebView + let channel = FlutterMethodChannel(name: HeadlessInAppWebView.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = HeadlessWebViewChannelDelegate(headlessWebView: self, channel: channel) + } + + public func onWebViewCreated() { + channelDelegate?.onWebViewCreated(); + } + + public func prepare(params: NSDictionary) { + if let view = flutterWebView?.view() { + view.alphaValue = 0.01 + let initialSize = params["initialSize"] as? [String: Any?] + if let size = Size2D.fromMap(map: initialSize) { + setSize(size: size) + } else { + view.frame = CGRect(x: 0.0, y: 0.0, width: NSApplication.shared.mainWindow?.contentView?.bounds.width ?? 0.0, + height: NSApplication.shared.mainWindow?.contentView?.bounds.height ?? 0.0) + } + } + } + + public func setSize(size: Size2D) { + if let view = flutterWebView?.view() { + let width = size.width == -1.0 ? NSApplication.shared.mainWindow?.contentView?.bounds.width ?? 0.0 : CGFloat(size.width) + let height = size.height == -1.0 ? NSApplication.shared.mainWindow?.contentView?.bounds.height ?? 0.0 : CGFloat(size.height) + view.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height) + } + } + + public func getSize() -> Size2D? { + if let view = flutterWebView?.view() { + return Size2D(width: Double(view.frame.width), height: Double(view.frame.height)) + } + return nil + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + HeadlessInAppWebViewManager.webViews[id] = nil + flutterWebView = nil + } + + deinit { + debugPrint("HeadlessInAppWebView - dealloc") + dispose() + } +} diff --git a/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift new file mode 100644 index 00000000..92354e8b --- /dev/null +++ b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift @@ -0,0 +1,68 @@ +// +// HeadlessInAppWebViewManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/2020. +// + +import Foundation + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation + +public class HeadlessInAppWebViewManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_headless_inappwebview" + static var registrar: FlutterPluginRegistrar? + static var webViews: [String: HeadlessInAppWebView?] = [:] + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: HeadlessInAppWebViewManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + HeadlessInAppWebViewManager.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let id: String = arguments!["id"] as! String + + switch call.method { + case "run": + let params = arguments!["params"] as! [String: Any?] + HeadlessInAppWebViewManager.run(id: id, params: params) + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func run(id: String, params: [String: Any?]) { + let flutterWebView = FlutterWebViewController(registrar: HeadlessInAppWebViewManager.registrar!, + withFrame: CGRect.zero, + viewIdentifier: id, + params: params as NSDictionary) + let headlessInAppWebView = HeadlessInAppWebView(id: id, flutterWebView: flutterWebView) + HeadlessInAppWebViewManager.webViews[id] = headlessInAppWebView + + headlessInAppWebView.prepare(params: params as NSDictionary) + headlessInAppWebView.onWebViewCreated() + flutterWebView.makeInitialLoad(params: params as NSDictionary) + } + + public override func dispose() { + super.dispose() + HeadlessInAppWebViewManager.registrar = nil + let headlessWebViews = HeadlessInAppWebViewManager.webViews.values + headlessWebViews.forEach { (headlessWebView: HeadlessInAppWebView?) in + headlessWebView?.dispose() + } + HeadlessInAppWebViewManager.webViews.removeAll() + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift b/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift new file mode 100644 index 00000000..1e6dc9c7 --- /dev/null +++ b/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift @@ -0,0 +1,64 @@ +// +// HeadlessWebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation +import FlutterMacOS + +public class HeadlessWebViewChannelDelegate : ChannelDelegate { + private weak var headlessWebView: HeadlessInAppWebView? + + public init(headlessWebView: HeadlessInAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.headlessWebView = headlessWebView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "dispose": + if let headlessWebView = headlessWebView { + headlessWebView.dispose() + result(true) + } else { + result(false) + } + break + case "setSize": + if let headlessWebView = headlessWebView { + let sizeMap = arguments!["size"] as? [String: Any?] + if let size = Size2D.fromMap(map: sizeMap) { + headlessWebView.setSize(size: size) + } + result(true) + } else { + result(false) + } + break + case "getSize": + result(headlessWebView?.getSize()?.toMap()) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onWebViewCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebViewCreated", arguments: arguments) + } + + public override func dispose() { + super.dispose() + headlessWebView = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/ISettings.swift b/macos/Classes/ISettings.swift new file mode 100755 index 00000000..3c536486 --- /dev/null +++ b/macos/Classes/ISettings.swift @@ -0,0 +1,50 @@ +// +// Options.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/09/18. +// + +import Foundation + +@objcMembers +public class ISettings: NSObject { + + override init(){ + super.init() + } + + func parse(settings: [String: Any?]) -> ISettings { + for (key, value) in settings { + if !(value is NSNull), value != nil { + if self.responds(to: Selector(key)) { + self.setValue(value, forKey: key) + } else if self.responds(to: Selector("_" + key)) { + self.setValue(value, forKey: "_" + key) + } + } + } + return self + } + + func toMap() -> [String: Any?] { + var settings: [String: Any?] = [:] + var counts = UInt32() + let properties = class_copyPropertyList(object_getClass(self), &counts) + for i in 0.. [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift b/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift new file mode 100644 index 00000000..ca9d4d99 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift @@ -0,0 +1,29 @@ +// +// InAppBrowserChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation +import FlutterMacOS + +public class InAppBrowserChannelDelegate : ChannelDelegate { + public override init(channel: FlutterMethodChannel) { + super.init(channel: channel) + } + + public func onBrowserCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onBrowserCreated", arguments: arguments) + } + + public func onExit() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExit", arguments: arguments) + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift b/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift new file mode 100644 index 00000000..ba013c58 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift @@ -0,0 +1,17 @@ +// +// InAppBrowserDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +public protocol InAppBrowserDelegate { + func didChangeTitle(title: String?) + func didStartNavigation(url: URL?) + func didUpdateVisitedHistory(url: URL?) + func didFinishNavigation(url: URL?) + func didFailNavigation(url: URL?, error: Error) + func didChangeProgress(progress: Double) +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserManager.swift b/macos/Classes/InAppBrowser/InAppBrowserManager.swift new file mode 100755 index 00000000..8811fa67 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserManager.swift @@ -0,0 +1,107 @@ +// +// InAppBrowserManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 18/12/2019. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation + +public class InAppBrowserManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappbrowser" + static let WEBVIEW_STORYBOARD = "WebView" + static let WEBVIEW_STORYBOARD_CONTROLLER_ID = "viewController" + static let NAV_STORYBOARD_CONTROLLER_ID = "navController" + static var registrar: FlutterPluginRegistrar? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: InAppBrowserManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + InAppBrowserManager.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "open": + open(arguments: arguments!) + result(true) + break + case "openWithSystemBrowser": + let url = arguments!["url"] as! String + openWithSystemBrowser(url: url, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func open(arguments: NSDictionary) { + let id = arguments["id"] as! String + let urlRequest = arguments["urlRequest"] as? [String:Any?] + let assetFilePath = arguments["assetFilePath"] as? String + let data = arguments["data"] as? String + let mimeType = arguments["mimeType"] as? String + let encoding = arguments["encoding"] as? String + let baseUrl = arguments["baseUrl"] as? String + let settings = arguments["settings"] as! [String: Any?] + let contextMenu = arguments["contextMenu"] as! [String: Any] + let windowId = arguments["windowId"] as? Int64 + let initialUserScripts = arguments["initialUserScripts"] as? [[String: Any]] + + let browserSettings = InAppBrowserSettings() + let _ = browserSettings.parse(settings: settings) + + let webViewSettings = InAppWebViewSettings() + let _ = webViewSettings.parse(settings: settings) + + let webViewController = InAppBrowserWebViewController() + webViewController.browserSettings = browserSettings + webViewController.webViewSettings = webViewSettings + + webViewController.id = id + webViewController.initialUrlRequest = urlRequest != nil ? URLRequest.init(fromPluginMap: urlRequest!) : nil + webViewController.initialFile = assetFilePath + webViewController.initialData = data + webViewController.initialMimeType = mimeType + webViewController.initialEncoding = encoding + webViewController.initialBaseUrl = baseUrl + webViewController.contextMenu = contextMenu + webViewController.windowId = windowId + webViewController.initialUserScripts = initialUserScripts ?? [] + + let window = InAppBrowserWindow(contentViewController: webViewController) + window.browserSettings = browserSettings + window.contentViewController = webViewController + window.prepare() + + NSApplication.shared.mainWindow?.addChildWindow(window, ordered: .above) + + if browserSettings.hidden { + window.hide() + } + } + + public func openWithSystemBrowser(url: String, result: @escaping FlutterResult) { + let absoluteUrl = URL(string: url)!.absoluteURL + if !NSWorkspace.shared.open(absoluteUrl) { + result(FlutterError(code: "InAppBrowserManager", message: url + " cannot be opened!", details: nil)) + return + } + result(true) + } + + public override func dispose() { + super.dispose() + InAppBrowserManager.registrar = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserSettings.swift b/macos/Classes/InAppBrowser/InAppBrowserSettings.swift new file mode 100755 index 00000000..bc482ec9 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserSettings.swift @@ -0,0 +1,45 @@ +// +// InAppBrowserOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import Foundation + +@objcMembers +public class InAppBrowserSettings: ISettings { + + var hidden = false + var hideToolbarTop = true + var toolbarTopBackgroundColor: String? + var hideUrlBar = false + var hideProgressBar = false + + var toolbarTopTranslucent = true + var toolbarTopBarTintColor: String? + var toolbarTopTintColor: String? + var hideToolbarBottom = true + var toolbarBottomBackgroundColor: String? + var toolbarBottomTintColor: String? + var toolbarBottomTranslucent = true + var closeButtonCaption: String? + var closeButtonColor: String? + var presentationStyle = 0 //fullscreen + var transitionStyle = 0 //crossDissolve + + override init(){ + super.init() + } + + override func getRealSettings(obj: InAppBrowserWebViewController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if let inAppBrowserWebViewController = obj { + realOptions["hideUrlBar"] = inAppBrowserWebViewController.window?.searchBar?.isHidden + realOptions["progressBar"] = inAppBrowserWebViewController.progressBar.isHidden + realOptions["hideToolbarTop"] = !(inAppBrowserWebViewController.window?.toolbar?.isVisible ?? true) + realOptions["toolbarTopBackgroundColor"] = inAppBrowserWebViewController.window?.backgroundColor + } + return realOptions + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift new file mode 100755 index 00000000..a0982ffa --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -0,0 +1,302 @@ +// +// InAppBrowserWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation + +public class InAppBrowserWebViewController: NSViewController, InAppBrowserDelegate, Disposable { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappbrowser_"; + + var progressBar: NSProgressIndicator! + + var window: InAppBrowserWindow? + var id: String = "" + var windowId: Int64? + var webView: InAppWebView? + var channelDelegate: InAppBrowserChannelDelegate? + var initialUrlRequest: URLRequest? + var initialFile: String? + var contextMenu: [String: Any]? + var browserSettings: InAppBrowserSettings? + var webViewSettings: InAppWebViewSettings? + var initialData: String? + var initialMimeType: String? + var initialEncoding: String? + var initialBaseUrl: String? + var initialUserScripts: [[String: Any]] = [] + + public override func loadView() { + let channel = FlutterMethodChannel(name: InAppBrowserWebViewController.METHOD_CHANNEL_NAME_PREFIX + id, binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + channelDelegate = InAppBrowserChannelDelegate(channel: channel) + + var userScripts: [UserScript] = [] + for intialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: intialUserScript, windowId: windowId)!) + } + + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: webViewSettings) + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.contextMenu = contextMenu + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: nil, + registrar: nil, + frame: .zero, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + userScripts: userScripts) + } + + guard let webView = webView else { + return + } + + webView.inAppBrowserDelegate = self + webView.id = id + webView.channelDelegate = WebViewChannelDelegate(webView: webView, channel: channel) + + prepareWebView() + webView.windowCreated = true + + progressBar = NSProgressIndicator() + progressBar.style = .bar + progressBar.isIndeterminate = false + progressBar.startAnimation(self) + + view = NSView(frame: NSApplication.shared.mainWindow?.frame ?? .zero) + view.addSubview(webView) + view.addSubview(progressBar, positioned: .above, relativeTo: webView) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + webView?.translatesAutoresizingMaskIntoConstraints = false + progressBar.translatesAutoresizingMaskIntoConstraints = false + + webView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0).isActive = true + webView?.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0).isActive = true + webView?.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + webView?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + progressBar.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -6.0).isActive = true + progressBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + progressBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView?.load(webViewTransport.request) + channelDelegate?.onBrowserCreated() + } else { + if #available(macOS 10.13, *) { + if let contentBlockers = webView?.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: String.Encoding.utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = self.webView!.configuration + configuration.userContentController.add(contentRuleList!) + + self.initLoad() + } + return + } catch { + print(error.localizedDescription) + } + } + } + + initLoad() + } + } + + public override func viewDidAppear() { + super.viewDidAppear() + window = view.window as? InAppBrowserWindow + } + + public func initLoad() { + if let initialFile = initialFile { + do { + try webView?.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData { + let baseUrl = URL(string: initialBaseUrl ?? "about:blank")! + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadData(data: initialData, mimeType: initialMimeType!, encoding: initialEncoding!, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, let url = initialUrlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadUrl(urlRequest: initialUrlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + + channelDelegate?.onBrowserCreated() + } + + public func prepareWebView() { + webView?.settings = webViewSettings + webView?.prepare() + + if let browserSettings = browserSettings { + if browserSettings.hideProgressBar { + progressBar.isHidden = true + } + } + } + + public func didChangeTitle(title: String?) { + guard let title = title else { + return + } + window?.title = title + window?.update() + } + + public func didStartNavigation(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = false + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didUpdateVisitedHistory(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didFinishNavigation(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = true + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didFailNavigation(url: URL?, error: Error) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = true + } + + public func didChangeProgress(progress: Double) { + progressBar.isHidden = false + progressBar.doubleValue = progress * 100 + if progress == 100.0 { + progressBar.isHidden = true + } + } + + @objc public func reload() { + webView?.reload() + didUpdateVisitedHistory(url: webView?.url) + } + + @objc public func goBack() { + if let webView = webView, webView.canGoBack { + webView.goBack() + } + } + + @objc public func goForward() { + if let webView = webView, webView.canGoForward { + webView.goForward() + } + } + + @objc public func goBackOrForward(steps: Int) { + webView?.goBackOrForward(steps: steps) + } + + public func setSettings(newSettings: InAppBrowserSettings, newSettingsMap: [String: Any]) { + window?.setSettings(newSettings: newSettings, newSettingsMap: newSettingsMap) + + let newInAppWebViewSettings = InAppWebViewSettings() + let _ = newInAppWebViewSettings.parse(settings: newSettingsMap) + webView?.setSettings(newSettings: newInAppWebViewSettings, newSettingsMap: newSettingsMap) + + if newSettingsMap["hideProgressBar"] != nil, browserSettings?.hideProgressBar != newSettings.hideProgressBar { + progressBar.isHidden = newSettings.hideProgressBar + } + + browserSettings = newSettings + webViewSettings = newInAppWebViewSettings + } + + public func getSettings() -> [String: Any?]? { + let webViewSettingsMap = webView?.getSettings() + if (self.browserSettings == nil || webViewSettingsMap == nil) { + return nil + } + var settingsMap = self.browserSettings!.getRealSettings(obj: self) + settingsMap.merge(webViewSettingsMap!, uniquingKeysWith: { (current, _) in current }) + return settingsMap + } + + public func hide() { + window?.hide() + } + + public func show() { + window?.show() + } + + public func close() { + window?.close() + } + + public func dispose() { + channelDelegate?.onExit() + channelDelegate?.dispose() + channelDelegate = nil + webView?.dispose() + webView = nil + window = nil + } + + deinit { + debugPrint("InAppBrowserWebViewController - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserWindow.swift b/macos/Classes/InAppBrowser/InAppBrowserWindow.swift new file mode 100644 index 00000000..61baf46a --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserWindow.swift @@ -0,0 +1,276 @@ +// +// InAppBrowserNavigationController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +struct ToolbarIdentifiers { + static let searchBar = NSToolbarItem.Identifier(rawValue: "SearchBar") + static let backButton = NSToolbarItem.Identifier(rawValue: "BackButton") + static let forwardButton = NSToolbarItem.Identifier(rawValue: "ForwardButton") + static let reloadButton = NSToolbarItem.Identifier(rawValue: "ReloadButton") +} + +public class InAppBrowserWindow : NSWindow, NSWindowDelegate, NSToolbarDelegate, NSSearchFieldDelegate { + var searchItem: NSToolbarItem? + var backItem: NSToolbarItem? + var forwardItem: NSToolbarItem? + var reloadItem: NSToolbarItem? + + var reloadButton: NSButton? { + get { + return reloadItem?.view as? NSButton + } + } + var backButton: NSButton? { + get { + return backItem?.view as? NSButton + } + } + var forwardButton: NSButton? { + get { + return forwardItem?.view as? NSButton + } + } + var searchBar: NSSearchField? { + get { + if #available(macOS 11.0, *), let searchItem = searchItem as? NSSearchToolbarItem { + return searchItem.searchField + } else { + return searchItem?.view as? NSSearchField + } + } + } + + var browserSettings: InAppBrowserSettings? + + public func prepare() { + title = "" + delegate = self + + if #available(macOS 10.13, *) { + let windowToolbar = NSToolbar() + windowToolbar.delegate = self + if #available(macOS 11.0, *) { + searchItem = NSSearchToolbarItem(itemIdentifier: ToolbarIdentifiers.searchBar) + (searchItem as! NSSearchToolbarItem).searchField.delegate = self + toolbarStyle = .expanded + } else { + searchItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.searchBar) + let textField = NSSearchField() + textField.usesSingleLineMode = true + textField.delegate = self + searchItem?.view = textField + } + searchItem?.label = "" + windowToolbar.displayMode = .default + + backItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.backButton) + backItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + backItem?.view = NSButton(image: NSImage(systemSymbolName: "chevron.left", + accessibilityDescription: "Go Back")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.goBack)) + } else { + backItem?.view = NSButton(title: "\u{2039}", + target: webViewController, + action: #selector(InAppBrowserWebViewController.goBack)) + } + } + + forwardItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.forwardButton) + forwardItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + forwardItem?.view = NSButton(image: NSImage(systemSymbolName: "chevron.right", + accessibilityDescription: "Go Forward")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.goForward)) + } else { + forwardItem?.view = NSButton(title: "\u{203A}", + target: webViewController, + action: #selector(InAppBrowserWebViewController.goForward)) + } + } + + reloadItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.reloadButton) + reloadItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + reloadItem?.view = NSButton(image: NSImage(systemSymbolName: "arrow.counterclockwise", + accessibilityDescription: "Reload")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.reload)) + } else { + reloadItem?.view = NSButton(title: "Reload", + target: webViewController, + action: #selector(InAppBrowserWebViewController.reload)) + } + } + + if #available(macOS 10.14, *) { + windowToolbar.centeredItemIdentifier = ToolbarIdentifiers.searchBar + } + toolbar = windowToolbar + } + + forwardButton?.isEnabled = false + backButton?.isEnabled = false + + if let browserSettings = browserSettings { + if !browserSettings.hideToolbarTop { + toolbar?.isVisible = true + if browserSettings.hideUrlBar { + if #available(macOS 11.0, *) { + (searchItem as! NSSearchToolbarItem).searchField.isHidden = true + } else { + searchItem?.view?.isHidden = true + } + } + if let bgColor = browserSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + backgroundColor = NSColor(hexString: bgColor) + } + } + else { + toolbar?.isVisible = false + } + } + } + + public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [ ToolbarIdentifiers.searchBar, + ToolbarIdentifiers.backButton, + ToolbarIdentifiers.forwardButton, + ToolbarIdentifiers.reloadButton, + .flexibleSpace ] + } + + public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.flexibleSpace, + ToolbarIdentifiers.searchBar, + .flexibleSpace, + ToolbarIdentifiers.reloadButton, + ToolbarIdentifiers.backButton, + ToolbarIdentifiers.forwardButton] + } + + public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + switch(itemIdentifier) { + case ToolbarIdentifiers.searchBar: + return searchItem + case ToolbarIdentifiers.backButton: + return backItem + case ToolbarIdentifiers.forwardButton: + return forwardItem + case ToolbarIdentifiers.reloadButton: + return reloadItem + default: + return nil + } + } + + public func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if (commandSelector == #selector(NSResponder.insertNewline(_:))) { + // ENTER key + var searchField: NSSearchField? = nil + if #available(macOS 11.0, *), let searchBar = searchItem as? NSSearchToolbarItem { + searchField = searchBar.searchField + } else if let searchBar = searchItem { + searchField = searchBar.view as? NSSearchField + } + + guard let searchField, + let urlEncoded = searchField.stringValue.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: urlEncoded) else { + return false + } + + let request = URLRequest(url: url) + (contentViewController as? InAppBrowserWebViewController)?.webView?.load(request) + + return true + } + + return false + } + + public func hide() { + orderOut(self) + + } + + public func show() { + if !(NSApplication.shared.mainWindow?.childWindows?.contains(self) ?? false) { + NSApplication.shared.mainWindow?.addChildWindow(self, ordered: .above) + } else { + orderFront(self) + } + NSApplication.shared.activate(ignoringOtherApps: true) + } + + public func setSettings(newSettings: InAppBrowserSettings, newSettingsMap: [String: Any]) { + if newSettingsMap["hidden"] != nil, browserSettings?.hidden != newSettings.hidden { + if newSettings.hidden { + hide() + } + else { + show() + } + } + + if newSettingsMap["hideUrlBar"] != nil, browserSettings?.hideUrlBar != newSettings.hideUrlBar { + searchBar?.isHidden = newSettings.hideUrlBar + } + + if newSettingsMap["hideToolbarTop"] != nil, browserSettings?.hideToolbarTop != newSettings.hideToolbarTop { + toolbar?.isVisible = !newSettings.hideToolbarTop + } + + if newSettingsMap["toolbarTopBackgroundColor"] != nil, browserSettings?.toolbarTopBackgroundColor != newSettings.toolbarTopBackgroundColor { + if let bgColor = newSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + backgroundColor = NSColor(hexString: bgColor) + } else { + backgroundColor = nil + } + } + + browserSettings = newSettings + } + + public func windowWillClose(_ notification: Notification) { + dispose() + } + + public func dispose() { + delegate = nil + if let webViewController = contentViewController as? InAppBrowserWebViewController { + webViewController.dispose() + } + if #available(macOS 11.0, *) { + (searchItem as? NSSearchToolbarItem)?.searchField.delegate = nil + } else { + (searchItem?.view as? NSTextField)?.delegate = nil + searchItem?.view = nil + } + searchItem = nil + (backItem?.view as? NSButton)?.target = nil + backItem?.view = nil + backItem = nil + (forwardItem?.view as? NSButton)?.target = nil + forwardItem?.view = nil + forwardItem = nil + (reloadItem?.view as? NSButton)?.target = nil + reloadItem?.view = nil + reloadItem = nil + } + + deinit { + debugPrint("InAppBrowserWindow - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/ContextMenuSettings.swift b/macos/Classes/InAppWebView/ContextMenuSettings.swift new file mode 100644 index 00000000..a1df58d9 --- /dev/null +++ b/macos/Classes/InAppWebView/ContextMenuSettings.swift @@ -0,0 +1,17 @@ +// +// ContextMenuOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 30/05/2020. +// + +import Foundation + +public class ContextMenuSettings: ISettings { + + var hideDefaultSystemContextMenuItems = false; + + override init(){ + super.init() + } +} diff --git a/macos/Classes/InAppWebView/CustomSchemeHandler.swift b/macos/Classes/InAppWebView/CustomSchemeHandler.swift new file mode 100755 index 00000000..95400fb2 --- /dev/null +++ b/macos/Classes/InAppWebView/CustomSchemeHandler.swift @@ -0,0 +1,45 @@ +// +// CustomeSchemeHandler.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 25/10/2019. +// + +import FlutterMacOS +import Foundation +import WebKit + +@available(macOS 10.13, *) +public class CustomSchemeHandler : NSObject, WKURLSchemeHandler { + var schemeHandlers: [Int:WKURLSchemeTask] = [:] + + public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + schemeHandlers[urlSchemeTask.hash] = urlSchemeTask + let inAppWebView = webView as! InAppWebView + let request = WebResourceRequest.init(fromURLRequest: urlSchemeTask.request) + let callback = WebViewChannelDelegate.LoadResourceWithCustomSchemeCallback() + callback.nonNullSuccess = { (response: CustomSchemeResponse) in + if (self.schemeHandlers[urlSchemeTask.hash] != nil) { + let urlResponse = URLResponse(url: request.url, mimeType: response.contentType, expectedContentLength: -1, textEncodingName: response.contentEncoding) + urlSchemeTask.didReceive(urlResponse) + urlSchemeTask.didReceive(response.data) + urlSchemeTask.didFinish() + self.schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } + return false + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + } + + if let channelDelegate = inAppWebView.channelDelegate { + channelDelegate.onLoadResourceWithCustomScheme(request: request, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } +} diff --git a/macos/Classes/InAppWebView/FlutterWebViewController.swift b/macos/Classes/InAppWebView/FlutterWebViewController.swift new file mode 100755 index 00000000..e4b7e702 --- /dev/null +++ b/macos/Classes/InAppWebView/FlutterWebViewController.swift @@ -0,0 +1,179 @@ +// +// FlutterWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class FlutterWebViewController: NSObject, /*FlutterPlatformView,*/ Disposable { + + var myView: NSView? + + init(registrar: FlutterPluginRegistrar, withFrame frame: CGRect, viewIdentifier viewId: Any, params: NSDictionary) { + super.init() + + myView = NSView(frame: frame) + + let initialSettings = params["initialSettings"] as! [String: Any?] + let contextMenu = params["contextMenu"] as? [String: Any] + let windowId = params["windowId"] as? Int64 + let initialUserScripts = params["initialUserScripts"] as? [[String: Any]] + + var userScripts: [UserScript] = [] + if let initialUserScripts = initialUserScripts { + for intialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: intialUserScript, windowId: windowId)!) + } + } + + let settings = InAppWebViewSettings() + let _ = settings.parse(settings: initialSettings) + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: settings) + + var webView: InAppWebView? + + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.id = viewId + let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: viewId), + binaryMessenger: registrar.messenger) + webView!.channelDelegate = WebViewChannelDelegate(webView: webView!, channel: channel) + webView!.frame = myView!.bounds + webView!.contextMenu = contextMenu + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: viewId, + registrar: registrar, + frame: myView!.bounds, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + userScripts: userScripts) + } + + webView!.autoresizingMask = [.width, .height] + myView!.autoresizesSubviews = true + myView!.autoresizingMask = [.width, .height] + myView!.addSubview(webView!) + + webView!.settings = settings + webView!.prepare() + webView!.windowCreated = true + } + + public func webView() -> InAppWebView? { + for subview in myView?.subviews ?? [] + { + if let item = subview as? InAppWebView + { + return item + } + } + return nil + } + + public func view() -> NSView { + return myView! + } + + public func makeInitialLoad(params: NSDictionary) { + guard let webView = webView() else { + return + } + + let windowId = params["windowId"] as? Int64 + let initialUrlRequest = params["initialUrlRequest"] as? [String: Any?] + let initialFile = params["initialFile"] as? String + let initialData = params["initialData"] as? [String: String?] + + if windowId == nil { + if #available(macOS 10.13, *) { + webView.configuration.userContentController.removeAllContentRuleLists() + if let contentBlockers = webView.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: String.Encoding.utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = webView.configuration + configuration.userContentController.add(contentRuleList!) + + self.load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + return + } catch { + print(error.localizedDescription) + } + } + } + load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + else if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView.load(webViewTransport.request) + } + } + + func load(initialUrlRequest: [String:Any?]?, initialFile: String?, initialData: [String: String?]?) { + guard let webView = webView() else { + return + } + + if let initialFile = initialFile { + do { + try webView.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData, let data = initialData["data"]!, + let mimeType = initialData["mimeType"]!, let encoding = initialData["encoding"]!, + let baseUrl = URL(string: initialData["baseUrl"]! ?? "about:blank") { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadData(data: data, + mimeType: mimeType, + encoding: encoding, + baseUrl: baseUrl, + allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + let urlRequest = URLRequest.init(fromPluginMap: initialUrlRequest) + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, let url = urlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadUrl(urlRequest: urlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + } + + public func dispose() { + if let webView = webView() { + webView.dispose() + } + myView = nil + } + + deinit { + debugPrint("FlutterWebViewController - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/FlutterWebViewFactory.swift b/macos/Classes/InAppWebView/FlutterWebViewFactory.swift new file mode 100755 index 00000000..223a8c81 --- /dev/null +++ b/macos/Classes/InAppWebView/FlutterWebViewFactory.swift @@ -0,0 +1,35 @@ +// +// FlutterWebViewFactory.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import SwiftUI +import Cocoa +import FlutterMacOS +import Foundation + +public class FlutterWebViewFactory: NSObject, FlutterPlatformViewFactory { + static let VIEW_TYPE_ID = "com.pichillilorenzo/flutter_inappwebview" + private var registrar: FlutterPluginRegistrar? + + init(registrar: FlutterPluginRegistrar?) { + super.init() + self.registrar = registrar + } + + public func createArgsCodec() -> (FlutterMessageCodec & NSObjectProtocol)? { + return FlutterStandardMessageCodec.sharedInstance() + } + + public func create(withViewIdentifier viewId: Int64, arguments args: Any?) -> NSView { + let arguments = args as? NSDictionary + let webviewController = FlutterWebViewController(registrar: registrar!, + withFrame: .zero, + viewIdentifier: viewId, + params: arguments!) + webviewController.makeInitialLoad(params: arguments!) + return webviewController.view() + } +} diff --git a/macos/Classes/InAppWebView/InAppWebView.swift b/macos/Classes/InAppWebView/InAppWebView.swift new file mode 100755 index 00000000..c8894aad --- /dev/null +++ b/macos/Classes/InAppWebView/InAppWebView.swift @@ -0,0 +1,2485 @@ +// +// InAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import FlutterMacOS +import Foundation +import WebKit + +public class InAppWebView: WKWebView, WKUIDelegate, + WKNavigationDelegate, WKScriptMessageHandler, + WKDownloadDelegate, + Disposable { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_" + + var id: Any? // viewId + var windowId: Int64? + var windowCreated = false + var inAppBrowserDelegate: InAppBrowserDelegate? + var channelDelegate: WebViewChannelDelegate? + var settings: InAppWebViewSettings? + var webMessageChannels: [String:WebMessageChannel] = [:] + var webMessageListeners: [WebMessageListener] = [] + var currentOriginalUrl: URL? + var inFullscreen = false + private var printJobCompletionHandler: PrintJobController.CompletionHandler? + + static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate] + 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] = [] + + var lastLongPressTouchPoint: CGPoint? + + 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] = [] + + static var windowWebViews: [Int64:WebViewTransport] = [:] + static var windowAutoincrementId: Int64 = 0; + + var callAsyncJavaScriptBelowIOS14Results: [String:((Any?) -> Void)] = [:] + + var oldZoomScale = Float(1.0) + + init(id: Any?, registrar: FlutterPluginRegistrar?, frame: CGRect, configuration: WKWebViewConfiguration, + contextMenu: [String: Any]?, userScripts: [UserScript] = []) { + super.init(frame: frame, configuration: configuration) + self.id = id + if let id = id, let registrar = 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 + } + + required public init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder)! + } + + public func prepare() { + 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(macOS 12.0, *) { + addObserver(self, + forKeyPath: #keyPath(WKWebView.cameraCaptureState), + options: [.new, .old], + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.microphoneCaptureState), + options: [.new, .old], + context: 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: NSWindow.didEnterFullScreenNotification, + object: window) + + // listen for videos stopping to play in fullscreen + NotificationCenter.default.addObserver(self, + selector: #selector(onExitFullscreen(_:)), + name: NSWindow.didExitFullScreenNotification, + object: window) +// } + + if let settings = settings { + + if #available(macOS 12.0, *), settings.transparentBackground { + underPageBackgroundColor = .clear + } + + allowsBackForwardNavigationGestures = settings.allowsBackForwardNavigationGestures + if #available(iOS 9.0, *) { + allowsLinkPreview = settings.allowsLinkPreview + if !settings.userAgent.isEmpty { + customUserAgent = settings.userAgent + } + } + + if #available(macOS 11.0, *) { + mediaType = settings.mediaType + pageZoom = CGFloat(settings.pageZoom) + } + + if #available(macOS 12.0, *) { + if let underPageBackgroundColor = settings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = NSColor(hexString: underPageBackgroundColor) + } + } + + // debugging is always enabled for iOS, + // there isn't any option to set about it such as on Android. + + 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 { + configuration.allowsAirPlayForMediaPlayback = settings.allowsAirPlayForMediaPlayback + configuration.preferences.javaScriptCanOpenWindowsAutomatically = settings.javaScriptCanOpenWindowsAutomatically + configuration.preferences.minimumFontSize = CGFloat(settings.minimumFontSize) + + if #available(macOS 10.15, *) { + configuration.preferences.isFraudulentWebsiteWarningEnabled = settings.isFraudulentWebsiteWarningEnabled + configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: settings.preferredContentMode)! + } + + configuration.preferences.javaScriptEnabled = settings.javaScriptEnabled + if #available(macOS 11.0, *) { + configuration.defaultWebpagePreferences.allowsContentJavaScript = settings.javaScriptEnabled + } + + if #available(macOS 11.3, *) { + configuration.preferences.isTextInteractionEnabled = settings.isTextInteractionEnabled + } + + if #available(macOS 12.0, *) { + configuration.preferences.isSiteSpecificQuirksModeEnabled = settings.isSiteSpecificQuirksModeEnabled + configuration.preferences.isElementFullscreenEnabled = settings.isElementFullscreenEnabled + } + } + } + + 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) + configuration.userContentController.addPluginScript(ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT) + if let settings = settings { + if settings.useShouldInterceptAjaxRequest { + 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.suppressesIncrementalRendering = settings.suppressesIncrementalRendering + + if settings.allowUniversalAccessFromFileURLs { + configuration.setValue(settings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs") + } + + if settings.allowFileAccessFromFileURLs { + configuration.preferences.setValue(settings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs") + } + + 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(macOS 10.12, *) { + configuration.mediaTypesRequiringUserActionForPlayback = settings.mediaPlaybackRequiresUserGesture ? .all : [] + } + + if #available(macOS 10.13, *) { + 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(macOS 11.0, *) { + configuration.limitsNavigationsToAppBoundDomains = settings.limitsNavigationsToAppBoundDomains + } + + if #available(macOS 11.3, *) { + 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 #available(macOS 12.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() +// } +// } + } + } + + public func initializeWindowIdJS() { + if let windowId = windowId { + if #available(macOS 11.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(macOS 10.13, *) + 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(macOS 10.15, *), 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, let cgImage = screenshot.cgImage(forProposedRect: nil, context: nil, hints: nil) { + let newRep = NSBitmapImageRep(cgImage: cgImage) + if let with = with { + switch with["compressFormat"] as! String { + case "JPEG": + let quality = Float(with["quality"] as! Int) / 100 + imageData = newRep.representation(using: .jpeg, properties: [ + NSBitmapImageRep.PropertyKey.compressionFactor:quality]) + break + case "PNG": + imageData = newRep.representation(using: .png, properties: [:]) + break + default: + imageData = newRep.representation(using: .png, properties: [:]) + } + } + else { + imageData = newRep.representation(using: .png, properties: [:]) + } + } + completionHandler(imageData) + }) + } + + @available(macOS 11.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(macOS 11.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(macOS 11.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 { + let assetURL = try Util.getUrlAsset(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["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(macOS 10.13, *) { + 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["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.useShouldInterceptFetchRequest = false + } + } + + 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(macOS 10.12, *) { + configuration.mediaTypesRequiringUserActionForPlayback = (newSettings.mediaPlaybackRequiresUserGesture) ? .all : [] + } + } + + 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 #available(macOS 10.15, *) { + 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["allowsLinkPreview"] != nil && settings?.allowsLinkPreview != newSettings.allowsLinkPreview { + allowsLinkPreview = newSettings.allowsLinkPreview + } + if newSettingsMap["allowsAirPlayForMediaPlayback"] != nil && settings?.allowsAirPlayForMediaPlayback != newSettings.allowsAirPlayForMediaPlayback { + configuration.allowsAirPlayForMediaPlayback = newSettings.allowsAirPlayForMediaPlayback + } + 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(macOS 11.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(macOS 10.13, *), 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: String.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(macOS 11.3, *) { + 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 #available(macOS 12.0, *) { + if newSettingsMap["underPageBackgroundColor"] != nil, settings?.underPageBackgroundColor != newSettings.underPageBackgroundColor, + let underPageBackgroundColor = newSettings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = NSColor(hexString: underPageBackgroundColor) + } + } + if #available(macOS 12.0, *) { + if newSettingsMap["isSiteSpecificQuirksModeEnabled"] != nil, settings?.isSiteSpecificQuirksModeEnabled != newSettings.isSiteSpecificQuirksModeEnabled { + configuration.preferences.isSiteSpecificQuirksModeEnabled = newSettings.isSiteSpecificQuirksModeEnabled + } + } + + 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(macOS 11.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(macOS 11.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) + } + } + } + + public func clearCache() { + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: date as Date, completionHandler:{ }) + } + + 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: String.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(macOS 11.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: String.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(macOS 11.0, *) + public func evaluateJavaScript(_ javaScript: String, frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> 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(macOS 11.0, *) + public func evaluateJavascript(source: String, contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, contentWorld: contentWorld, withWrapper: nil, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func callAsyncJavaScript(_ functionBody: String, arguments: [String : Any] = [:], frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + super.callAsyncJavaScript(functionBody, arguments: arguments, in: frame, in: contentWorld, completionHandler: completionHandler) + } + + @available(macOS 11.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) + } + } + + 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, *) + @available(macOS 12.0, *) + @available(macCatalyst 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) + + let callback = WebViewChannelDelegate.PermissionRequestCallback() + callback.nonNullSuccess = { (response: PermissionResponse) in + if let action = response.action { + switch action { + case 1: + decisionHandler(.grant) + break + case 2: + decisionHandler(.prompt) + break + default: + decisionHandler(.deny) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: PermissionResponse?) in + 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(macOS 10.15, *) + 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(macOS 11.3, *) + 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(macOS 11.3, *) + 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) { + + if windowId != nil, !windowCreated { + decisionHandler(.cancel) + return + } + + let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() + callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in + decisionHandler(.allow) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let useShouldOverrideUrlLoading = settings?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading, let channelDelegate = channelDelegate { + channelDelegate.shouldOverrideUrlLoading(navigationAction: navigationAction, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + 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! { + let callback = WebViewChannelDelegate.NavigationResponseCallback() + callback.nonNullSuccess = { (response: WKNavigationResponsePolicy) in + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationResponsePolicy?) in + 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(macOS 11.3, *), !navigationResponse.canShowMIMEType { + 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(macOS 11.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) + + 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) { + + if windowId != nil, !windowCreated { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + 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 { + switch action { + case 0: + InAppWebView.credentialsProposed = [] + // used .performDefaultHandling to mantain 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, let credentialStore = CredentialDatabase.credentialStore { + for (protectionSpace, credentials) in 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 + completionHandler(.performDefaultHandling, nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + 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", + let sslCertificate = challenge.protectionSpace.sslCertificate { + InAppWebView.sslCertificatesMap[challenge.protectionSpace.host] = sslCertificate + } + + let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() + callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in + if let action = response.action { + switch action { + case 0: + InAppWebView.credentialsProposed = [] + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + 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 + completionHandler(.performDefaultHandling, nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { + let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback() + callback.nonNullSuccess = { (response: ClientCertResponse) in + if let action = response.action { + switch action { + case 0: + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let certificatePath = response.certificatePath + let certificatePassword = response.certificatePassword ?? ""; + + var path: String = certificatePath + do { + path = try Util.getAbsPathAsset(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 + completionHandler(.performDefaultHandling, nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onReceivedClientCertRequest(challenge: ClientCertChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + 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? = nil + 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 { + // 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 alert = NSAlert() + alert.messageText = title ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.runModal() + completionHandler() + } + + public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + + if (isPausedTimers) { + isPausedTimersCompletionHandler = completionHandler + return + } + + let callback = WebViewChannelDelegate.JsAlertCallback() + callback.nonNullSuccess = { (response: JsAlertResponse) in + if response.handledByClient { + let action = response.action ?? 1 + switch action { + case 0: + completionHandler() + break + default: + completionHandler() + } + return false + } + return true + } + callback.defaultBehaviour = { (response: JsAlertResponse?) in + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + self.createAlertDialog(message: message, responseMessage: responseMessage, + confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + } + callback.error = { (code: String, message: String?, details: Any?) in + 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 alert = NSAlert() + alert.messageText = title ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.addButton(withTitle: cancelButton ?? "") + let res = alert.runModal() + completionHandler(res == .alertFirstButtonReturn) + } + + public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (Bool) -> Void) { + let callback = WebViewChannelDelegate.JsConfirmCallback() + callback.nonNullSuccess = { (response: JsConfirmResponse) in + if response.handledByClient { + 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 = { (response: JsConfirmResponse?) in + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + self.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + } + callback.error = { (code: String, message: String?, details: Any?) in + 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 alert = NSAlert() + alert.messageText = title ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.addButton(withTitle: cancelButton ?? "") + let txt = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) + txt.stringValue = defaultValue ?? "" + alert.accessoryView = txt + let res = alert.runModal() + + completionHandler(value != nil ? value : (res == .alertFirstButtonReturn ? txt.stringValue : nil)) + } + + public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (String?) -> Void) { + let callback = WebViewChannelDelegate.JsPromptCallback() + callback.nonNullSuccess = { (response: JsPromptResponse) in + if response.handledByClient { + 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 = { (response: JsPromptResponse?) in + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + let value = response?.value + self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, + cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + } + callback.error = { (code: String, message: String?, details: Any?) in + 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) + } + } + + public func webView(_ webView: WKWebView, + createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures: WKWindowFeatures) -> WKWebView? { + InAppWebView.windowAutoincrementId += 1 + let windowId = InAppWebView.windowAutoincrementId + + let windowWebView = InAppWebView(id: nil, registrar: nil, frame: CGRect.zero, configuration: configuration, contextMenu: nil) + windowWebView.windowId = windowId + + let webViewTransport = WebViewTransport( + webView: windowWebView, + request: navigationAction.request + ) + + InAppWebView.windowWebViews[windowId] = webViewTransport + windowWebView.stopLoading() + + let createWindowAction = CreateWindowAction(navigationAction: navigationAction, windowId: windowId, windowFeatures: windowFeatures, isDialog: nil) + + let callback = WebViewChannelDelegate.CreateWindowCallback() + callback.nonNullSuccess = { (handledByClient: Bool) in + return !handledByClient + } + callback.defaultBehaviour = { (handledByClient: Bool?) in + if InAppWebView.windowWebViews[windowId] != nil { + InAppWebView.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) { + if windowId != nil, !windowCreated { + decisionHandler(false) + return + } + + let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback() + callback.nonNullSuccess = { (action: Bool) in + decisionHandler(action) + return false + } + callback.defaultBehaviour = { (action: Bool?) in + decisionHandler(false) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.shouldAllowDeprecatedTLS(challenge: challenge, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + 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) { + if message.name.starts(with: "console") { + 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 body = message.body as! [String: Any?] + let consoleMessage = body["message"] as! String + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onConsoleMessage(message: consoleMessage, messageLevel: messageLevel) + } else if message.name == "callHandler" { + let body = message.body as! [String: Any?] + 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 = { (handledByClient: Bool?) in + if let printJob = 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 + let args = body["args"] as! String + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + + let callback = WebViewChannelDelegate.CallJsHandlerCallback() + callback.defaultBehaviour = { (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)](\(json)); + delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)]; +} +""", completionHandler: nil) + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + } + + if let channelDelegate = webView.channelDelegate { + channelDelegate.onCallJsHandler(handlerName: handlerName, args: args, callback: callback) + } + } else if message.name == "onFindResultReceived" { + let body = message.body as! [String: Any?] + 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 = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) + } else if message.name == "onScrollChanged" { + let body = message.body as! [String: Any?] + let x = body["x"] as! Int + let y = body["y"] as! Int + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onScrollChanged(x: x, y: y) + } else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received" { + let body = message.body as! [String: Any?] + let resultUuid = body["resultUuid"] as! String + if let result = callAsyncJavaScriptBelowIOS14Results[resultUuid] { + result([ + "value": body["value"], + "error": body["error"] + ]) + callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) + } + } else if message.name == "onWebMessagePortMessageReceived" { + let body = message.body as! [String: Any?] + let webMessageChannelId = body["webMessageChannelId"] as! String + let index = body["index"] as! Int64 + let webMessage = body["message"] as? String + if let webMessageChannel = webMessageChannels[webMessageChannelId] { + webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage) + } + } else if message.name == "onWebMessageListenerPostMessageReceived" { + let body = message.body as! [String: Any?] + let jsObjectName = body["jsObjectName"] as! String + let messageData = body["message"] as? String + 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: messageData, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) + } + } + } + + public func scrollTo(x: Int, y: Int, animated: Bool) { + evaluateJavaScript("window.scrollTo({left: \(x), top: \(y), behavior: \(animated ? "'smooth'" : "'auto'")})") + } + + public func scrollBy(x: Int, y: Int, animated: Bool) { + evaluateJavaScript("window.scrollBy({left: \(x), top: \(y), behavior: \(animated ? "'smooth'" : "'auto'")})") + } + + + 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: PrintJobController.CompletionHandler? = nil) -> String? { + if #available(macOS 11.0, *) { + var printJobId: String? = nil + if let settings = settings, settings.handledByClient { + printJobId = NSUUID().uuidString + } + + let printInfo = NSPrintInfo() + + if let settings = settings { + if let orientationValue = settings.orientation, + let orientation = NSPrintInfo.PaperOrientation.init(rawValue: orientationValue) { + printInfo.orientation = orientation + } + if let margins = settings.margins { + printInfo.topMargin = margins.top + printInfo.rightMargin = margins.right + printInfo.bottomMargin = margins.bottom + printInfo.leftMargin = margins.left + } + } + let printOperation = printOperation(with: printInfo) + printOperation.jobTitle = settings?.jobName ?? (title ?? url?.absoluteString ?? "") + " Document" + printOperation.view?.frame = bounds + printOperation.printPanel.options.insert(.showsOrientation) + printOperation.printPanel.options.insert(.showsPaperSize) + printOperation.printPanel.options.insert(.showsScaling) + + if let id = printJobId { + let printJob = PrintJobController(id: id, job: printOperation, settings: settings) + PrintJobManager.jobs[id] = printJob + printJob.present(parentWindow: window, completionHandler: completionHandler) + } else if let window = window { + printJobCompletionHandler = completionHandler + printOperation.runModal(for: window, delegate: self, didRun: #selector(printOperationDidRun), contextInfo: nil) + } else { + printView(self) + } + + return printJobId + } else { + printView(self) + } + return nil + } + + @objc func printOperationDidRun(printOperation: NSPrintOperation, + success: Bool, + contextInfo: UnsafeMutableRawPointer?) { + if let completionHandler = printJobCompletionHandler { + completionHandler(printOperation, success, contextInfo) + printJobCompletionHandler = nil + } + } + + public func getContentHeight(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("document.body.scrollHeight") { scrollHeight, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollHeight as? Double ?? 0.0), nil) + } + } + } + + public func getOriginalUrl() -> URL? { + return currentOriginalUrl + } + + 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.enclosingScrollView?.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 enclosingScrollView?.contentSize.height ?? 0 > self.frame.height + } + + public func canScrollHorizontally() -> Bool { + return enclosingScrollView?.contentSize.width ?? 0 > self.frame.width + } + + public func createWebMessageChannel(completionHandler: ((WebMessageChannel) -> Void)? = nil) -> WebMessageChannel { + let id = NSUUID().uuidString + let webMessageChannel = WebMessageChannel(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 data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let url = URL(string: targetOrigin)?.absoluteString ?? "*" + let source = """ + (function() { + window.postMessage('\(data)', '\(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() + } + + public func getScrollX(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("window.scrollX") { scrollX, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollX as? Double ?? 0.0), nil) + } + } + } + + public func getScrollY(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("window.scrollY") { scrollY, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollY as? Double ?? 0.0), nil) + } + } + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + printJobCompletionHandler = nil + removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.url)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.title)) + if #available(macOS 12.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)) +// } + resumeTimers() + stopLoading() + disposeWebMessageChannels() + for webMessageListener in webMessageListeners { + webMessageListener.dispose() + } + webMessageListeners.removeAll() + 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(macOS 10.13, *) { + configuration.userContentController.removeAllContentRuleLists() + } + } else if let wId = windowId, InAppWebView.windowWebViews[wId] != nil { + InAppWebView.windowWebViews.removeValue(forKey: wId) + } + configuration.userContentController.dispose(windowId: windowId) + NotificationCenter.default.removeObserver(self) + for imp in customIMPs { + imp_removeBlock(imp) + } + uiDelegate = nil + navigationDelegate = nil + isPausedTimersCompletionHandler = nil + SharedLastTouchPointTimestamp.removeValue(forKey: self) + callAsyncJavaScriptBelowIOS14Results.removeAll() + super.removeFromSuperview() + } + + deinit { + debugPrint("InAppWebView - dealloc") + } +} diff --git a/macos/Classes/InAppWebView/InAppWebViewSettings.swift b/macos/Classes/InAppWebView/InAppWebViewSettings.swift new file mode 100755 index 00000000..1d9f7f96 --- /dev/null +++ b/macos/Classes/InAppWebView/InAppWebViewSettings.swift @@ -0,0 +1,136 @@ +// +// InAppWebViewSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import Foundation +import WebKit + +@objcMembers +public class InAppWebViewSettings: ISettings { + + var useShouldOverrideUrlLoading = false + var useOnLoadResource = false + var useOnDownloadStart = false + var clearCache = false + var userAgent = "" + var applicationNameForUserAgent = "" + var javaScriptEnabled = true + var javaScriptCanOpenWindowsAutomatically = false + var mediaPlaybackRequiresUserGesture = true + var verticalScrollBarEnabled = true + var horizontalScrollBarEnabled = true + var resourceCustomSchemes: [String] = [] + var contentBlockers: [[String: [String : Any]]] = [] + var minimumFontSize = 0 + var useShouldInterceptAjaxRequest = false + var useShouldInterceptFetchRequest = false + var incognito = false + var cacheEnabled = true + var transparentBackground = false + var disableVerticalScroll = false + var disableHorizontalScroll = false + var disableContextMenu = false + var supportZoom = true + var allowUniversalAccessFromFileURLs = false + var allowFileAccessFromFileURLs = false + + var disallowOverScroll = false + var enableViewportScale = false + var suppressesIncrementalRendering = false + var allowsAirPlayForMediaPlayback = true + var allowsBackForwardNavigationGestures = true + var allowsLinkPreview = true + var ignoresViewportScaleLimits = false + var allowsInlineMediaPlayback = false + var allowsPictureInPictureMediaPlayback = true + var isFraudulentWebsiteWarningEnabled = true + var selectionGranularity = 0 + var dataDetectorTypes: [String] = ["NONE"] // WKDataDetectorTypeNone + var preferredContentMode = 0 + var sharedCookiesEnabled = false + var automaticallyAdjustsScrollIndicatorInsets = false + var accessibilityIgnoresInvertColors = false + var decelerationRate = "NORMAL" // UIScrollView.DecelerationRate.normal + var alwaysBounceVertical = false + var alwaysBounceHorizontal = false + var scrollsToTop = true + var isPagingEnabled = false + var maximumZoomScale = 1.0 + var minimumZoomScale = 1.0 + var contentInsetAdjustmentBehavior = 2 // UIScrollView.ContentInsetAdjustmentBehavior.never + var isDirectionalLockEnabled = false + var mediaType: String? = nil + var pageZoom = 1.0 + var limitsNavigationsToAppBoundDomains = false + var useOnNavigationResponse = false + var applePayAPIEnabled = false + var allowingReadAccessTo: String? = nil + var disableLongPressContextMenuOnLinks = false + var disableInputAccessoryView = false + var underPageBackgroundColor: String? + var isTextInteractionEnabled = true + var isSiteSpecificQuirksModeEnabled = true + var upgradeKnownHostsToHTTPS = true + var isElementFullscreenEnabled = true + var isFindInteractionEnabled = false + var minimumViewportInset: NSEdgeInsets? = nil + var maximumViewportInset: NSEdgeInsets? = nil + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> InAppWebViewSettings { + let _ = super.parse(settings: settings) + if #available(iOS 13.0, *) {} else { + applePayAPIEnabled = false + } + return self + } + + override func getRealSettings(obj: InAppWebView?) -> [String: Any?] { + var realSettings: [String: Any?] = toMap() + if let webView = obj { + let configuration = webView.configuration + if #available(iOS 9.0, *) { + realSettings["userAgent"] = webView.customUserAgent + realSettings["applicationNameForUserAgent"] = configuration.applicationNameForUserAgent + realSettings["allowsAirPlayForMediaPlayback"] = configuration.allowsAirPlayForMediaPlayback + realSettings["allowsLinkPreview"] = webView.allowsLinkPreview + } + realSettings["javaScriptCanOpenWindowsAutomatically"] = configuration.preferences.javaScriptCanOpenWindowsAutomatically + if #available(macOS 10.12, *) { + realSettings["mediaPlaybackRequiresUserGesture"] = configuration.mediaTypesRequiringUserActionForPlayback == .all + } + realSettings["minimumFontSize"] = configuration.preferences.minimumFontSize + realSettings["suppressesIncrementalRendering"] = configuration.suppressesIncrementalRendering + realSettings["allowsBackForwardNavigationGestures"] = webView.allowsBackForwardNavigationGestures + if #available(macOS 10.15, *) { + realSettings["isFraudulentWebsiteWarningEnabled"] = configuration.preferences.isFraudulentWebsiteWarningEnabled + realSettings["preferredContentMode"] = configuration.defaultWebpagePreferences.preferredContentMode.rawValue + } + realSettings["allowUniversalAccessFromFileURLs"] = configuration.value(forKey: "allowUniversalAccessFromFileURLs") + realSettings["allowFileAccessFromFileURLs"] = configuration.preferences.value(forKey: "allowFileAccessFromFileURLs") + realSettings["javaScriptEnabled"] = configuration.preferences.javaScriptEnabled + if #available(macOS 11.0, *) { + realSettings["mediaType"] = webView.mediaType + realSettings["pageZoom"] = Float(webView.pageZoom) + realSettings["limitsNavigationsToAppBoundDomains"] = configuration.limitsNavigationsToAppBoundDomains + realSettings["javaScriptEnabled"] = configuration.defaultWebpagePreferences.allowsContentJavaScript + } + if #available(macOS 11.3, *) { + realSettings["isTextInteractionEnabled"] = configuration.preferences.isTextInteractionEnabled + realSettings["upgradeKnownHostsToHTTPS"] = configuration.upgradeKnownHostsToHTTPS + } + if #available(macOS 12.0, *) { + realSettings["underPageBackgroundColor"] = webView.underPageBackgroundColor.hexString + realSettings["isSiteSpecificQuirksModeEnabled"] = configuration.preferences.isSiteSpecificQuirksModeEnabled + realSettings["isElementFullscreenEnabled"] = configuration.preferences.isElementFullscreenEnabled + } + } + return realSettings + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift new file mode 100644 index 00000000..79eb1feb --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift @@ -0,0 +1,75 @@ +// +// WebMessageChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import FlutterMacOS + +public class WebMessageChannel : FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_channel_" + var id: String + var channelDelegate: WebMessageChannelChannelDelegate? + weak var webView: InAppWebView? + var ports: [WebMessagePort] = [] + + public init(id: String) { + self.id = id + super.init() + let channel = FlutterMethodChannel(name: WebMessageChannel.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = WebMessageChannelChannelDelegate(webMessageChannel: self, channel: channel) + self.ports = [ + WebMessagePort(name: "port1", webMessageChannel: self), + WebMessagePort(name: "port2", webMessageChannel: self) + ] + } + + public func initJsInstance(webView: InAppWebView, completionHandler: ((WebMessageChannel) -> Void)? = nil) { + self.webView = webView + if let webView = self.webView { + webView.evaluateJavascript(source: """ + (function() { + \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"] = new MessageChannel(); + })(); + """) { (_) in + completionHandler?(self) + } + } else { + completionHandler?(self) + } + } + + public func toMap() -> [String:Any?] { + return [ + "id": id + ] + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + for port in ports { + port.dispose() + } + ports.removeAll() + webView?.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + if (webMessageChannel != null) { + webMessageChannel.port1.close(); + webMessageChannel.port2.close(); + delete \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + } + })(); + """) + webView = nil + } + + deinit { + debugPrint("WebMessageChannel - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift new file mode 100644 index 00000000..1e841100 --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift @@ -0,0 +1,105 @@ +// +// WebMessageChannelChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebMessageChannelChannelDelegate : ChannelDelegate { + private weak var webMessageChannel: WebMessageChannel? + + public init(webMessageChannel: WebMessageChannel, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageChannel = webMessageChannel + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "setWebMessageCallback": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.setWebMessageCallback { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + + } else { + result(true) + } + break + case "postMessage": + if let webView = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + let message = arguments!["message"] as! [String: Any?] + + var webMessagePorts: [WebMessagePort] = [] + let portsMap = message["ports"] as? [[String: Any?]] + if let portsMap = portsMap { + for portMap in portsMap { + let webMessageChannelId = portMap["webMessageChannelId"] as! String + let index = portMap["index"] as! Int + if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { + webMessagePorts.append(webMessageChannel.ports[index]) + } + } + } + let webMessage = WebMessage(data: message["data"] as? String, ports: webMessagePorts) + do { + try port.postMessage(message: webMessage) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + case "close": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.close { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onMessage(index: Int64, message: String?) { + let arguments: [String:Any?] = [ + "index": index, + "message": message + ] + channel?.invokeMethod("onMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageChannel = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift new file mode 100644 index 00000000..b5691ba9 --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift @@ -0,0 +1,184 @@ +// +// WebMessageListener.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class WebMessageListener : FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_listener_" + var jsObjectName: String + var allowedOriginRules: Set + var channelDelegate: WebMessageListenerChannelDelegate? + weak var webView: InAppWebView? + + public init(jsObjectName: String, allowedOriginRules: Set) { + self.jsObjectName = jsObjectName + self.allowedOriginRules = allowedOriginRules + super.init() + let channel = FlutterMethodChannel(name: WebMessageListener.METHOD_CHANNEL_NAME_PREFIX + self.jsObjectName, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = WebMessageListenerChannelDelegate(webMessageListener: self, channel: channel) + } + + public func assertOriginRulesValid() throws { + for (index, originRule) in allowedOriginRules.enumerated() { + if originRule.isEmpty { + throw NSError(domain: "allowedOriginRules[\(index)] is empty", code: 0) + } + if originRule == "*" { + continue + } + if let url = URL(string: originRule) { + guard let scheme = url.scheme else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme == "http" || scheme == "https", url.host == nil || url.host!.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme != "http", scheme != "https", url.host != nil || url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if url.host == nil || url.host!.isEmpty, url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if !url.path.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if let hostname = url.host { + if let firstIndex = hostname.firstIndex(of: "*") { + let distance = hostname.distance(from: hostname.startIndex, to: firstIndex) + if distance != 0 || (distance == 0 && hostname.prefix(2) != "*.") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + if hostname.hasPrefix("[") { + if !hostname.hasSuffix("]") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + let ipv6 = String(hostname[indexRange]) + if !Util.isIPv6(address: ipv6) { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + } else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + + public func initJsInstance(webView: InAppWebView) { + self.webView = webView + if let webView = self.webView { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let allowedOriginRulesString = allowedOriginRules.map { (allowedOriginRule) -> String in + if allowedOriginRule == "*" { + return "'*'" + } + let rule = URL(string: allowedOriginRule)! + let host = rule.host != nil ? "'" + rule.host!.replacingOccurrences(of: "\'", with: "\\'") + "'" : "null" + return """ + {scheme: '\(rule.scheme!)', host: \(host), port: \(rule.port != nil ? String(rule.port!) : "null")} + """ + }.joined(separator: ", ") + let source = """ + (function() { + var allowedOriginRules = [\(allowedOriginRulesString)]; + var isPageBlank = window.location.href === "about:blank"; + var scheme = !isPageBlank ? window.location.protocol.replace(":", "") : null; + var host = !isPageBlank ? window.location.hostname : null; + var port = !isPageBlank ? window.location.port : null; + if (window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed(allowedOriginRules, scheme, host, port)) { + window['\(jsObjectNameEscaped)'] = new FlutterInAppWebViewWebMessageListener('\(jsObjectNameEscaped)'); + } + })(); + """ + webView.configuration.userContentController.addPluginScript(PluginScript( + groupName: "WebMessageListener-" + jsObjectName, + source: source, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onWebMessageListenerPostMessageReceived"] + )) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + } + + public static func fromMap(map: [String:Any?]?) -> WebMessageListener? { + guard let map = map else { + return nil + } + return WebMessageListener( + jsObjectName: map["jsObjectName"] as! String, + allowedOriginRules: Set(map["allowedOriginRules"] as! [String]) + ) + } + + public func isOriginAllowed(scheme: String?, host: String?, port: Int?) -> Bool { + for allowedOriginRule in allowedOriginRules { + if allowedOriginRule == "*" { + return true + } + if scheme == nil || scheme!.isEmpty { + continue + } + if scheme == nil || scheme!.isEmpty, host == nil || host!.isEmpty, port == nil || port == 0 { + continue + } + if let rule = URL(string: allowedOriginRule) { + let rulePort = rule.port == nil || rule.port == 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port! + let currentPort = port == nil || port == 0 ? (scheme == "https" ? 443 : 80) : port! + var IPv6: String? = nil + if let hostname = rule.host, hostname.hasPrefix("[") { + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + do { + IPv6 = try Util.normalizeIPv6(address: String(hostname[indexRange])) + } catch {} + } + var hostIPv6: String? = nil + if let host = host, Util.isIPv6(address: host) { + do { + hostIPv6 = try Util.normalizeIPv6(address: host) + } catch {} + } + + let schemeAllowed = scheme != nil && !scheme!.isEmpty && scheme == rule.scheme + + let hostAllowed = rule.host == nil || + rule.host!.isEmpty || + host == rule.host || + (rule.host!.hasPrefix("*") && host != nil && host!.hasSuffix(rule.host!.split(separator: "*", omittingEmptySubsequences: false)[1])) || + (hostIPv6 != nil && IPv6 != nil && hostIPv6 == IPv6) + + let portAllowed = rulePort == currentPort + + if schemeAllowed, hostAllowed, portAllowed { + return true + } + } + } + return false + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + } + + deinit { + debugPrint("WebMessageListener - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift new file mode 100644 index 00000000..76b91d03 --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift @@ -0,0 +1,72 @@ +// +// WebMessageListenerChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebMessageListenerChannelDelegate : ChannelDelegate { + private weak var webMessageListener: WebMessageListener? + + public init(webMessageListener: WebMessageListener, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageListener = webMessageListener + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "postMessage": + if let webView = webMessageListener?.webView, let jsObjectName = webMessageListener?.jsObjectName { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let messageEscaped = (arguments!["message"] as! String).replacingOccurrences(of: "\'", with: "\\'") + let source = """ + (function() { + var webMessageListener = window['\(jsObjectNameEscaped)']; + if (webMessageListener != null) { + var event = {data: '\(messageEscaped)'}; + if (webMessageListener.onmessage != null) { + webMessageListener.onmessage(event); + } + for (var listener of webMessageListener.listeners) { + listener(event); + } + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + result(true) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onPostMessage(message: String?, sourceOrigin: URL?, isMainFrame: Bool) { + let arguments: [String:Any?] = [ + "message": message, + "sourceOrigin": sourceOrigin?.absoluteString, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onPostMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageListener = nil + } + + deinit { + dispose() + } +} + diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift new file mode 100644 index 00000000..b32b9baa --- /dev/null +++ b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -0,0 +1,1085 @@ +// +// WebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class WebViewChannelDelegate : ChannelDelegate { + private weak var webView: InAppWebView? + + public init(webView: InAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webView = webView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + guard let method = WebViewChannelDelegateMethods.init(rawValue: call.method) else { + result(FlutterMethodNotImplemented) + return + } + + switch method { + case .getUrl: + result(webView?.url?.absoluteString) + break + case .getTitle: + result(webView?.title) + break + case .getProgress: + result( (webView != nil) ? Int(webView!.estimatedProgress * 100) : nil ) + break + case .loadUrl: + let urlRequest = arguments!["urlRequest"] as! [String:Any?] + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadUrl(urlRequest: URLRequest.init(fromPluginMap: urlRequest), allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .postUrl: + if let webView = webView { + let url = arguments!["url"] as! String + let postData = arguments!["postData"] as! FlutterStandardTypedData + webView.postUrl(url: URL(string: url)!, postData: postData.data) + } + result(true) + break + case .loadData: + let data = arguments!["data"] as! String + let mimeType = arguments!["mimeType"] as! String + let encoding = arguments!["encoding"] as! String + let baseUrl = URL(string: arguments!["baseUrl"] as! String)! + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadData(data: data, mimeType: mimeType, encoding: encoding, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .loadFile: + let assetFilePath = arguments!["assetFilePath"] as! String + + do { + try webView?.loadFile(assetFilePath: assetFilePath) + } + catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + return + } + result(true) + break + case .evaluateJavascript: + if let webView = webView { + let source = arguments!["source"] as! String + let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] + if #available(macOS 11.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + webView.evaluateJavascript(source: source, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + webView.evaluateJavascript(source: source) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .injectJavascriptFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let scriptHtmlTagAttributes = arguments!["scriptHtmlTagAttributes"] as? [String:Any?] + webView?.injectJavascriptFileFromUrl(urlFile: urlFile, scriptHtmlTagAttributes: scriptHtmlTagAttributes) + result(true) + break + case .injectCSSCode: + let source = arguments!["source"] as! String + webView?.injectCSSCode(source: source) + result(true) + break + case .injectCSSFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let cssLinkHtmlTagAttributes = arguments!["cssLinkHtmlTagAttributes"] as? [String:Any?] + webView?.injectCSSFileFromUrl(urlFile: urlFile, cssLinkHtmlTagAttributes: cssLinkHtmlTagAttributes) + result(true) + break + case .reload: + webView?.reload() + result(true) + break + case .goBack: + webView?.goBack() + result(true) + break + case .canGoBack: + result(webView?.canGoBack ?? false) + break + case .goForward: + webView?.goForward() + result(true) + break + case .canGoForward: + result(webView?.canGoForward ?? false) + break + case .goBackOrForward: + let steps = arguments!["steps"] as! Int + webView?.goBackOrForward(steps: steps) + result(true) + break + case .canGoBackOrForward: + let steps = arguments!["steps"] as! Int + result(webView?.canGoBackOrForward(steps: steps) ?? false) + break + case .stopLoading: + webView?.stopLoading() + result(true) + break + case .isLoading: + result(webView?.isLoading ?? false) + break + case .takeScreenshot: + if let webView = webView, #available(macOS 10.13, *) { + let screenshotConfiguration = arguments!["screenshotConfiguration"] as? [String: Any?] + webView.takeScreenshot(with: screenshotConfiguration, completionHandler: { (screenshot) -> Void in + result(screenshot) + }) + } + else { + result(nil) + } + break + case .setSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + let inAppBrowserSettings = InAppBrowserSettings() + let inAppBrowserSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppBrowserSettings.parse(settings: inAppBrowserSettingsMap) + iabController.setSettings(newSettings: inAppBrowserSettings, newSettingsMap: inAppBrowserSettingsMap) + } else { + let inAppWebViewSettings = InAppWebViewSettings() + let inAppWebViewSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppWebViewSettings.parse(settings: inAppWebViewSettingsMap) + webView?.setSettings(newSettings: inAppWebViewSettings, newSettingsMap: inAppWebViewSettingsMap) + } + result(true) + break + case .getSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + result(iabController.getSettings()) + } else { + result(webView?.getSettings()) + } + break + case .close: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.close() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .show: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.show() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .hide: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.hide() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .getCopyBackForwardList: + result(webView?.getCopyBackForwardList()) + break + case .clearCache: + webView?.clearCache() + result(true) + break + case .scrollTo: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollTo(x: x, y: y, animated: animated) + result(true) + break + case .scrollBy: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollBy(x: x, y: y, animated: animated) + result(true) + break + case .pauseTimers: + webView?.pauseTimers() + result(true) + break + case .resumeTimers: + webView?.resumeTimers() + result(true) + break + case .printCurrentPage: + if let webView = webView { + let settings = PrintJobSettings() + if let settingsMap = arguments!["settings"] as? [String: Any?] { + let _ = settings.parse(settings: settingsMap) + } + result(webView.printCurrentPage(settings: settings)) + } else { + result(nil) + } + break + case .getContentHeight: + webView?.getContentHeight { contentHeight, error in + if let error = error { + print(error) + result(nil) + return + } + result(contentHeight) + } + break + case .zoomBy: +// let zoomFactor = (arguments!["zoomFactor"] as! NSNumber).floatValue +// let animated = arguments!["animated"] as! Bool +// webView?.zoomBy(zoomFactor: zoomFactor, animated: animated) + result(true) + break + case .reloadFromOrigin: + webView?.reloadFromOrigin() + result(true) + break + case .getOriginalUrl: + result(webView?.getOriginalUrl()?.absoluteString) + break + case .getZoomScale: + result(nil) + break + case .hasOnlySecureContent: + result(webView?.hasOnlySecureContent ?? false) + break + case .getSelectedText: + if let webView = webView { + webView.getSelectedText { (value, error) in + if let err = error { + print(err.localizedDescription) + result("") + return + } + result(value) + } + } + else { + result(nil) + } + break + case .getHitTestResult: + if let webView = webView { + webView.getHitTestResult { (hitTestResult) in + result(hitTestResult.toMap()) + } + } + else { + result(nil) + } + break + case .clearFocus: + webView?.clearFocus() + result(true) + break + case .setContextMenu: + if let webView = webView { + let contextMenu = arguments!["contextMenu"] as? [String: Any] + webView.contextMenu = contextMenu + result(true) + } else { + result(false) + } + break + case .requestFocusNodeHref: + if let webView = webView { + webView.requestFocusNodeHref { (value, error) in + if let err = error { + print(err.localizedDescription) + result(nil) + return + } + result(value) + } + } else { + result(nil) + } + break + case .requestImageRef: + if let webView = webView { + webView.requestImageRef { (value, error) in + if let err = error { + print(err.localizedDescription) + result(nil) + return + } + result(value) + } + } else { + result(nil) + } + break + case .getScrollX: + if let webView = webView { + webView.getScrollX { scrollX, error in + if let error = error { + print(error) + result(nil) + return + } + result(scrollX) + } + + } else { + result(nil) + } + break + case .getScrollY: + if let webView = webView { + webView.getScrollY { scrollY, error in + if let error = error { + print(error) + result(nil) + return + } + result(scrollY) + } + + } else { + result(nil) + } + break + case .getCertificate: + result(webView?.getCertificate()?.toMap()) + break + case .addUserScript: + if let webView = webView { + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView.windowId)! + webView.configuration.userContentController.addUserOnlyScript(userScript) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + result(true) + break + case .removeUserScript: + let index = arguments!["index"] as! Int + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView?.windowId)! + webView?.configuration.userContentController.removeUserOnlyScript(at: index, injectionTime: userScript.injectionTime) + result(true) + break + case .removeUserScriptsByGroupName: + let groupName = arguments!["groupName"] as! String + webView?.configuration.userContentController.removeUserOnlyScripts(with: groupName) + result(true) + break + case .removeAllUserScripts: + webView?.configuration.userContentController.removeAllUserOnlyScripts() + result(true) + break + case .callAsyncJavaScript: + if let webView = webView { + if #available(macOS 11.0, *) { // on iOS 14.0, for some reason, it crashes + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + var contentWorld = WKContentWorld.page + if let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] { + contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + } + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .createPdf: + if let webView = webView, #available(macOS 11.0, *) { + let configuration = arguments!["pdfConfiguration"] as? [String: Any?] + webView.createPdf(configuration: configuration, completionHandler: { (pdf) -> Void in + result(pdf) + }) + } + else { + result(nil) + } + break + case .createWebArchiveData: + if let webView = webView, #available(macOS 11.0, *) { + webView.createWebArchiveData(dataCompletionHandler: { (webArchiveData) -> Void in + result(webArchiveData) + }) + } + else { + result(nil) + } + break + case .saveWebArchive: + if let webView = webView, #available(macOS 11.0, *) { + let filePath = arguments!["filePath"] as! String + let autoname = arguments!["autoname"] as! Bool + webView.saveWebArchive(filePath: filePath, autoname: autoname, completionHandler: { (path) -> Void in + result(path) + }) + } + else { + result(nil) + } + break + case .isSecureContext: + if let webView = webView { + webView.isSecureContext(completionHandler: { (isSecureContext) in + result(isSecureContext) + }) + } + else { + result(false) + } + break + case .createWebMessageChannel: + if let webView = webView { + let _ = webView.createWebMessageChannel { (webMessageChannel) in + result(webMessageChannel.toMap()) + } + } else { + result(nil) + } + break + case .postWebMessage: + if let webView = webView { + let message = arguments!["message"] as! [String: Any?] + let targetOrigin = arguments!["targetOrigin"] as! String + + var ports: [WebMessagePort] = [] + let portsMap = message["ports"] as? [[String: Any?]] + if let portsMap = portsMap { + for portMap in portsMap { + let webMessageChannelId = portMap["webMessageChannelId"] as! String + let index = portMap["index"] as! Int + if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { + ports.append(webMessageChannel.ports[index]) + } + } + } + let webMessage = WebMessage(data: message["data"] as? String, ports: ports) + do { + try webView.postWebMessage(message: webMessage, targetOrigin: targetOrigin) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .addWebMessageListener: + if let webView = webView { + let webMessageListenerMap = arguments!["webMessageListener"] as! [String: Any?] + let webMessageListener = WebMessageListener.fromMap(map: webMessageListenerMap)! + do { + try webView.addWebMessageListener(webMessageListener: webMessageListener) + result(false) + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .canScrollVertically: + if let webView = webView { + result(webView.canScrollVertically()) + } else { + result(false) + } + break + case .canScrollHorizontally: + if let webView = webView { + result(webView.canScrollHorizontally()) + } else { + result(false) + } + break + case .pauseAllMediaPlayback: + if let webView = webView, #available(macOS 12.0 , *) { + webView.pauseAllMediaPlayback(completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .setAllMediaPlaybackSuspended: + if let webView = webView, #available(macOS 12.0 , *) { + let suspended = arguments!["suspended"] as! Bool + webView.setAllMediaPlaybackSuspended(suspended, completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .closeAllMediaPresentations: + if let webView = self.webView, #available(macOS 11.3, *) { + // closeAllMediaPresentations with completionHandler v15.0 makes the app crash + // with error EXC_BAD_ACCESS, so use closeAllMediaPresentations v14.5 + webView.closeAllMediaPresentations() + result(true) + } else { + result(false) + } + break + case .requestMediaPlaybackState: + if let webView = webView, #available(macOS 12.0, *) { + webView.requestMediaPlaybackState(completionHandler: { (state) -> Void in + result(state.rawValue) + }) + } else { + result(nil) + } + break + case .getMetaThemeColor: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.themeColor?.hexString) + } else { + result(nil) + } + break + case .isInFullscreen: + if let webView = webView { + if #available(macOS 12.0, *) { + result(webView.fullscreenState == .inFullscreen) + } else { + result(webView.inFullscreen) + } + } + else { + result(false) + } + break + case .getCameraCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.cameraCaptureState.rawValue) + } else { + result(nil) + } + break + case .setCameraCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setCameraCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .getMicrophoneCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.microphoneCaptureState.rawValue) + } else { + result(nil) + } + break + case .setMicrophoneCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setMicrophoneCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .loadSimulatedRequest: + if let webView = webView, #available(macOS 12.0, *) { + let request = URLRequest.init(fromPluginMap: arguments!["urlRequest"] as! [String:Any?]) + let data = arguments!["data"] as! FlutterStandardTypedData + var response: URLResponse? = nil + if let urlResponse = arguments!["urlResponse"] as? [String:Any?] { + response = URLResponse.init(fromPluginMap: urlResponse) + } + if let response = response { + webView.loadSimulatedRequest(request, response: response, responseData: data.data) + } else { + webView.loadSimulatedRequest(request, responseHTML: String(decoding: data.data, as: UTF8.self)) + } + result(true) + } else { + result(false) + } + } + } + + @available(*, deprecated, message: "Use FindInteractionChannelDelegate.onFindResultReceived instead.") + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public func onLongPressHitTestResult(hitTestResult: HitTestResult) { + channel?.invokeMethod("onLongPressHitTestResult", arguments: hitTestResult.toMap()) + } + + public func onScrollChanged(x: Int, y: Int) { + let arguments: [String: Any?] = ["x": x, "y": y] + channel?.invokeMethod("onScrollChanged", arguments: arguments) + } + + public func onDownloadStartRequest(request: DownloadStartRequest) { + channel?.invokeMethod("onDownloadStartRequest", arguments: request.toMap()) + } + + public func onCreateContextMenu(hitTestResult: HitTestResult) { + channel?.invokeMethod("onCreateContextMenu", arguments: hitTestResult.toMap()) + } + + public func onOverScrolled(x: Int, y: Int, clampedX: Bool, clampedY: Bool) { + let arguments: [String: Any?] = ["x": x, "y": y, "clampedX": clampedX, "clampedY": clampedY] + channel?.invokeMethod("onOverScrolled", arguments: arguments) + } + + public func onContextMenuActionItemClicked(id: Any, title: String) { + let arguments: [String: Any?] = [ + "id": id, + "iosId": id is Int64 ? String(id as! Int64) : id as! String, + "androidId": nil, + "title": title + ] + channel?.invokeMethod("onContextMenuActionItemClicked", arguments: arguments) + } + + public func onHideContextMenu() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onHideContextMenu", arguments: arguments) + } + + public func onEnterFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onEnterFullscreen", arguments: arguments) + } + + public func onExitFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExitFullscreen", arguments: arguments) + } + + public class JsAlertCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsAlertResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsAlert(url: URL?, message: String, isMainFrame: Bool, callback: JsAlertCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsAlert", arguments: arguments, callback: callback) + } + + public class JsConfirmCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsConfirmResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsConfirm(url: URL?, message: String, isMainFrame: Bool, callback: JsConfirmCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsConfirm", arguments: arguments, callback: callback) + } + + public class JsPromptCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsPromptResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsPrompt(url: URL?, message: String, defaultValue: String?, isMainFrame: Bool, callback: JsPromptCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "defaultValue": defaultValue, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsPrompt", arguments: arguments, callback: callback) + } + + public class CreateWindowCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onCreateWindow(createWindowAction: CreateWindowAction, callback: CreateWindowCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onCreateWindow", arguments: createWindowAction.toMap(), callback: callback) + } + + public func onCloseWindow() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onCloseWindow", arguments: arguments) + } + + public func onConsoleMessage(message: String, messageLevel: Int) { + let arguments: [String: Any?] = [ + "message": message, + "messageLevel": messageLevel + ] + channel?.invokeMethod("onConsoleMessage", arguments: arguments) + } + + public func onProgressChanged(progress: Int) { + let arguments: [String: Any?] = [ + "progress": progress + ] + channel?.invokeMethod("onProgressChanged", arguments: arguments) + } + + public func onTitleChanged(title: String?) { + let arguments: [String: Any?] = [ + "title": title + ] + channel?.invokeMethod("onTitleChanged", arguments: arguments) + } + + public class PermissionRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return PermissionResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onPermissionRequest(request: PermissionRequest, callback: PermissionRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onPermissionRequest", arguments: request.toMap(), callback: callback); + } + + public class ShouldOverrideUrlLoadingCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationActionPolicy.init(rawValue: action) ?? WKNavigationActionPolicy.cancel + } + return WKNavigationActionPolicy.cancel + } + } + } + + public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, callback: ShouldOverrideUrlLoadingCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("shouldOverrideUrlLoading", arguments: navigationAction.toMap(), callback: callback); + } + + public func onLoadStart(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStart", arguments: arguments) + } + + public func onLoadStop(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStop", arguments: arguments) + } + + public func onUpdateVisitedHistory(url: String?, isReload: Bool?) { + let arguments: [String: Any?] = [ + "url": url, + "isReload": nil + ] + channel?.invokeMethod("onUpdateVisitedHistory", arguments: arguments) + } + + public func onReceivedError(request: WebResourceRequest, error: WebResourceError) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "error": error.toMap() + ] + channel?.invokeMethod("onReceivedError", arguments: arguments) + } + + public func onReceivedHttpError(request: WebResourceRequest, errorResponse: WebResourceResponse) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "errorResponse": errorResponse.toMap() + ] + channel?.invokeMethod("onReceivedHttpError", arguments: arguments) + } + + public class ReceivedHttpAuthRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return HttpAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge, callback: ReceivedHttpAuthRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onReceivedHttpAuthRequest", arguments: challenge.toMap(), callback: callback) + } + + public class ReceivedServerTrustAuthRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ServerTrustAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge, callback: ReceivedServerTrustAuthRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onReceivedServerTrustAuthRequest", arguments: challenge.toMap(), callback: callback); + } + + public class ReceivedClientCertRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ClientCertResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onReceivedClientCertRequest(challenge: ClientCertChallenge, callback: ReceivedClientCertRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onReceivedClientCertRequest", arguments: challenge.toMap(), callback: callback); + } + + public func onZoomScaleChanged(newScale: Float, oldScale: Float) { + let arguments: [String: Any?] = [ + "newScale": newScale, + "oldScale": oldScale + ] + channel?.invokeMethod("onZoomScaleChanged", arguments: arguments) + } + + public func onPageCommitVisible(url: String?) { + let arguments: [String: Any?] = [ + "url": url + ] + channel?.invokeMethod("onPageCommitVisible", arguments: arguments) + } + + public class LoadResourceWithCustomSchemeCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return CustomSchemeResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onLoadResourceWithCustomScheme(request: WebResourceRequest, callback: LoadResourceWithCustomSchemeCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = ["request": request.toMap()] + channel.invokeMethod("onLoadResourceWithCustomScheme", arguments: arguments, callback: callback) + } + + public class CallJsHandlerCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj + } + } + } + + public func onCallJsHandler(handlerName: String, args: String, callback: CallJsHandlerCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "handlerName": handlerName, + "args": args + ] + channel.invokeMethod("onCallJsHandler", arguments: arguments, callback: callback); + } + + public class NavigationResponseCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationResponsePolicy.init(rawValue: action) ?? WKNavigationResponsePolicy.cancel + } + return WKNavigationResponsePolicy.cancel + } + } + } + + public func onNavigationResponse(navigationResponse: WKNavigationResponse, callback: NavigationResponseCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onNavigationResponse", arguments: navigationResponse.toMap(), callback: callback); + } + + public class ShouldAllowDeprecatedTLSCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return action == 1 + } + return false + } + } + } + + public func shouldAllowDeprecatedTLS(challenge: URLAuthenticationChallenge, callback: ShouldAllowDeprecatedTLSCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("shouldAllowDeprecatedTLS", arguments: challenge.toMap(), callback: callback) + } + + public func onWebContentProcessDidTerminate() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebContentProcessDidTerminate", arguments: arguments) + } + + public func onDidReceiveServerRedirectForProvisionalNavigation() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onDidReceiveServerRedirectForProvisionalNavigation", arguments: arguments) + } + + @available(macOS 12.0, *) + public func onCameraCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onCameraCaptureStateChanged", arguments: arguments) + } + + @available(macOS 12.0, *) + public func onMicrophoneCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onMicrophoneCaptureStateChanged", arguments: arguments) + } + + public class PrintRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onPrintRequest(url: URL?, printJobId: String?, callback: PrintRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + let arguments = [ + "url": url?.absoluteString, + "printJobId": printJobId, + ] + channel.invokeMethod("onPrintRequest", arguments: arguments, callback: callback) + } + + public override func dispose() { + super.dispose() + webView = nil + } + + deinit { + debugPrint("WebViewChannelDelegate - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift b/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift new file mode 100644 index 00000000..da6dba91 --- /dev/null +++ b/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift @@ -0,0 +1,84 @@ +// +// WebViewChannelDelegateMethods.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/10/22. +// + +import Foundation + +public enum WebViewChannelDelegateMethods: String { + case getUrl = "getUrl" + case getTitle = "getTitle" + case getProgress = "getProgress" + case loadUrl = "loadUrl" + case postUrl = "postUrl" + case loadData = "loadData" + case loadFile = "loadFile" + case evaluateJavascript = "evaluateJavascript" + case injectJavascriptFileFromUrl = "injectJavascriptFileFromUrl" + case injectCSSCode = "injectCSSCode" + case injectCSSFileFromUrl = "injectCSSFileFromUrl" + case reload = "reload" + case goBack = "goBack" + case canGoBack = "canGoBack" + case goForward = "goForward" + case canGoForward = "canGoForward" + case goBackOrForward = "goBackOrForward" + case canGoBackOrForward = "canGoBackOrForward" + case stopLoading = "stopLoading" + case isLoading = "isLoading" + case takeScreenshot = "takeScreenshot" + case setSettings = "setSettings" + case getSettings = "getSettings" + case close = "close" + case show = "show" + case hide = "hide" + case getCopyBackForwardList = "getCopyBackForwardList" + case clearCache = "clearCache" + case scrollTo = "scrollTo" + case scrollBy = "scrollBy" + case pauseTimers = "pauseTimers" + case resumeTimers = "resumeTimers" + case printCurrentPage = "printCurrentPage" + case getContentHeight = "getContentHeight" + case zoomBy = "zoomBy" + case reloadFromOrigin = "reloadFromOrigin" + case getOriginalUrl = "getOriginalUrl" + case getZoomScale = "getZoomScale" + case hasOnlySecureContent = "hasOnlySecureContent" + case getSelectedText = "getSelectedText" + case getHitTestResult = "getHitTestResult" + case clearFocus = "clearFocus" + case setContextMenu = "setContextMenu" + case requestFocusNodeHref = "requestFocusNodeHref" + case requestImageRef = "requestImageRef" + case getScrollX = "getScrollX" + case getScrollY = "getScrollY" + case getCertificate = "getCertificate" + case addUserScript = "addUserScript" + case removeUserScript = "removeUserScript" + case removeUserScriptsByGroupName = "removeUserScriptsByGroupName" + case removeAllUserScripts = "removeAllUserScripts" + case callAsyncJavaScript = "callAsyncJavaScript" + case createPdf = "createPdf" + case createWebArchiveData = "createWebArchiveData" + case saveWebArchive = "saveWebArchive" + case isSecureContext = "isSecureContext" + case createWebMessageChannel = "createWebMessageChannel" + case postWebMessage = "postWebMessage" + case addWebMessageListener = "addWebMessageListener" + case canScrollVertically = "canScrollVertically" + case canScrollHorizontally = "canScrollHorizontally" + case pauseAllMediaPlayback = "pauseAllMediaPlayback" + case setAllMediaPlaybackSuspended = "setAllMediaPlaybackSuspended" + case closeAllMediaPresentations = "closeAllMediaPresentations" + case requestMediaPlaybackState = "requestMediaPlaybackState" + case getMetaThemeColor = "getMetaThemeColor" + case isInFullscreen = "isInFullscreen" + case getCameraCaptureState = "getCameraCaptureState" + case setCameraCaptureState = "setCameraCaptureState" + case getMicrophoneCaptureState = "getMicrophoneCaptureState" + case setMicrophoneCaptureState = "setMicrophoneCaptureState" + case loadSimulatedRequest = "loadSimulatedRequest" +} diff --git a/macos/Classes/InAppWebViewFlutterPlugin.swift b/macos/Classes/InAppWebViewFlutterPlugin.swift new file mode 100644 index 00000000..c0c8043e --- /dev/null +++ b/macos/Classes/InAppWebViewFlutterPlugin.swift @@ -0,0 +1,8 @@ +import Cocoa +import FlutterMacOS + +public class InAppWebViewFlutterPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + SwiftFlutterPlugin.register(with: registrar) + } +} diff --git a/macos/Classes/InAppWebViewStatic.swift b/macos/Classes/InAppWebViewStatic.swift new file mode 100755 index 00000000..8d28d1db --- /dev/null +++ b/macos/Classes/InAppWebViewStatic.swift @@ -0,0 +1,81 @@ +// +// InAppWebViewStatic.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/12/2019. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class InAppWebViewStatic: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_static" + static var registrar: FlutterPluginRegistrar? + static var webViewForUserAgent: WKWebView? + static var defaultUserAgent: String? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: InAppWebViewStatic.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + InAppWebViewStatic.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getDefaultUserAgent": + InAppWebViewStatic.getDefaultUserAgent(completionHandler: { (value) in + result(value) + }) + break + case "handlesURLScheme": + let urlScheme = arguments!["urlScheme"] as! String + if #available(macOS 10.13, *) { + result(WKWebView.handlesURLScheme(urlScheme)) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + static public func getDefaultUserAgent(completionHandler: @escaping (_ value: String?) -> Void) { + if defaultUserAgent == nil { + InAppWebViewStatic.webViewForUserAgent = WKWebView() + InAppWebViewStatic.webViewForUserAgent?.evaluateJavaScript("navigator.userAgent") { (value, error) in + + if error != nil { + print("Error occured to get userAgent") + self.webViewForUserAgent = nil + completionHandler(nil) + return + } + + if let unwrappedUserAgent = value as? String { + InAppWebViewStatic.defaultUserAgent = unwrappedUserAgent + completionHandler(defaultUserAgent) + } else { + print("Failed to get userAgent") + } + self.webViewForUserAgent = nil + } + } else { + completionHandler(defaultUserAgent) + } + } + + public override func dispose() { + super.dispose() + InAppWebViewStatic.registrar = nil + InAppWebViewStatic.webViewForUserAgent = nil + InAppWebViewStatic.defaultUserAgent = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/LeakAvoider.swift b/macos/Classes/LeakAvoider.swift new file mode 100755 index 00000000..9f447142 --- /dev/null +++ b/macos/Classes/LeakAvoider.swift @@ -0,0 +1,26 @@ +// +// LeakAvoider.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation +import FlutterMacOS + +public class LeakAvoider: NSObject { + weak var delegate : FlutterMethodCallDelegate? + + init(delegate: FlutterMethodCallDelegate) { + super.init() + self.delegate = delegate + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + self.delegate?.handle(call, result: result) + } + + deinit { + debugPrint("LeakAvoider - dealloc") + } +} diff --git a/macos/Classes/MyCookieManager.swift b/macos/Classes/MyCookieManager.swift new file mode 100755 index 00000000..f42f83d3 --- /dev/null +++ b/macos/Classes/MyCookieManager.swift @@ -0,0 +1,309 @@ +// +// MyCookieManager.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/10/18. +// + +import Foundation +import WebKit +import FlutterMacOS + +@available(macOS 10.13, *) +public class MyCookieManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager" + static var registrar: FlutterPluginRegistrar? + static var httpCookieStore: WKHTTPCookieStore? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: MyCookieManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + MyCookieManager.registrar = registrar + MyCookieManager.httpCookieStore = WKWebsiteDataStore.default().httpCookieStore + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "setCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let value = arguments!["value"] as! String + let path = arguments!["path"] as! String + + var expiresDate: Int64? + if let expiresDateString = arguments!["expiresDate"] as? String { + expiresDate = Int64(expiresDateString) + } + + let maxAge = arguments!["maxAge"] as? Int64 + let isSecure = arguments!["isSecure"] as? Bool + let isHttpOnly = arguments!["isHttpOnly"] as? Bool + let sameSite = arguments!["sameSite"] as? String + let domain = arguments!["domain"] as? String + + MyCookieManager.setCookie(url: url, + name: name, + value: value, + path: path, + domain: domain, + expiresDate: expiresDate, + maxAge: maxAge, + isSecure: isSecure, + isHttpOnly: isHttpOnly, + sameSite: sameSite, + result: result) + break + case "getCookies": + let url = arguments!["url"] as! String + MyCookieManager.getCookies(url: url, result: result) + break + case "getAllCookies": + MyCookieManager.getAllCookies(result: result) + break + case "deleteCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookie(url: url, name: name, path: path, domain: domain, result: result) + break; + case "deleteCookies": + let url = arguments!["url"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookies(url: url, path: path, domain: domain, result: result) + break; + case "deleteAllCookies": + MyCookieManager.deleteAllCookies(result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func setCookie(url: String, + name: String, + value: String, + path: String, + domain: String?, + expiresDate: Int64?, + maxAge: Int64?, + isSecure: Bool?, + isHttpOnly: Bool?, + sameSite: String?, + result: @escaping FlutterResult) { + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(false) + return + } + + var properties: [HTTPCookiePropertyKey: Any] = [:] + properties[.originURL] = url + properties[.name] = name + properties[.value] = value + properties[.path] = path + + if domain != nil { + properties[.domain] = domain + } + + if expiresDate != nil { + // convert from milliseconds + properties[.expires] = Date(timeIntervalSince1970: TimeInterval(Double(expiresDate!)/1000)) + } + if maxAge != nil { + properties[.maximumAge] = String(maxAge!) + } + if isSecure != nil && isSecure! { + properties[.secure] = "TRUE" + } + if isHttpOnly != nil && isHttpOnly! { + properties[.init("HttpOnly")] = "YES" + } + if sameSite != nil { + if #available(macOS 10.15, *) { + var sameSiteValue = HTTPCookieStringPolicy(rawValue: "None") + switch sameSite { + case "Lax": + sameSiteValue = HTTPCookieStringPolicy.sameSiteLax + case "Strict": + sameSiteValue = HTTPCookieStringPolicy.sameSiteStrict + default: + break + } + properties[.sameSitePolicy] = sameSiteValue + } else { + properties[.init("SameSite")] = sameSite + } + } + + let cookie = HTTPCookie(properties: properties)! + + httpCookieStore.setCookie(cookie, completionHandler: {() in + result(true) + }) + } + + public static func getCookies(url: String, result: @escaping FlutterResult) { + var cookieList: [[String: Any?]] = [] + + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(cookieList) + return + } + + if let urlHost = URL(string: url)?.host { + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + if urlHost.hasSuffix(cookie.domain) || ".\(urlHost)".hasSuffix(cookie.domain) { + var sameSite: String? = nil + if #available(macOS 10.15, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + } + result(cookieList) + } + return + } else { + print("Cannot get WebView cookies. No HOST found for URL: \(url)") + } + + result(cookieList) + } + + public static func getAllCookies(result: @escaping FlutterResult) { + var cookieList: [[String: Any?]] = [] + + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(cookieList) + return + } + + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var sameSite: String? = nil + if #available(macOS 10.15, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + result(cookieList) + } + } + + public static func deleteCookie(url: String, name: String, path: String, domain: String?, result: @escaping FlutterResult) { + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(false) + return + } + + var domain = domain + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + domain = domainUrl.host + } + if let domain = domain, cookie.domain == domain, cookie.name == name, cookie.path == path { + httpCookieStore.delete(cookie, completionHandler: { + result(true) + }) + return + } + } + result(false) + } + } + + public static func deleteCookies(url: String, path: String, domain: String?, result: @escaping FlutterResult) { + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(false) + return + } + + var domain = domain + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + domain = domainUrl.host + } + if let domain = domain, cookie.domain == domain, cookie.path == path { + httpCookieStore.delete(cookie, completionHandler: nil) + } + } + result(true) + } + } + + public static func deleteAllCookies(result: @escaping FlutterResult) { + let websiteDataTypes = NSSet(array: [WKWebsiteDataTypeCookies]) + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes as! Set, modifiedSince: date as Date, completionHandler:{ + result(true) + }) + } + + public override func dispose() { + super.dispose() + MyCookieManager.registrar = nil + MyCookieManager.httpCookieStore = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/MyWebStorageManager.swift b/macos/Classes/MyWebStorageManager.swift new file mode 100755 index 00000000..274a8c3f --- /dev/null +++ b/macos/Classes/MyWebStorageManager.swift @@ -0,0 +1,112 @@ +// +// MyWebStorageManager.swift +// connectivity +// +// Created by Lorenzo Pichilli on 16/12/2019. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class MyWebStorageManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager" + static var registrar: FlutterPluginRegistrar? + static var websiteDataStore: WKWebsiteDataStore? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: MyWebStorageManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + MyWebStorageManager.registrar = registrar + MyWebStorageManager.websiteDataStore = WKWebsiteDataStore.default() + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "fetchDataRecords": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + MyWebStorageManager.fetchDataRecords(dataTypes: dataTypes, result: result) + break + case "removeDataFor": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let recordList = arguments!["recordList"] as! [[String: Any?]] + MyWebStorageManager.removeDataFor(dataTypes: dataTypes, recordList: recordList, result: result) + break + case "removeDataModifiedSince": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let timestamp = arguments!["timestamp"] as! Int64 + MyWebStorageManager.removeDataModifiedSince(dataTypes: dataTypes, timestamp: timestamp, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func fetchDataRecords(dataTypes: Set, result: @escaping FlutterResult) { + var recordList: [[String: Any?]] = [] + + guard let websiteDataStore = MyWebStorageManager.websiteDataStore else { + result(recordList) + return + } + + websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + recordList.append([ + "displayName": record.displayName, + "dataTypes": record.dataTypes.map({ (dataType) -> String in + return dataType + }) + ]) + } + result(recordList) + } + } + + public static func removeDataFor(dataTypes: Set, recordList: [[String: Any?]], result: @escaping FlutterResult) { + var records: [WKWebsiteDataRecord] = [] + + guard let websiteDataStore = MyWebStorageManager.websiteDataStore else { + result(false) + return + } + + websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + for r in recordList { + let displayName = r["displayName"] as! String + if (record.displayName == displayName) { + records.append(record) + break + } + } + } + websiteDataStore.removeData(ofTypes: dataTypes, for: records) { + result(true) + } + } + } + + public static func removeDataModifiedSince(dataTypes: Set, timestamp: Int64, result: @escaping FlutterResult) { + guard let websiteDataStore = MyWebStorageManager.websiteDataStore else { + result(false) + return + } + + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) + websiteDataStore.removeData(ofTypes: dataTypes, modifiedSince: date as Date) { + result(true) + } + } + + public override func dispose() { + super.dispose() + MyWebStorageManager.registrar = nil + MyWebStorageManager.websiteDataStore = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PlatformUtil.swift b/macos/Classes/PlatformUtil.swift new file mode 100644 index 00000000..cdd3c62e --- /dev/null +++ b/macos/Classes/PlatformUtil.swift @@ -0,0 +1,66 @@ +// +// PlatformUtil.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/03/21. +// + +import Foundation +import FlutterMacOS + +public class PlatformUtil: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_platformutil" + static var registrar: FlutterPluginRegistrar? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: PlatformUtil.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + InAppWebViewStatic.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getSystemVersion": + result(ProcessInfo.processInfo.operatingSystemVersionString) + break + case "formatDate": + let date = arguments!["date"] as! Int64 + let format = arguments!["format"] as! String + let locale = PlatformUtil.getLocaleFromString(locale: arguments!["locale"] as? String) + let timezone = TimeZone.init(abbreviation: arguments!["timezone"] as? String ?? "UTC")! + result(PlatformUtil.formatDate(date: date, format: format, locale: locale, timezone: timezone)) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + static public func getLocaleFromString(locale: String?) -> Locale { + guard let locale = locale else { + return Locale.init(identifier: "en_US") + } + return Locale.init(identifier: locale) + } + + static public func getDateFromMilliseconds(date: Int64) -> Date { + return Date(timeIntervalSince1970: TimeInterval(Double(date)/1000)) + } + + static public func formatDate(date: Int64, format: String, locale: Locale, timezone: TimeZone) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + formatter.timeZone = timezone + return formatter.string(from: PlatformUtil.getDateFromMilliseconds(date: date)) + } + + public override func dispose() { + super.dispose() + PlatformUtil.registrar = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift b/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift new file mode 100644 index 00000000..5622691a --- /dev/null +++ b/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift @@ -0,0 +1,21 @@ +// +// CallAsyncJavaScriptBelowIOS14WrapperJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS = """ +(function(obj) { + (async function(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES) { + \(PluginScriptsUtil.VAR_FUNCTION_BODY) + })(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES)).then(function(value) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': value, 'error': null, 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }).catch(function(error) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': null, 'error': error + '', 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }); + return null; +})(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ)); +""" diff --git a/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift b/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift new file mode 100644 index 00000000..69a92953 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift @@ -0,0 +1,58 @@ +// +// ConsoleLogJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_CONSOLE_LOG_JS_PLUGIN_SCRIPT" + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT = PluginScript( + groupName: CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: CONSOLE_LOG_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"]) + +// the message needs to be concatenated with '' in order to have the same behavior like on Android +let CONSOLE_LOG_JS_SOURCE = """ +(function(console) { + + var oldLogs = { + 'consoleLog': console.log, + 'consoleDebug': console.debug, + 'consoleError': console.error, + 'consoleInfo': console.info, + 'consoleWarn': console.warn + }; + + for (var k in oldLogs) { + (function(oldLog) { + console[oldLog.replace('console', '').toLowerCase()] = function() { + oldLogs[oldLog].apply(null, arguments); + var args = arguments; + + // on iOS, for some reason, accessing the arguments object synchronously can throw some errors, such as "TypeError" + // see https://github.com/pichillilorenzo/flutter_inappwebview/issues/776 + setTimeout(function() { + var message = ''; + for (var i in args) { + if (message == '') { + message += args[i]; + } + else { + message += ' ' + args[i]; + } + } + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers[oldLog].postMessage({'message': message, '_windowId': _windowId}); + }); + } + })(k); + } +})(window.console); +""" diff --git a/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift b/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift new file mode 100644 index 00000000..593ba00e --- /dev/null +++ b/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift @@ -0,0 +1,36 @@ +// +// EnableViewportScaleJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT" + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ENABLE_VIEWPORT_SCALE_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width'); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" + +let NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" diff --git a/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift b/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift new file mode 100644 index 00000000..b3c22282 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift @@ -0,0 +1,73 @@ +// +// FindElementsAtPointJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT" + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_ELEMENTS_AT_POINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +/** + https://developer.android.com/reference/android/webkit/WebView.HitTestResult + */ +let FIND_ELEMENTS_AT_POINT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint = function(x, y) { + var hitTestResultType = { + UNKNOWN_TYPE: 0, + PHONE_TYPE: 2, + GEO_TYPE: 3, + EMAIL_TYPE: 4, + IMAGE_TYPE: 5, + SRC_ANCHOR_TYPE: 7, + SRC_IMAGE_ANCHOR_TYPE: 8, + EDIT_TEXT_TYPE: 9 + }; + var element = document.elementFromPoint(x, y); + var data = { + type: 0, + extra: null + }; + while (element) { + if (element.tagName === 'IMG' && element.src) { + if (element.parentNode && element.parentNode.tagName === 'A' && element.parentNode.href) { + data.type = hitTestResultType.SRC_IMAGE_ANCHOR_TYPE; + } else { + data.type = hitTestResultType.IMAGE_TYPE; + } + data.extra = element.src; + break; + } else if (element.tagName === 'A' && element.href) { + if (element.href.indexOf('mailto:') === 0) { + data.type = hitTestResultType.EMAIL_TYPE; + data.extra = element.href.replace('mailto:', ''); + } else if (element.href.indexOf('tel:') === 0) { + data.type = hitTestResultType.PHONE_TYPE; + data.extra = element.href.replace('tel:', ''); + } else if (element.href.indexOf('geo:') === 0) { + data.type = hitTestResultType.GEO_TYPE; + data.extra = element.href.replace('geo:', ''); + } else { + data.type = hitTestResultType.SRC_ANCHOR_TYPE; + data.extra = element.href; + } + break; + } else if ( + (element.tagName === 'INPUT' && ['text', 'email', 'password', 'number', 'search', 'tel', 'url'].indexOf(element.type) >= 0) || + element.tagName === 'TEXTAREA') { + data.type = hitTestResultType.EDIT_TEXT_TYPE + } + element = element.parentNode; + } + return data; +} +""" diff --git a/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift b/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift new file mode 100644 index 00000000..a61a05b7 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift @@ -0,0 +1,187 @@ +// +// FindTextHighlightJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT" +let FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._searchResultCount" +let FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._currentHighlight" +let FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._isDoneCounting" + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_TEXT_HIGHLIGHT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onFindResultReceived"]) + +let FIND_TEXT_HIGHLIGHT_JS_SOURCE = """ +\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement = function(element, keyword) { + if (element) { + if (element.nodeType == 3) { + // Text node + + var elementTmp = element; + while (true) { + var value = elementTmp.nodeValue; // Search for keyword in text node + var idx = value.toLowerCase().indexOf(keyword); + + if (idx < 0) break; + + var span = document.createElement("span"); + var text = document.createTextNode(value.substr(idx, keyword.length)); + span.appendChild(text); + + span.setAttribute( + "id", + "\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ); + span.setAttribute("class", "\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + var backgroundColor = \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) == 0 ? "#FF9732" : "#FFFF00"; + span.setAttribute("style", "color: #000 !important; background: " + backgroundColor + " !important; padding: 0px !important; margin: 0px !important; border: 0px !important;"); + + text = document.createTextNode(value.substr(idx + keyword.length)); + element.deleteData(idx, value.length - idx); + + var next = element.nextSibling; + element.parentNode.insertBefore(span, next); + element.parentNode.insertBefore(text, next); + element = text; + + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE)++; + elementTmp = document.createTextNode( + value.substr(idx + keyword.length) + ); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } + } else if (element.nodeType == 1) { + // Element node + if ( + element.style.display != "none" && + element.nodeName.toLowerCase() != "select" + ) { + for (var i = element.childNodes.length - 1; i >= 0; i--) { + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement( + element.childNodes[element.childNodes.length - 1 - i], + keyword + ); + } + } + } + } +} + +// the main entry point to start the search +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync = function(keyword) { + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches(); + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement(document.body, keyword.toLowerCase()); + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = true; + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); +} + +// helper function, recursively removes the highlights in elements and their childs +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement = function(element) { + if (element) { + if (element.nodeType == 1) { + if (element.getAttribute("class") == "\(JAVASCRIPT_BRIDGE_NAME)_Highlight") { + var text = element.removeChild(element.firstChild); + element.parentNode.insertBefore(text, element); + element.parentNode.removeChild(element); + return true; + } else { + var normalize = false; + for (var i = element.childNodes.length - 1; i >= 0; i--) { + if (window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(element.childNodes[i])) { + normalize = true; + } + } + if (normalize) { + element.normalize(); + } + } + } + } + return false; +} + +// the main entry point to remove the highlights +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches = function() { + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(document.body); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._findNext = function(forward) { + if (\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) <= 0) return; + + var idx = \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) + (forward ? +1 : -1); + idx = + idx < 0 + ? \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) - 1 + : idx >= \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ? 0 + : idx; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = idx; + + var scrollTo = document.getElementById("\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + idx); + if (scrollTo) { + var highlights = document.getElementsByClassName("\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + for (var i = 0; i < highlights.length; i++) { + var span = highlights[i]; + span.style.backgroundColor = "#FFFF00"; + } + scrollTo.style.backgroundColor = "#FF9732"; + + scrollTo.scrollIntoView({ + behavior: "auto", + block: "center" + }); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } +} +""" diff --git a/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift b/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift new file mode 100644 index 00000000..b5d3a54f --- /dev/null +++ b/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift @@ -0,0 +1,251 @@ +// +// InterceptAjaxRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptAjaxRequest" + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_AJAX_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) = true; +(function(ajax) { + var send = ajax.prototype.send; + var open = ajax.prototype.open; + var setRequestHeader = ajax.prototype.setRequestHeader; + ajax.prototype._flutter_inappwebview_url = null; + ajax.prototype._flutter_inappwebview_method = null; + ajax.prototype._flutter_inappwebview_isAsync = null; + ajax.prototype._flutter_inappwebview_user = null; + ajax.prototype._flutter_inappwebview_password = null; + ajax.prototype._flutter_inappwebview_password = null; + ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false; + ajax.prototype._flutter_inappwebview_request_headers = {}; + function convertRequestResponse(request, callback) { + if (request.response != null && request.responseType != null) { + switch (request.responseType) { + case 'arraybuffer': + callback(new Uint8Array(request.response)); + return; + case 'blob': + const reader = new FileReader(); + reader.addEventListener('loadend', function() { + callback(new Uint8Array(reader.result)); + }); + reader.readAsArrayBuffer(blob); + return; + case 'document': + callback(request.response.documentElement.outerHTML); + return; + case 'json': + callback(request.response); + return; + }; + } + callback(null); + }; + ajax.prototype.open = function(method, url, isAsync, user, password) { + isAsync = (isAsync != null) ? isAsync : true; + this._flutter_inappwebview_url = url; + this._flutter_inappwebview_method = method; + this._flutter_inappwebview_isAsync = isAsync; + this._flutter_inappwebview_user = user; + this._flutter_inappwebview_password = password; + this._flutter_inappwebview_request_headers = {}; + open.call(this, method, url, isAsync, user, password); + }; + ajax.prototype.setRequestHeader = function(header, value) { + this._flutter_inappwebview_request_headers[header] = value; + setRequestHeader.call(this, header, value); + }; + function handleEvent(e) { + var self = this; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + var headers = this.getAllResponseHeaders(); + var responseHeaders = {}; + if (headers != null) { + var arr = headers.trim().split(/[\\r\\n]+/); + arr.forEach(function (line) { + var parts = line.split(': '); + var header = parts.shift(); + var value = parts.join(': '); + responseHeaders[header] = value; + }); + } + convertRequestResponse(this, function(response) { + var ajaxRequest = { + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + readyState: self.readyState, + status: self.status, + responseURL: self.responseURL, + responseType: self.responseType, + response: response, + responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, + responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, + statusText: self.statusText, + responseHeaders, responseHeaders, + event: { + type: e.type, + loaded: e.loaded, + lengthComputable: e.lengthComputable, + total: e.total + } + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxProgress', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + } + }); + }); + } + }; + ajax.prototype.send = function(data) { + var self = this; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) { + this._flutter_inappwebview_already_onreadystatechange_wrapped = true; + var onreadystatechange = this.onreadystatechange; + this.onreadystatechange = function() { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + var headers = this.getAllResponseHeaders(); + var responseHeaders = {}; + if (headers != null) { + var arr = headers.trim().split(/[\\r\\n]+/); + arr.forEach(function (line) { + var parts = line.split(': '); + var header = parts.shift(); + var value = parts.join(': '); + responseHeaders[header] = value; + }); + } + convertRequestResponse(this, function(response) { + var ajaxRequest = { + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + readyState: self.readyState, + status: self.status, + responseURL: self.responseURL, + responseType: self.responseType, + response: response, + responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, + responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, + statusText: self.statusText, + responseHeaders: responseHeaders + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + } + if (onreadystatechange != null) { + onreadystatechange(); + } + }); + }); + } else if (onreadystatechange != null) { + onreadystatechange(); + } + }; + } + this.addEventListener('loadstart', handleEvent); + this.addEventListener('load', handleEvent); + this.addEventListener('loadend', handleEvent); + this.addEventListener('progress', handleEvent); + this.addEventListener('error', handleEvent); + this.addEventListener('abort', handleEvent); + this.addEventListener('timeout', handleEvent); + \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(data).then(function(data) { + var ajaxRequest = { + data: data, + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + responseType: self.responseType + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + if (result.data != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) && result.data.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.data); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(JAVASCRIPT_UTIL_VAR_NAME).getFormDataContentType(bodyString); + if (result.headers != null) { + result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type']; + } else { + result.headers = { 'Content-Type': formDataContentType }; + } + } + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) || result.data == null) { + data = result.data; + } else if (result.data.length > 0) { + data = new Uint8Array(result.data); + } + self.withCredentials = result.withCredentials; + if (result.responseType != null) { + self.responseType = result.responseType; + }; + if (result.headers != null) { + for (var header in result.headers) { + var value = result.headers[header]; + var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header]; + if (flutter_inappwebview_value == null) { + self._flutter_inappwebview_request_headers[header] = value; + } else { + self._flutter_inappwebview_request_headers[header] += ', ' + value; + } + setRequestHeader.call(self, header, value); + }; + } + if ((self._flutter_inappwebview_method != result.method && result.method != null) || (self._flutter_inappwebview_url != result.url && result.url != null)) { + self.abort(); + self.open(result.method, result.url, result.isAsync, result.user, result.password); + return; + } + } + send.call(self, data); + }); + }); + } else { + send.call(this, data); + } + }; +})(window.XMLHttpRequest); +""" diff --git a/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift b/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift new file mode 100644 index 00000000..14539811 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift @@ -0,0 +1,153 @@ +// +// InterceptFetchRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptFetchRequest" + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_FETCH_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) = true; +(function(fetch) { + if (fetch == null) { + return; + } + window.fetch = async function(resource, init) { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == true) { + var fetchRequest = { + url: null, + method: null, + headers: null, + body: null, + mode: null, + credentials: null, + cache: null, + redirect: null, + referrer: null, + referrerPolicy: null, + integrity: null, + keepalive: null + }; + if (resource instanceof Request) { + fetchRequest.url = resource.url; + fetchRequest.method = resource.method; + fetchRequest.headers = resource.headers; + fetchRequest.body = resource.body; + fetchRequest.mode = resource.mode; + fetchRequest.credentials = resource.credentials; + fetchRequest.cache = resource.cache; + fetchRequest.redirect = resource.redirect; + fetchRequest.referrer = resource.referrer; + fetchRequest.referrerPolicy = resource.referrerPolicy; + fetchRequest.integrity = resource.integrity; + fetchRequest.keepalive = resource.keepalive; + } else { + fetchRequest.url = resource != null ? resource.toString() : null; + if (init != null) { + fetchRequest.method = init.method; + fetchRequest.headers = init.headers; + fetchRequest.body = init.body; + fetchRequest.mode = init.mode; + fetchRequest.credentials = init.credentials; + fetchRequest.cache = init.cache; + fetchRequest.redirect = init.redirect; + fetchRequest.referrer = init.referrer; + fetchRequest.referrerPolicy = init.referrerPolicy; + fetchRequest.integrity = init.integrity; + fetchRequest.keepalive = init.keepalive; + } + } + if (fetchRequest.headers instanceof Headers) { + fetchRequest.headers = \(JAVASCRIPT_UTIL_VAR_NAME).convertHeadersToJson(fetchRequest.headers); + } + fetchRequest.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertCredentialsToJson(fetchRequest.credentials); + return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(fetchRequest.body).then(function(body) { + fetchRequest.body = body; + return window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) { + if (result != null) { + switch (result.action) { + case 0: + var controller = new AbortController(); + if (init != null) { + init.signal = controller.signal; + } else { + init = { + signal: controller.signal + }; + } + controller.abort(); + break; + } + if (result.body != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) && result.body.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.body); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(JAVASCRIPT_UTIL_VAR_NAME).getFormDataContentType(bodyString); + if (result.headers != null) { + result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type']; + } else { + result.headers = { 'Content-Type': formDataContentType }; + } + } + } + resource = result.url; + if (init == null) { + init = {}; + } + if (result.method != null && result.method.length > 0) { + init.method = result.method; + } + if (result.headers != null && Object.keys(result.headers).length > 0) { + init.headers = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToHeaders(result.headers); + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) || result.body == null) { + init.body = result.body; + } else if (result.body.length > 0) { + init.body = new Uint8Array(result.body); + } + if (result.mode != null && result.mode.length > 0) { + init.mode = result.mode; + } + if (result.credentials != null) { + init.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToCredential(result.credentials); + } + if (result.cache != null && result.cache.length > 0) { + init.cache = result.cache; + } + if (result.redirect != null && result.redirect.length > 0) { + init.redirect = result.redirect; + } + if (result.referrer != null && result.referrer.length > 0) { + init.referrer = result.referrer; + } + if (result.referrerPolicy != null && result.referrerPolicy.length > 0) { + init.referrerPolicy = result.referrerPolicy; + } + if (result.integrity != null && result.integrity.length > 0) { + init.integrity = result.integrity; + } + if (result.keepalive != null) { + init.keepalive = result.keepalive; + } + return fetch(resource, init); + } + return fetch(resource, init); + }); + }); + } else { + return fetch(resource, init); + } + }; +})(window.fetch); +""" diff --git a/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift b/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift new file mode 100644 index 00000000..cddca764 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift @@ -0,0 +1,243 @@ +// +// javaScriptBridgeJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview" +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT" + +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: JAVASCRIPT_BRIDGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["callHandler"]) + +let JAVASCRIPT_BRIDGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME) = {}; +\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME) = {}; +window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + var _callHandlerID = setTimeout(function(){}); + window.webkit.messageHandlers['callHandler'].postMessage( {'handlerName': arguments[0], '_callHandlerID': _callHandlerID, 'args': JSON.stringify(Array.prototype.slice.call(arguments, 1)), '_windowId': _windowId} ); + return new Promise(function(resolve, reject) { + window.\(JAVASCRIPT_BRIDGE_NAME)[_callHandlerID] = resolve; + }); +}; +\(WEB_MESSAGE_LISTENER_JS_SOURCE) +\(UTIL_JS_SOURCE) +""" + +let PLATFORM_READY_JS_SOURCE = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));"; + +let JAVASCRIPT_UTIL_VAR_NAME = "window.\(JAVASCRIPT_BRIDGE_NAME)._Util" + +/* + https://github.com/github/fetch/blob/master/fetch.js + */ +let UTIL_JS_SOURCE = """ +\(JAVASCRIPT_UTIL_VAR_NAME) = { + support: { + searchParams: 'URLSearchParams' in window, + iterable: 'Symbol' in window && 'iterator' in Symbol, + blob: + 'FileReader' in window && + 'Blob' in window && + (function() { + try { + new Blob(); + return true; + } catch (e) { + return false; + } + })(), + formData: 'FormData' in window, + arrayBuffer: 'ArrayBuffer' in window + }, + isDataView: function(obj) { + return obj && DataView.prototype.isPrototypeOf(obj); + }, + fileReaderReady: function(reader) { + return new Promise(function(resolve, reject) { + reader.onload = function() { + resolve(reader.result); + }; + reader.onerror = function() { + reject(reader.error); + }; + }); + }, + readBlobAsArrayBuffer: function(blob) { + var reader = new FileReader(); + var promise = \(JAVASCRIPT_UTIL_VAR_NAME).fileReaderReady(reader); + reader.readAsArrayBuffer(blob); + return promise; + }, + convertBodyToArrayBuffer: function(body) { + var viewClasses = [ + '[object Int8Array]', + '[object Uint8Array]', + '[object Uint8ClampedArray]', + '[object Int16Array]', + '[object Uint16Array]', + '[object Int32Array]', + '[object Uint32Array]', + '[object Float32Array]', + '[object Float64Array]' + ]; + var isArrayBufferView = null; + if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer) { + isArrayBufferView = + ArrayBuffer.isView || + function(obj) { + return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1; + }; + } + + var bodyUsed = false; + + this._bodyInit = body; + if (!body) { + this._bodyText = ''; + } else if (typeof body === 'string') { + this._bodyText = body; + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body; + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body; + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this._bodyText = body.toString(); + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer && \(JAVASCRIPT_UTIL_VAR_NAME).support.blob && \(JAVASCRIPT_UTIL_VAR_NAME).isDataView(body)) { + this._bodyArrayBuffer = bufferClone(body.buffer); + this._bodyInit = new Blob([this._bodyArrayBuffer]); + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + this._bodyArrayBuffer = bufferClone(body); + } else { + this._bodyText = body = Object.prototype.toString.call(body); + } + + this.blob = function () { + if (bodyUsed) { + return Promise.reject(new TypeError('Already read')); + } + bodyUsed = true; + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob); + } else if (this._bodyArrayBuffer) { + return Promise.resolve(new Blob([this._bodyArrayBuffer])); + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob'); + } else { + return Promise.resolve(new Blob([this._bodyText])); + } + }; + + if (this._bodyArrayBuffer) { + if (bodyUsed) { + return Promise.reject(new TypeError('Already read')); + } + bodyUsed = true; + if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + return Promise.resolve( + this._bodyArrayBuffer.buffer.slice( + this._bodyArrayBuffer.byteOffset, + this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength + ) + ); + } else { + return Promise.resolve(this._bodyArrayBuffer); + } + } + return this.blob().then(\(JAVASCRIPT_UTIL_VAR_NAME).readBlobAsArrayBuffer); + }, + isString: function(variable) { + return typeof variable === 'string' || variable instanceof String; + }, + convertBodyRequest: function(body) { + if (body == null) { + return new Promise((resolve, reject) => resolve(null)); + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(body) || (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && body instanceof URLSearchParams)) { + return new Promise((resolve, reject) => resolve(body.toString())); + } + if (window.Response != null) { + return new Response(body).arrayBuffer().then(function(arrayBuffer) { + return Array.from(new Uint8Array(arrayBuffer)); + }); + } + return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyToArrayBuffer(body).then(function(arrayBuffer) { + return Array.from(new Uint8Array(arrayBuffer)); + }); + }, + arrayBufferToString: function(arrayBuffer) { + var uint8Array = new Uint8Array(arrayBuffer); + return uint8Array.reduce(function(acc, i) { return acc += String.fromCharCode.apply(null, [i]); }, ''); + }, + isBodyFormData: function(bodyString) { + return bodyString.indexOf('------WebKitFormBoundary') >= 0; + }, + getFormDataContentType: function(bodyString) { + var boundary = bodyString.substr(2, 40); + return 'multipart/form-data; boundary=' + boundary; + }, + convertHeadersToJson: function(headers) { + var headersObj = {}; + for (var header of headers.keys()) { + var value = headers.get(header); + headersObj[header] = value; + } + return headersObj; + }, + convertJsonToHeaders: function(headersJson) { + return new Headers(headersJson); + }, + convertCredentialsToJson: function(credentials) { + var credentialsObj = {}; + if (window.FederatedCredential != null && credentials instanceof FederatedCredential) { + credentialsObj.type = credentials.type; + credentialsObj.id = credentials.id; + credentialsObj.name = credentials.name; + credentialsObj.protocol = credentials.protocol; + credentialsObj.provider = credentials.provider; + credentialsObj.iconURL = credentials.iconURL; + } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) { + credentialsObj.type = credentials.type; + credentialsObj.id = credentials.id; + credentialsObj.name = credentials.name; + credentialsObj.password = credentials.password; + credentialsObj.iconURL = credentials.iconURL; + } else { + credentialsObj.type = 'default'; + credentialsObj.value = credentials; + } + return credentialsObj; + }, + convertJsonToCredential: function(credentialsJson) { + var credentials; + if (window.FederatedCredential != null && credentialsJson.type === 'federated') { + credentials = new FederatedCredential({ + id: credentialsJson.id, + name: credentialsJson.name, + protocol: credentialsJson.protocol, + provider: credentialsJson.provider, + iconURL: credentialsJson.iconURL + }); + } else if (window.PasswordCredential != null && credentialsJson.type === 'password') { + credentials = new PasswordCredential({ + id: credentialsJson.id, + name: credentialsJson.name, + password: credentialsJson.password, + iconURL: credentialsJson.iconURL + }); + } else { + credentials = credentialsJson.value == null ? undefined : credentialsJson.value; + } + return credentials; + } +}; +""" diff --git a/macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift b/macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift new file mode 100644 index 00000000..ef2e7d67 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift @@ -0,0 +1,62 @@ +// +// LastTouchedAnchorOrImageJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT" + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = null; +window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = null; +(function() { + document.addEventListener('touchstart', function(event) { + var target = event.target; + while (target) { + if (target.tagName === 'IMG') { + var img = target; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = { + url: img.src + }; + var parent = img.parentNode; + while (parent) { + if (parent.tagName === 'A') { + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: parent.textContent, + url: parent.href, + src: img.src + }; + break; + } + parent = parent.parentNode; + } + return; + } else if (target.tagName === 'A') { + var link = target; + var images = link.getElementsByTagName('img'); + var img = (images.length > 0) ? images[0] : null; + var imgSrc = (img != null) ? img.src : null; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = (img != null) ? {url: imgSrc} : window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: link.textContent, + url: link.href, + src: imgSrc + }; + return; + } + target = target.parentNode; + } + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift b/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift new file mode 100644 index 00000000..828de9ef --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift @@ -0,0 +1,39 @@ +// +// resourceObserverJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useOnLoadResource" + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_LOAD_RESOURCE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_LOAD_RESOURCE_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) = true; +(function() { + var observer = new PerformanceObserver(function(list) { + list.getEntries().forEach(function(entry) { + if (\(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) == true) { + var resource = { + "url": entry.name, + "initiatorType": entry.initiatorType, + "startTime": entry.startTime, + "duration": entry.duration + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onLoadResource", resource); + } + }); + }); + observer.observe({entryTypes: ['resource']}); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift b/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift new file mode 100644 index 00000000..4418c042 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift @@ -0,0 +1,33 @@ +// +// OnScrollEvent.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/10/22. +// + +import Foundation + +let ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT" + +let ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_SCROLL_CHANGED_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onScrollChanged"]) + +let ON_SCROLL_CHANGED_EVENT_JS_SOURCE = """ +(function(){ + document.addEventListener('scroll', function(e) { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers["onScrollChanged"].postMessage( + { + x: window.scrollX, + y: window.scrollY, + _windowId: _windowId + } + ); + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift b/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift new file mode 100644 index 00000000..345b5ece --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowBlurEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_WINDOW_BLUR_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_BLUR_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('blur', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowBlur'); + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift b/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift new file mode 100644 index 00000000..9ebda3e9 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowFocusEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + source: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_FOCUS_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('focus', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowFocus'); + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift b/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift new file mode 100644 index 00000000..a1c6637d --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift @@ -0,0 +1,31 @@ +// +// OriginalViewPortMetaTagContentJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT" + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = ""; +(function() { + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + if (metaTagNode.name === "viewport") { + window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = metaTagNode.content; + } + } +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift b/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift new file mode 100644 index 00000000..51f9c867 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift @@ -0,0 +1,31 @@ +// +// PluginScripts.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class PluginScriptsUtil { + public static let VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE" + public static let VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES" + public static let VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES" + public static let VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ" + public static let VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY" + public static let VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID" + + public static let GET_SELECTED_TEXT_JS_SOURCE = """ +(function(){ + var txt; + if (window.getSelection) { + txt = window.getSelection().toString(); + } else if (window.document.getSelection) { + txt = window.document.getSelection().toString(); + } else if (window.document.selection) { + txt = window.document.selection.createRange().text; + } + return txt; +})(); +""" +} diff --git a/macos/Classes/PluginScriptsJS/PrintJS.swift b/macos/Classes/PluginScriptsJS/PrintJS.swift new file mode 100644 index 00000000..5ac12691 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/PrintJS.swift @@ -0,0 +1,24 @@ +// +// PrintJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT" + +let PRINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PRINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let PRINT_JS_SOURCE = """ +window.print = function() { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onPrintRequest", window.location.href); +} +""" diff --git a/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift b/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift new file mode 100644 index 00000000..d2ef3e2a --- /dev/null +++ b/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift @@ -0,0 +1,27 @@ +// +// PromisePolyfillJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT" + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PROMISE_POLYFILL_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +// https://github.com/tildeio/rsvp.js +let PROMISE_POLYFILL_JS_SOURCE = """ +if (window.Promise == null) { + !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.RSVP={})}(this,function(t){"use strict";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if("function"!=typeof r)throw new TypeError("Callback must be a function");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s= 0) { + this.listeners.splice(index, 1); + } +}; + +window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6 = function(ip_string) { + // replace ipv4 address if any + var ipv4 = ip_string.match(/(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)/); + if (ipv4) { + ip_string = ipv4[1]; + ipv4 = ipv4[2].match(/[0-9]+/g); + for (var i = 0;i < 4;i ++) { + var byte = parseInt(ipv4[i],10); + ipv4[i] = ("0" + byte.toString(16)).substr(-2); + } + ip_string += ipv4[0] + ipv4[1] + ':' + ipv4[2] + ipv4[3]; + } + + // take care of leading and trailing :: + ip_string = ip_string.replace(/^:|:$/g, ''); + + var ipv6 = ip_string.split(':'); + + for (var i = 0; i < ipv6.length; i ++) { + var hex = ipv6[i]; + if (hex != "") { + // normalize leading zeros + ipv6[i] = ("0000" + hex).substr(-4); + } + else { + // normalize grouped zeros :: + hex = []; + for (var j = ipv6.length; j <= 8; j ++) { + hex.push('0000'); + } + ipv6[i] = hex.join(':'); + } + } + + return ipv6.join(':'); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed = function(allowedOriginRules, scheme, host, port) { + for (var rule of allowedOriginRules) { + if (rule === "*") { + return true; + } + if (scheme == null || scheme === "") { + continue; + } + if ((scheme == null || scheme === "") && (host == null || host === "") && (port === 0 || port === "" || port == null)) { + continue; + } + var rulePort = rule.port == null || rule.port === 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port; + var currentPort = port === 0 || port === "" || port == null ? (scheme == "https" ? 443 : 80) : port; + var IPv6 = null; + if (rule.host != null && rule.host[0] === "[") { + try { + IPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(rule.host.substring(1, rule.host.length - 1)); + } catch {} + } + var hostIPv6 = null; + try { + hostIPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(host); + } catch {} + + var schemeAllowed = scheme == rule.scheme; + + var hostAllowed = rule.host == null || + rule.host === "" || + host === rule.host || + (rule.host[0] === "*" && host != null && host.indexOf(rule.host.split("*")[1]) >= 0) || + (hostIPv6 != null && IPv6 != null && hostIPv6 === IPv6); + + var portAllowed = rulePort === currentPort; + + if (schemeAllowed && hostAllowed && portAllowed) { + return true; + } + } + return false; +} +""" diff --git a/macos/Classes/PluginScriptsJS/WindowIdJS.swift b/macos/Classes/PluginScriptsJS/WindowIdJS.swift new file mode 100644 index 00000000..8b3a1879 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/WindowIdJS.swift @@ -0,0 +1,19 @@ +// +// WindowIdJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let WINDOW_ID_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_WINDOW_ID_JS_PLUGIN_SCRIPT" + +let WINDOW_ID_VARIABLE_JS_SOURCE = "window._\(JAVASCRIPT_BRIDGE_NAME)_windowId" + +let WINDOW_ID_INITIALIZE_JS_SOURCE = """ +(function() { + \(WINDOW_ID_VARIABLE_JS_SOURCE) = \(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE); + return \(WINDOW_ID_VARIABLE_JS_SOURCE); +})() +""" diff --git a/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift b/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift new file mode 100644 index 00000000..c1976a15 --- /dev/null +++ b/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift @@ -0,0 +1,34 @@ +// +// CustomUIPrintPageRenderer.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation +// +//public class CustomUIPrintPageRenderer : UIPrintPageRenderer { +// private var _numberOfPages: Int? +// private var forceRenderingQuality: Int? +// +// public init(numberOfPage: Int? = nil, forceRenderingQuality: Int? = nil) { +// super.init() +// self._numberOfPages = numberOfPage +// self.forceRenderingQuality = forceRenderingQuality +// } +// +// open override var numberOfPages: Int { +// get { +// return _numberOfPages ?? super.numberOfPages +// } +// } +// +// @available(iOS 14.5, *) +// open override func currentRenderingQuality(forRequested requestedRenderingQuality: UIPrintRenderingQuality) -> UIPrintRenderingQuality { +// if let forceRenderingQuality = forceRenderingQuality, +// let quality = UIPrintRenderingQuality.init(rawValue: forceRenderingQuality) { +// return quality +// } +// return super.currentRenderingQuality(forRequested: requestedRenderingQuality) +// } +//} diff --git a/macos/Classes/PrintJob/PrintAttributes.swift b/macos/Classes/PrintJob/PrintAttributes.swift new file mode 100644 index 00000000..21178457 --- /dev/null +++ b/macos/Classes/PrintJob/PrintAttributes.swift @@ -0,0 +1,42 @@ +// +// PrintAttributes.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintAttributes : NSObject { + var orientation: NSPrintInfo.PaperOrientation? + var margins: NSEdgeInsets? + var paperRect: CGRect? + var colorMode: String? + var duplex: Int? + + public init(fromPrintJobController: PrintJobController) { + super.init() + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + orientation = printInfo.orientation + margins = NSEdgeInsets(top: printInfo.topMargin, + left: printInfo.leftMargin, + bottom: printInfo.bottomMargin, + right: printInfo.rightMargin) + paperRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: printInfo.paperSize) + colorMode = printInfo.printSettings["ColorModel"] as? String + duplex = printInfo.printSettings["com_apple_print_PrintSettings_PMDuplexing"] as? Int + print(printInfo.printSettings) + } + } + + public func toMap () -> [String:Any?] { + return [ + "paperRect": paperRect?.toMap(), + "margins": margins?.toMap(), + "orientation": orientation?.rawValue, + "colorMode": colorMode, + "duplex": duplex + ] + } +} diff --git a/macos/Classes/PrintJob/PrintJobChannelDelegate.swift b/macos/Classes/PrintJob/PrintJobChannelDelegate.swift new file mode 100644 index 00000000..4c8dcd48 --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobChannelDelegate.swift @@ -0,0 +1,60 @@ +// +// PrintJobChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation +import FlutterMacOS + +public class PrintJobChannelDelegate : ChannelDelegate { + private weak var printJobController: PrintJobController? + + public init(printJobController: PrintJobController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.printJobController = printJobController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getInfo": + if let printJobController = printJobController { + result(printJobController.getInfo()?.toMap()) + } else { + result(false) + } + break + case "dispose": + if let printJobController = printJobController { + printJobController.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(completed: Bool, error: Error?) { + let arguments: [String: Any?] = [ + "completed": completed, + "error": error?.localizedDescription + ] + channel?.invokeMethod("onComplete", arguments: arguments) + } + + public override func dispose() { + super.dispose() + printJobController = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PrintJob/PrintJobController.swift b/macos/Classes/PrintJob/PrintJobController.swift new file mode 100644 index 00000000..30637c2b --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobController.swift @@ -0,0 +1,88 @@ +// +// PrintJob.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation +import FlutterMacOS + +public enum PrintJobState: Int { + case created = 1 + case started = 3 + case completed = 5 + case failed = 6 + case canceled = 7 +} + +public class PrintJobController : NSObject, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_printjobcontroller_" + var id: String + var job: NSPrintOperation? + var settings: PrintJobSettings? + var channelDelegate: PrintJobChannelDelegate? + var state = PrintJobState.created + var creationTime = Int64(Date().timeIntervalSince1970 * 1000) + private var completionHandler: PrintJobController.CompletionHandler? + + public typealias CompletionHandler = (_ printOperation: NSPrintOperation, + _ success: Bool, + _ contextInfo: UnsafeMutableRawPointer?) -> Void + + public init(id: String, job: NSPrintOperation? = nil, settings: PrintJobSettings? = nil) { + self.id = id + super.init() + self.job = job + self.settings = settings + let channel = FlutterMethodChannel(name: PrintJobController.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = PrintJobChannelDelegate(printJobController: self, channel: channel) + } + + public func present(parentWindow: NSWindow? = nil, completionHandler: PrintJobController.CompletionHandler? = nil) { + guard let job = job else { + return + } + state = .started + self.completionHandler = completionHandler + if let mainWindow = parentWindow ?? NSApplication.shared.mainWindow { + job.runModal(for: mainWindow, delegate: self, didRun: #selector(printOperationDidRun), contextInfo: nil) + } + } + + @objc func printOperationDidRun(printOperation: NSPrintOperation, + success: Bool, + contextInfo: UnsafeMutableRawPointer?) { + state = success ? .completed : .canceled + channelDelegate?.onComplete(completed: success, error: nil) + if let completionHandler = completionHandler { + completionHandler(printOperation, success, contextInfo) + self.completionHandler = nil + } + } + + public func getInfo() -> PrintJobInfo? { + guard let _ = job else { + return nil + } + + return PrintJobInfo.init(fromPrintJobController: self) + } + + public func disposeNoDismiss() { + channelDelegate?.dispose() + channelDelegate = nil + completionHandler = nil + job = nil + PrintJobManager.jobs[id] = nil + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + completionHandler = nil + job = nil + PrintJobManager.jobs[id] = nil + } +} diff --git a/macos/Classes/PrintJob/PrintJobInfo.swift b/macos/Classes/PrintJob/PrintJobInfo.swift new file mode 100644 index 00000000..6937c077 --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobInfo.swift @@ -0,0 +1,53 @@ +// +// PrintJobInfo.swift +// flutter_downloader +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintJobInfo : NSObject { + var state: PrintJobState + var attributes: PrintAttributes + var creationTime: Int64 + var numberOfPages: Int? + var copies: Int? + var label: String? + var printerName: String? + var printerType: String? + + public init(fromPrintJobController: PrintJobController) { + state = fromPrintJobController.state + creationTime = fromPrintJobController.creationTime + attributes = PrintAttributes.init(fromPrintJobController: fromPrintJobController) + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + printerName = printInfo.printer.name + printerType = printInfo.printer.type.rawValue + copies = printInfo.printSettings["com_apple_print_PrintSettings_PMCopies"] as? Int + } + super.init() + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + label = job.jobTitle + numberOfPages = printInfo.printSettings["com_apple_print_PrintSettings_PMLastPage"] as? Int + if numberOfPages == nil || numberOfPages! > job.pageRange.length { + numberOfPages = job.pageRange.length + } + } + } + + public func toMap () -> [String:Any?] { + return [ + "state": state.rawValue, + "attributes": attributes.toMap(), + "numberOfPages": numberOfPages, + "copies": copies, + "creationTime": creationTime, + "label": label, + "printerName": printerName, + "printerType": printerType + ] + } +} diff --git a/macos/Classes/PrintJob/PrintJobManager.swift b/macos/Classes/PrintJob/PrintJobManager.swift new file mode 100644 index 00000000..7148027a --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobManager.swift @@ -0,0 +1,28 @@ +// +// PrintJobManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +public class PrintJobManager: NSObject, Disposable { + static var jobs: [String: PrintJobController?] = [:] + + public override init() { + super.init() + } + + public func dispose() { + let jobs = PrintJobManager.jobs.values + jobs.forEach { (job: PrintJobController?) in + job?.dispose() + } + PrintJobManager.jobs.removeAll() + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PrintJob/PrintJobSettings.swift b/macos/Classes/PrintJob/PrintJobSettings.swift new file mode 100644 index 00000000..15abee8c --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobSettings.swift @@ -0,0 +1,154 @@ +// +// PrintJobSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +@objcMembers +public class PrintJobSettings: ISettings { + + public var handledByClient = false + public var jobName: String? + public var animated = true + public var _orientation: NSNumber? + public var orientation: Int? { + get { + return _orientation?.intValue + } + set { + if let newValue = newValue { + _orientation = NSNumber.init(value: newValue) + } else { + _orientation = nil + } + } + } + public var _numberOfPages: NSNumber? + public var numberOfPages: Int? { + get { + return _numberOfPages?.intValue + } + set { + if let newValue = newValue { + _numberOfPages = NSNumber.init(value: newValue) + } else { + _numberOfPages = nil + } + } + } + public var _forceRenderingQuality: NSNumber? + public var forceRenderingQuality: Int? { + get { + return _forceRenderingQuality?.intValue + } + set { + if let newValue = newValue { + _forceRenderingQuality = NSNumber.init(value: newValue) + } else { + _forceRenderingQuality = nil + } + } + } + public var margins: NSEdgeInsets? + public var _duplexMode: NSNumber? + public var duplexMode: Int? { + get { + return _duplexMode?.intValue + } + set { + if let newValue = newValue { + _duplexMode = NSNumber.init(value: newValue) + } else { + _duplexMode = nil + } + } + } + public var _outputType: NSNumber? + public var outputType: Int? { + get { + return _outputType?.intValue + } + set { + if let newValue = newValue { + _outputType = NSNumber.init(value: newValue) + } else { + _outputType = nil + } + } + } + public var showsNumberOfCopies = true + public var showsPaperSelectionForLoadedPapers = false + public var showsPaperOrientation = true + public var _maximumContentHeight: NSNumber? + public var maximumContentHeight: Double? { + get { + return _maximumContentHeight?.doubleValue + } + set { + if let newValue = newValue { + _maximumContentHeight = NSNumber.init(value: newValue) + } else { + _maximumContentHeight = nil + } + } + } + public var _maximumContentWidth: NSNumber? + public var maximumContentWidth: Double? { + get { + return _maximumContentWidth?.doubleValue + } + set { + if let newValue = newValue { + _maximumContentWidth = NSNumber.init(value: newValue) + } else { + _maximumContentWidth = nil + } + } + } + public var _footerHeight: NSNumber? + public var footerHeight: Double? { + get { + return _footerHeight?.doubleValue + } + set { + if let newValue = newValue { + _footerHeight = NSNumber.init(value: newValue) + } else { + _footerHeight = nil + } + } + } + public var _headerHeight: NSNumber? + public var headerHeight: Double? { + get { + return _headerHeight?.doubleValue + } + set { + if let newValue = newValue { + _headerHeight = NSNumber.init(value: newValue) + } else { + _headerHeight = nil + } + } + } + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> PrintJobSettings { + let _ = super.parse(settings: settings) + if let marginsMap = settings["margins"] as? [String : Double] { + margins = NSEdgeInsets.fromMap(map: marginsMap) + } + return self + } + + override func getRealSettings(obj: PrintJobController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + return realOptions + } +} diff --git a/macos/Classes/SwiftFlutterPlugin.swift b/macos/Classes/SwiftFlutterPlugin.swift new file mode 100755 index 00000000..3a60cbca --- /dev/null +++ b/macos/Classes/SwiftFlutterPlugin.swift @@ -0,0 +1,86 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class SwiftFlutterPlugin: NSObject, FlutterPlugin { + + static var instance: SwiftFlutterPlugin? + var registrar: FlutterPluginRegistrar? + var platformUtil: PlatformUtil? + var inAppWebViewStatic: InAppWebViewStatic? + var myCookieManager: Any? + var myWebStorageManager: MyWebStorageManager? + var credentialDatabase: CredentialDatabase? + var inAppBrowserManager: InAppBrowserManager? + var headlessInAppWebViewManager: HeadlessInAppWebViewManager? + var webAuthenticationSessionManager: WebAuthenticationSessionManager? +// var printJobManager: PrintJobManager? + + var webViewControllers: [String: InAppBrowserWebViewController?] = [:] + var safariViewControllers: [String: Any?] = [:] + + public init(with registrar: FlutterPluginRegistrar) { + super.init() + self.registrar = registrar + registrar.register(FlutterWebViewFactory(registrar: registrar) as FlutterPlatformViewFactory, withId: FlutterWebViewFactory.VIEW_TYPE_ID) + + platformUtil = PlatformUtil(registrar: registrar) + inAppBrowserManager = InAppBrowserManager(registrar: registrar) + headlessInAppWebViewManager = HeadlessInAppWebViewManager(registrar: registrar) + inAppWebViewStatic = InAppWebViewStatic(registrar: registrar) + credentialDatabase = CredentialDatabase(registrar: registrar) + if #available(macOS 10.13, *) { + myCookieManager = MyCookieManager(registrar: registrar) + } + myWebStorageManager = MyWebStorageManager(registrar: registrar) + webAuthenticationSessionManager = WebAuthenticationSessionManager(registrar: registrar) +// printJobManager = PrintJobManager() + } + + public static func register(with registrar: FlutterPluginRegistrar) { + SwiftFlutterPlugin.instance = SwiftFlutterPlugin(with: registrar) + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + platformUtil?.dispose() + platformUtil = nil + inAppBrowserManager?.dispose() + inAppBrowserManager = nil + headlessInAppWebViewManager?.dispose() + headlessInAppWebViewManager = nil + inAppWebViewStatic?.dispose() + inAppWebViewStatic = nil + credentialDatabase?.dispose() + credentialDatabase = nil + if #available(macOS 10.13, *) { + (myCookieManager as! MyCookieManager?)?.dispose() + myCookieManager = nil + } + myWebStorageManager?.dispose() + myWebStorageManager = nil + webAuthenticationSessionManager?.dispose() + webAuthenticationSessionManager = nil +// printJobManager?.dispose() +// printJobManager = nil + } +} diff --git a/macos/Classes/Types/BaseCallbackResult.swift b/macos/Classes/Types/BaseCallbackResult.swift new file mode 100644 index 00000000..f7edb4a5 --- /dev/null +++ b/macos/Classes/Types/BaseCallbackResult.swift @@ -0,0 +1,32 @@ +// +// BaseCallbackResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class BaseCallbackResult : CallbackResult { + + override init() { + super.init() + + self.success = { [weak self] (obj: Any?) in + let result: T? = self?.decodeResult(obj) + var shouldRunDefaultBehaviour = false + if let result = result { + shouldRunDefaultBehaviour = self?.nonNullSuccess(result) ?? shouldRunDefaultBehaviour + } else { + shouldRunDefaultBehaviour = self?.nullSuccess() ?? shouldRunDefaultBehaviour + } + if shouldRunDefaultBehaviour { + self?.defaultBehaviour(result) + } + } + + self.notImplemented = { [weak self] in + self?.defaultBehaviour(nil) + } + } +} diff --git a/macos/Classes/Types/CGRect.swift b/macos/Classes/Types/CGRect.swift new file mode 100644 index 00000000..61cb7627 --- /dev/null +++ b/macos/Classes/Types/CGRect.swift @@ -0,0 +1,23 @@ +// +// CGRect.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +extension CGRect { + public static func fromMap(map: [String: Double]) -> CGRect { + return CGRect(x: map["x"]!, y: map["y"]!, width: map["width"]!, height: map["height"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "x": minX, + "y": minY, + "width": width, + "height": height + ] + } +} diff --git a/macos/Classes/Types/CallbackResult.swift b/macos/Classes/Types/CallbackResult.swift new file mode 100644 index 00000000..5b0ba2b5 --- /dev/null +++ b/macos/Classes/Types/CallbackResult.swift @@ -0,0 +1,18 @@ +// +// CallbackResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class CallbackResult : MethodChannelResult { + public var notImplemented: () -> Void = {} + public var success: (Any?) -> Void = {_ in } + public var error: (String, String?, Any?) -> Void = {_,_,_ in } + public var nonNullSuccess: (T) -> Bool = {_ in true} + public var nullSuccess: () -> Bool = {true} + public var defaultBehaviour: (T?) -> Void = {_ in } + public var decodeResult: (Any?) -> T? = {_ in nil} +} diff --git a/macos/Classes/Types/ChannelDelegate.swift b/macos/Classes/Types/ChannelDelegate.swift new file mode 100644 index 00000000..b8e7030f --- /dev/null +++ b/macos/Classes/Types/ChannelDelegate.swift @@ -0,0 +1,28 @@ +// +// ChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation +import FlutterMacOS + +public class ChannelDelegate : FlutterMethodCallDelegate, Disposable { + var channel: FlutterMethodChannel? + + public init(channel: FlutterMethodChannel) { + super.init() + self.channel = channel + self.channel?.setMethodCallHandler(handle) + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } + + public func dispose() { + channel?.setMethodCallHandler(nil) + channel = nil + } +} diff --git a/macos/Classes/Types/ClientCertChallenge.swift b/macos/Classes/Types/ClientCertChallenge.swift new file mode 100644 index 00000000..f2381ee4 --- /dev/null +++ b/macos/Classes/Types/ClientCertChallenge.swift @@ -0,0 +1,22 @@ +// +// ClientCertChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ClientCertChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + ] + } +} diff --git a/macos/Classes/Types/ClientCertResponse.swift b/macos/Classes/Types/ClientCertResponse.swift new file mode 100644 index 00000000..75ae6818 --- /dev/null +++ b/macos/Classes/Types/ClientCertResponse.swift @@ -0,0 +1,33 @@ +// +// ClientCertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ClientCertResponse : NSObject { + var certificatePath: String + var certificatePassword: String? + var keyStoreType: String? + var action: Int? + + public init(certificatePath: String, certificatePassword: String? = nil, keyStoreType: String? = nil, action: Int? = nil) { + self.certificatePath = certificatePath + self.certificatePassword = certificatePassword + self.keyStoreType = keyStoreType + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ClientCertResponse? { + guard let map = map else { + return nil + } + let certificatePath = map["certificatePath"] as! String + let certificatePassword = map["certificatePassword"] as? String + let keyStoreType = map["keyStoreType"] as? String + let action = map["action"] as? Int + return ClientCertResponse(certificatePath: certificatePath, certificatePassword: certificatePassword, keyStoreType: keyStoreType, action: action) + } +} diff --git a/macos/Classes/Types/CreateWindowAction.swift b/macos/Classes/Types/CreateWindowAction.swift new file mode 100644 index 00000000..db7f7658 --- /dev/null +++ b/macos/Classes/Types/CreateWindowAction.swift @@ -0,0 +1,31 @@ +// +// CreateWindowAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import WebKit + +public class CreateWindowAction : NSObject { + var navigationAction: WKNavigationAction + var windowId: Int64 + var windowFeatures: WKWindowFeatures + var isDialog: Bool? + + public init(navigationAction: WKNavigationAction, windowId: Int64, windowFeatures: WKWindowFeatures, isDialog: Bool? = nil) { + self.navigationAction = navigationAction + self.windowId = windowId + self.windowFeatures = windowFeatures + self.isDialog = isDialog + } + + public func toMap () -> [String:Any?] { + var map = navigationAction.toMap() + map["windowId"] = windowId + map["windowFeatures"] = windowFeatures.toMap() + map["isDialog"] = isDialog + return map + } +} diff --git a/macos/Classes/Types/CustomSchemeResponse.swift b/macos/Classes/Types/CustomSchemeResponse.swift new file mode 100644 index 00000000..ba5f0c60 --- /dev/null +++ b/macos/Classes/Types/CustomSchemeResponse.swift @@ -0,0 +1,31 @@ +// +// CustomSchemeResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class CustomSchemeResponse : NSObject { + var data: Data + var contentType: String + var contentEncoding: String + + public init(data: Data, contentType: String, contentEncoding: String) { + self.data = data + self.contentType = contentType + self.contentEncoding = contentEncoding + } + + public static func fromMap(map: [String:Any?]?) -> CustomSchemeResponse? { + guard let map = map else { + return nil + } + let data = map["data"] as! FlutterStandardTypedData + let contentType = map["contentType"] as! String + let contentEncoding = map["contentEncoding"] as! String + return CustomSchemeResponse(data: data.data, contentType: contentType, contentEncoding: contentEncoding) + } +} diff --git a/macos/Classes/Types/Disposable.swift b/macos/Classes/Types/Disposable.swift new file mode 100644 index 00000000..0a34a942 --- /dev/null +++ b/macos/Classes/Types/Disposable.swift @@ -0,0 +1,12 @@ +// +// Disposable.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation + +public protocol Disposable { + func dispose() -> Void +} diff --git a/macos/Classes/Types/DownloadStartRequest.swift b/macos/Classes/Types/DownloadStartRequest.swift new file mode 100644 index 00000000..808f9453 --- /dev/null +++ b/macos/Classes/Types/DownloadStartRequest.swift @@ -0,0 +1,42 @@ +// +// DownloadStartRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/04/22. +// + +import Foundation + +public class DownloadStartRequest: NSObject { + var url: String + var userAgent: String? + var contentDisposition: String? + var mimeType: String? + var contentLength: Int64 + var suggestedFilename: String? + var textEncodingName: String? + + public init(url: String, userAgent: String?, contentDisposition: String?, + mimeType: String?, contentLength: Int64, + suggestedFilename: String?, textEncodingName: String?) { + self.url = url + self.userAgent = userAgent + self.contentDisposition = contentDisposition + self.mimeType = mimeType + self.contentLength = contentLength + self.suggestedFilename = suggestedFilename + self.textEncodingName = textEncodingName + } + + public func toMap () -> [String:Any?] { + return [ + "url": url, + "userAgent": userAgent, + "contentDisposition": contentDisposition, + "mimeType": mimeType, + "contentLength": contentLength, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName + ] + } +} diff --git a/macos/Classes/Types/FindSession.swift b/macos/Classes/Types/FindSession.swift new file mode 100644 index 00000000..0c247004 --- /dev/null +++ b/macos/Classes/Types/FindSession.swift @@ -0,0 +1,28 @@ +// +// UIFindSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindSession: NSObject { + var resultCount: Int + var highlightedResultIndex: Int + var searchResultDisplayStyle: Int + + public init(resultCount: Int, highlightedResultIndex: Int, searchResultDisplayStyle: Int) { + self.resultCount = resultCount + self.highlightedResultIndex = highlightedResultIndex + self.searchResultDisplayStyle = searchResultDisplayStyle + } + + public func toMap () -> [String:Any?] { + return [ + "resultCount": resultCount, + "highlightedResultIndex": highlightedResultIndex, + "searchResultDisplayStyle": searchResultDisplayStyle + ] + } +} diff --git a/macos/Classes/Types/FlutterMethodCallDelegate.swift b/macos/Classes/Types/FlutterMethodCallDelegate.swift new file mode 100755 index 00000000..0454d2f7 --- /dev/null +++ b/macos/Classes/Types/FlutterMethodCallDelegate.swift @@ -0,0 +1,19 @@ +// +// FlutterMethodCallDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation +import FlutterMacOS + +public class FlutterMethodCallDelegate: NSObject { + public override init() { + super.init() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } +} diff --git a/macos/Classes/Types/FlutterMethodChannel.swift b/macos/Classes/Types/FlutterMethodChannel.swift new file mode 100644 index 00000000..e9055b2e --- /dev/null +++ b/macos/Classes/Types/FlutterMethodChannel.swift @@ -0,0 +1,26 @@ +// +// FlutterMethodChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import FlutterMacOS + +extension FlutterMethodChannel { + public func invokeMethod(_ method: String, arguments: Any, callback: MethodChannelResult) { + invokeMethod(method, arguments: arguments) {(result) -> Void in + if result is FlutterError { + let error = result as! FlutterError + callback.error(error.code, error.message, error.details) + } + else if (result as? NSObject) == FlutterMethodNotImplemented { + callback.notImplemented() + } + else { + callback.success(result) + } + } + } +} diff --git a/macos/Classes/Types/HitTestResult.swift b/macos/Classes/Types/HitTestResult.swift new file mode 100644 index 00000000..53307789 --- /dev/null +++ b/macos/Classes/Types/HitTestResult.swift @@ -0,0 +1,44 @@ +// +// HitTestResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public enum HitTestResultType: Int { + case unknownType = 0 + case phoneType = 2 + case geoType = 3 + case emailType = 4 + case imageType = 5 + case srcAnchorType = 7 + case srcImageAnchorType = 8 + case editTextType = 9 +} + +public class HitTestResult: NSObject { + var type: HitTestResultType + var extra: String? + + public init(type: HitTestResultType, extra: String?) { + self.type = type + self.extra = extra + } + + public static func fromMap(map: [String:Any?]?) -> HitTestResult? { + guard let map = map else { + return nil + } + let type = HitTestResultType.init(rawValue: map["type"] as? Int ?? HitTestResultType.unknownType.rawValue) ?? HitTestResultType.unknownType + return HitTestResult(type: type, extra: map["extra"] as? String) + } + + public func toMap () -> [String:Any?] { + return [ + "type": type.rawValue, + "extra": extra, + ] + } +} diff --git a/macos/Classes/Types/HttpAuthResponse.swift b/macos/Classes/Types/HttpAuthResponse.swift new file mode 100644 index 00000000..f2f85d64 --- /dev/null +++ b/macos/Classes/Types/HttpAuthResponse.swift @@ -0,0 +1,33 @@ +// +// HttpAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class HttpAuthResponse : NSObject { + var username: String + var password: String + var permanentPersistence: Bool + var action: Int? + + public init(username: String, password: String, permanentPersistence: Bool, action: Int? = nil) { + self.username = username + self.password = password + self.permanentPersistence = permanentPersistence + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> HttpAuthResponse? { + guard let map = map else { + return nil + } + let username = map["username"] as! String + let password = map["password"] as! String + let permanentPersistence = map["permanentPersistence"] as! Bool + let action = map["action"] as? Int + return HttpAuthResponse(username: username, password: password, permanentPersistence: permanentPersistence, action: action) + } +} diff --git a/macos/Classes/Types/HttpAuthenticationChallenge.swift b/macos/Classes/Types/HttpAuthenticationChallenge.swift new file mode 100644 index 00000000..b22a0092 --- /dev/null +++ b/macos/Classes/Types/HttpAuthenticationChallenge.swift @@ -0,0 +1,34 @@ +// +// HttpAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class HttpAuthenticationChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + var previousFailureCount: Int = 0 + var failureResponse: URLResponse? + var error: Error? + var proposedCredential: URLCredential? + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + previousFailureCount = fromChallenge.previousFailureCount + failureResponse = fromChallenge.failureResponse + error = fromChallenge.error + proposedCredential = fromChallenge.proposedCredential + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap() + ] + } +} diff --git a/macos/Classes/Types/JsAlertResponse.swift b/macos/Classes/Types/JsAlertResponse.swift new file mode 100644 index 00000000..3e6b3e78 --- /dev/null +++ b/macos/Classes/Types/JsAlertResponse.swift @@ -0,0 +1,33 @@ +// +// JsAlertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class JsAlertResponse: NSObject { + var message: String + var confirmButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsAlertResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsAlertResponse(message: message, confirmButtonTitle: confirmButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/macos/Classes/Types/JsConfirmResponse.swift b/macos/Classes/Types/JsConfirmResponse.swift new file mode 100644 index 00000000..76bfbe20 --- /dev/null +++ b/macos/Classes/Types/JsConfirmResponse.swift @@ -0,0 +1,36 @@ +// +// JsConfirmResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsConfirmResponse: NSObject { + var message: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsConfirmResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsConfirmResponse(message: message, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/macos/Classes/Types/JsPromptResponse.swift b/macos/Classes/Types/JsPromptResponse.swift new file mode 100644 index 00000000..5c40b492 --- /dev/null +++ b/macos/Classes/Types/JsPromptResponse.swift @@ -0,0 +1,43 @@ +// +// JsPromptResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsPromptResponse: NSObject { + var message: String + var defaultValue: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var value: String? + var action: Int? + + public init(message: String, defaultValue: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, value: String? = nil, action: Int? = nil) { + self.message = message + self.defaultValue = defaultValue + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.value = value + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsPromptResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let defaultValue = map["defaultValue"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let value = map["value"] as? String + let action = map["action"] as? Int + return JsPromptResponse(message: message, defaultValue: defaultValue, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, + handledByClient: handledByClient, value: value, action: action) + } +} diff --git a/macos/Classes/Types/MethodChannelResult.swift b/macos/Classes/Types/MethodChannelResult.swift new file mode 100644 index 00000000..d1555e39 --- /dev/null +++ b/macos/Classes/Types/MethodChannelResult.swift @@ -0,0 +1,14 @@ +// +// MethodChannelResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public protocol MethodChannelResult { + var success: (_ obj: Any?) -> Void { get set } + var error: (_ code: String, _ message: String?, _ details: Any?) -> Void { get set } + var notImplemented: () -> Void { get set } +} diff --git a/macos/Classes/Types/NSAttributedString.swift b/macos/Classes/Types/NSAttributedString.swift new file mode 100644 index 00000000..64380da6 --- /dev/null +++ b/macos/Classes/Types/NSAttributedString.swift @@ -0,0 +1,63 @@ +// +// NSAttributedString.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/03/21. +// + +import Foundation + +extension NSAttributedString { + public static func fromMap(map: [String:Any?]?) -> NSAttributedString? { + guard let map = map, let string = map["string"] as? String else { + return nil + } + + var attributes: [NSAttributedString.Key : Any] = [:] + + if let backgroundColor = map["backgroundColor"] as? String { + attributes[.backgroundColor] = NSColor(hexString: backgroundColor) + } + if let baselineOffset = map["baselineOffset"] as? Double { + attributes[.baselineOffset] = baselineOffset + } + if let expansion = map["expansion"] as? Double { + attributes[.expansion] = expansion + } + if let foregroundColor = map["foregroundColor"] as? String { + attributes[.foregroundColor] = NSColor(hexString: foregroundColor) + } + if let kern = map["kern"] as? Double { + attributes[.kern] = kern + } + if let ligature = map["ligature"] as? Int64 { + attributes[.ligature] = ligature + } + if let obliqueness = map["obliqueness"] as? Double { + attributes[.obliqueness] = obliqueness + } + if let strikethroughColor = map["strikethroughColor"] as? String { + attributes[.strikethroughColor] = NSColor(hexString: strikethroughColor) + } + if let strikethroughStyle = map["strikethroughStyle"] as? Int64 { + attributes[.strikethroughStyle] = strikethroughStyle + } + if let strokeColor = map["strokeColor"] as? String { + attributes[.strokeColor] = NSColor(hexString: strokeColor) + } + if let strokeWidth = map["strokeWidth"] as? Double { + attributes[.strokeWidth] = strokeWidth + } + if let textEffect = map["textEffect"] as? String { + attributes[.textEffect] = textEffect + } + if let underlineColor = map["underlineColor"] as? String { + attributes[.underlineColor] = NSColor(hexString: underlineColor) + } + if let underlineStyle = map["underlineStyle"] as? Int64 { + attributes[.underlineStyle] = underlineStyle + } + + return NSAttributedString(string: string, attributes: attributes) + } +} diff --git a/macos/Classes/Types/NSColor.swift b/macos/Classes/Types/NSColor.swift new file mode 100644 index 00000000..263a5c24 --- /dev/null +++ b/macos/Classes/Types/NSColor.swift @@ -0,0 +1,57 @@ +// +// NSColor.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension NSColor { + convenience init(hexString: String) { + let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) + } + + var hexString: String? { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + let multiplier = CGFloat(255.999999) + + self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + if alpha == 1.0 { + return String( + format: "#%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier) + ) + } + else { + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier), + Int(alpha * multiplier) + ) + } + } +} diff --git a/macos/Classes/Types/NSEdgeInsets.swift b/macos/Classes/Types/NSEdgeInsets.swift new file mode 100644 index 00000000..68aadcd2 --- /dev/null +++ b/macos/Classes/Types/NSEdgeInsets.swift @@ -0,0 +1,23 @@ +// +// UIEdgeInsets.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 11/05/22. +// + +import Foundation + +extension NSEdgeInsets { + public static func fromMap(map: [String: Double]) -> NSEdgeInsets { + return NSEdgeInsets.init(top: map["top"]!, left: map["left"]!, bottom: map["bottom"]!, right: map["right"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "top": top, + "right": self.right, + "bottom": bottom, + "left": self.left + ] + } +} diff --git a/macos/Classes/Types/PermissionRequest.swift b/macos/Classes/Types/PermissionRequest.swift new file mode 100644 index 00000000..a286a3f3 --- /dev/null +++ b/macos/Classes/Types/PermissionRequest.swift @@ -0,0 +1,29 @@ +// +// PermissionRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation +import WebKit + +public class PermissionRequest: NSObject { + var origin: String + var resources: [StringOrInt] + var frame: WKFrameInfo + + public init(origin: String, resources: [StringOrInt], frame: WKFrameInfo) { + self.origin = origin + self.resources = resources + self.frame = frame + } + + public func toMap () -> [String:Any?] { + return [ + "origin": origin, + "resources": resources, + "frame": frame.toMap() + ] + } +} diff --git a/macos/Classes/Types/PermissionResponse.swift b/macos/Classes/Types/PermissionResponse.swift new file mode 100644 index 00000000..7971f95e --- /dev/null +++ b/macos/Classes/Types/PermissionResponse.swift @@ -0,0 +1,27 @@ +// +// PermissionResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class PermissionResponse : NSObject { + var resources: [Any] + var action: Int? + + public init(resources: [Any], action: Int? = nil) { + self.resources = resources + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> PermissionResponse? { + guard let map = map else { + return nil + } + let resources = map["resources"] as! [Any] + let action = map["action"] as? Int + return PermissionResponse(resources: resources, action: action) + } +} diff --git a/macos/Classes/Types/PluginScript.swift b/macos/Classes/Types/PluginScript.swift new file mode 100644 index 00000000..503e440b --- /dev/null +++ b/macos/Classes/Types/PluginScript.swift @@ -0,0 +1,90 @@ +// +// PluginScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit + +public class PluginScript : UserScript { + var requiredInAllContentWorlds = false + var messageHandlerNames: [String] = [] + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(macOS 11.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(macOS 11.0, *) + public init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(macOS 11.0, *) + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + if #available(macOS 11.0, *) { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + + @available(macOS 11.0, *) + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + contentWorld: WKContentWorld? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: contentWorld ?? self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } +} diff --git a/macos/Classes/Types/SecCertificate.swift b/macos/Classes/Types/SecCertificate.swift new file mode 100644 index 00000000..06a17970 --- /dev/null +++ b/macos/Classes/Types/SecCertificate.swift @@ -0,0 +1,18 @@ +// +// SecCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension SecCertificate { + var data: Data { + let serverCertificateCFData = SecCertificateCopyData(self) + let data = CFDataGetBytePtr(serverCertificateCFData) + let size = CFDataGetLength(serverCertificateCFData) + let certificateData = NSData(bytes: data, length: size) + return Data(certificateData) + } +} diff --git a/macos/Classes/Types/ServerTrustAuthResponse.swift b/macos/Classes/Types/ServerTrustAuthResponse.swift new file mode 100644 index 00000000..d78fb07c --- /dev/null +++ b/macos/Classes/Types/ServerTrustAuthResponse.swift @@ -0,0 +1,24 @@ +// +// ServerTrustAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ServerTrustAuthResponse : NSObject { + var action: Int? + + public init(action: Int? = nil) { + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ServerTrustAuthResponse? { + guard let map = map else { + return nil + } + let action = map["action"] as? Int + return ServerTrustAuthResponse(action: action) + } +} diff --git a/macos/Classes/Types/ServerTrustChallenge.swift b/macos/Classes/Types/ServerTrustChallenge.swift new file mode 100644 index 00000000..190c831b --- /dev/null +++ b/macos/Classes/Types/ServerTrustChallenge.swift @@ -0,0 +1,22 @@ +// +// ServerTrustChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ServerTrustChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap() + ] + } +} diff --git a/macos/Classes/Types/Size2D.swift b/macos/Classes/Types/Size2D.swift new file mode 100644 index 00000000..7c2b9c6c --- /dev/null +++ b/macos/Classes/Types/Size2D.swift @@ -0,0 +1,35 @@ +// +// Size.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation + +public class Size2D : NSObject { + var width: Double + var height: Double + + public init(width: Double, height: Double) { + self.width = width + self.height = height + } + + public static func fromMap(map: [String:Any?]?) -> Size2D? { + guard let map = map else { + return nil + } + return Size2D( + width: map["width"] as? Double ?? -1.0, + height: map["height"] as? Double ?? -1.0 + ) + } + + public func toMap() -> [String:Any?] { + return [ + "width": width, + "height": height + ] + } +} diff --git a/macos/Classes/Types/SslCertificate.swift b/macos/Classes/Types/SslCertificate.swift new file mode 100644 index 00000000..6b9d9a17 --- /dev/null +++ b/macos/Classes/Types/SslCertificate.swift @@ -0,0 +1,30 @@ +// +// SslCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslCertificate: NSObject { + var x509Certificate: Data + var issuedBy: Any? + var issuedTo: Any? + var validNotAfterDate: Any? + var validNotBeforeDate: Any? + + public init(x509Certificate: Data) { + self.x509Certificate = x509Certificate + } + + public func toMap () -> [String:Any?] { + return [ + "x509Certificate": x509Certificate, + "issuedBy": issuedBy, + "issuedTo": issuedTo, + "validNotAfterDate": validNotAfterDate, + "validNotBeforeDate": validNotBeforeDate + ] + } +} diff --git a/macos/Classes/Types/SslError.swift b/macos/Classes/Types/SslError.swift new file mode 100644 index 00000000..58e3cf94 --- /dev/null +++ b/macos/Classes/Types/SslError.swift @@ -0,0 +1,50 @@ +// +// SslError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslError: NSObject { + var errorType: SecTrustResultType? + var message: String? + + public init(errorType: SecTrustResultType?) { + self.errorType = errorType + + var sslErrorMessage: String? = nil + switch errorType { + case .deny: + sslErrorMessage = "Indicates a user-configured deny; do not proceed." + break + case .fatalTrustFailure: + sslErrorMessage = "Indicates a trust failure which cannot be overridden by the user." + break + case .invalid: + sslErrorMessage = "Indicates an invalid setting or result." + break + case .otherError: + sslErrorMessage = "Indicates a failure other than that of trust evaluation." + break + case .recoverableTrustFailure: + sslErrorMessage = "Indicates a trust policy failure which can be overridden by the user." + break + case .unspecified: + sslErrorMessage = "Indicates the evaluation succeeded and the certificate is implicitly trusted, but user intent was not explicitly specified." + break + default: + sslErrorMessage = nil + } + + self.message = sslErrorMessage + } + + public func toMap () -> [String:Any?] { + return [ + "code": errorType?.rawValue, + "message": message + ] + } +} diff --git a/macos/Classes/Types/StringOrInt.swift b/macos/Classes/Types/StringOrInt.swift new file mode 100644 index 00000000..f7cd40c7 --- /dev/null +++ b/macos/Classes/Types/StringOrInt.swift @@ -0,0 +1,13 @@ +// +// StringOrInt.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation + +public protocol StringOrInt { } + +extension Int: StringOrInt { } +extension String: StringOrInt { } diff --git a/macos/Classes/Types/URLAuthenticationChallenge.swift b/macos/Classes/Types/URLAuthenticationChallenge.swift new file mode 100644 index 00000000..39b21fce --- /dev/null +++ b/macos/Classes/Types/URLAuthenticationChallenge.swift @@ -0,0 +1,20 @@ +// +// URLAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLAuthenticationChallenge { + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap(), + ] + } +} diff --git a/macos/Classes/Types/URLCredential.swift b/macos/Classes/Types/URLCredential.swift new file mode 100644 index 00000000..469f1e15 --- /dev/null +++ b/macos/Classes/Types/URLCredential.swift @@ -0,0 +1,26 @@ +// +// URLCredential.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLCredential { + public func toMap () -> [String:Any?] { + var x509Certificates: [Data] = [] + // certificates could be nil!!! + if certificates != nil { + for certificate in certificates { + x509Certificates.append((certificate as! SecCertificate).data) + } + } + return [ + "password": password, + "username": user, + "certificates": x509Certificates, + "persistence": persistence.rawValue + ] + } +} diff --git a/macos/Classes/Types/URLProtectionSpace.swift b/macos/Classes/Types/URLProtectionSpace.swift new file mode 100644 index 00000000..75557e17 --- /dev/null +++ b/macos/Classes/Types/URLProtectionSpace.swift @@ -0,0 +1,63 @@ +// +// URLProtectionSpace.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLProtectionSpace { + + var x509Certificate: Data? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + let secTrustEvaluateStatus = SecTrustEvaluate(serverTrust, &secResult); + + if secTrustEvaluateStatus == errSecSuccess, let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { + return serverCertificate.data + } + return nil + } + + var sslCertificate: SslCertificate? { + var sslCertificate: SslCertificate? = nil + if let x509Certificate = x509Certificate { + sslCertificate = SslCertificate(x509Certificate: x509Certificate) + } + return sslCertificate + } + + var sslError: SslError? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + SecTrustEvaluate(serverTrust, &secResult); + + guard let sslErrorType = secResult != SecTrustResultType.proceed ? secResult : nil else { + return nil + } + + return SslError(errorType: sslErrorType) + } + + public func toMap () -> [String:Any?] { + return [ + "host": host, + "procotol": self.protocol, + "realm": realm, + "port": port, + "sslCertificate": sslCertificate?.toMap(), + "sslError": sslError?.toMap(), + "authenticationMethod": authenticationMethod, + "distinguishedNames": distinguishedNames, + "receivesCredentialSecurely": receivesCredentialSecurely, + "proxyType": proxyType + ] + } +} diff --git a/macos/Classes/Types/URLRequest.swift b/macos/Classes/Types/URLRequest.swift new file mode 100644 index 00000000..54a251d2 --- /dev/null +++ b/macos/Classes/Types/URLRequest.swift @@ -0,0 +1,99 @@ +// +// URLRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import FlutterMacOS + +extension URLRequest { + public init(fromPluginMap: [String:Any?]) { + if let urlString = fromPluginMap["url"] as? String, let url = URL(string: urlString) { + self.init(url: url) + } else { + self.init(url: URL(string: "about:blank")!) + } + + if let method = fromPluginMap["method"] as? String { + httpMethod = method + } + if let body = fromPluginMap["body"] as? FlutterStandardTypedData { + httpBody = body.data + } + if let headers = fromPluginMap["headers"] as? [String:String] { + for (key, value) in headers { + setValue(value, forHTTPHeaderField: key) + } + } + if let _allowsCellularAccess = fromPluginMap["allowsCellularAccess"] as? Bool { + allowsCellularAccess = _allowsCellularAccess + } + if #available(macOS 10.15, *), let _allowsConstrainedNetworkAccess = fromPluginMap["allowsConstrainedNetworkAccess"] as? Bool { + allowsConstrainedNetworkAccess = _allowsConstrainedNetworkAccess + } + if #available(macOS 10.15, *), let _allowsExpensiveNetworkAccess = fromPluginMap["allowsExpensiveNetworkAccess"] as? Bool { + allowsExpensiveNetworkAccess = _allowsExpensiveNetworkAccess + } + if let _cachePolicy = fromPluginMap["cachePolicy"] as? Int { + cachePolicy = CachePolicy.init(rawValue: UInt(_cachePolicy)) ?? .useProtocolCachePolicy + } + if let _httpShouldHandleCookies = fromPluginMap["httpShouldHandleCookies"] as? Bool { + httpShouldHandleCookies = _httpShouldHandleCookies + } + if let _httpShouldUsePipelining = fromPluginMap["httpShouldUsePipelining"] as? Bool { + httpShouldUsePipelining = _httpShouldUsePipelining + } + if let _networkServiceType = fromPluginMap["networkServiceType"] as? Int { + networkServiceType = NetworkServiceType.init(rawValue: UInt(_networkServiceType)) ?? .default + } + if let _timeoutInterval = fromPluginMap["timeoutInterval"] as? Double { + timeoutInterval = _timeoutInterval + } + if let _mainDocumentURL = fromPluginMap["mainDocumentURL"] as? String { + mainDocumentURL = URL(string: _mainDocumentURL)! + } + if #available(macOS 11.3, *), let _assumesHTTP3Capable = fromPluginMap["assumesHTTP3Capable"] as? Bool { + assumesHTTP3Capable = _assumesHTTP3Capable + } + if #available(macOS 12.0, *), let attributionRawValue = fromPluginMap["attribution"] as? UInt, + let _attribution = URLRequest.Attribution(rawValue: attributionRawValue) { + attribution = _attribution + } + } + + public func toMap () -> [String:Any?] { + var _allowsConstrainedNetworkAccess: Bool? = nil + var _allowsExpensiveNetworkAccess: Bool? = nil + if #available(macOS 10.15, *) { + _allowsConstrainedNetworkAccess = allowsConstrainedNetworkAccess + _allowsExpensiveNetworkAccess = allowsExpensiveNetworkAccess + } + var _assumesHTTP3Capable: Bool? = nil + if #available(macOS 11.3, *) { + _assumesHTTP3Capable = assumesHTTP3Capable + } + var _attribution: UInt? = nil + if #available(macOS 12.0, *) { + _attribution = attribution.rawValue + } + return [ + "url": url?.absoluteString, + "method": httpMethod, + "headers": allHTTPHeaderFields, + "body": httpBody.map(FlutterStandardTypedData.init(bytes:)), + "allowsCellularAccess": allowsCellularAccess, + "allowsConstrainedNetworkAccess": _allowsConstrainedNetworkAccess, + "allowsExpensiveNetworkAccess": _allowsExpensiveNetworkAccess, + "cachePolicy": cachePolicy.rawValue, + "httpShouldHandleCookies": httpShouldHandleCookies, + "httpShouldUsePipelining": httpShouldUsePipelining, + "networkServiceType": networkServiceType.rawValue, + "timeoutInterval": timeoutInterval, + "mainDocumentURL": mainDocumentURL?.absoluteString, + "assumesHTTP3Capable": _assumesHTTP3Capable, + "attribution": _attribution + ] + } +} diff --git a/macos/Classes/Types/URLResponse.swift b/macos/Classes/Types/URLResponse.swift new file mode 100644 index 00000000..0a6a4835 --- /dev/null +++ b/macos/Classes/Types/URLResponse.swift @@ -0,0 +1,31 @@ +// +// URLResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLResponse { + public convenience init?(fromPluginMap: [String:Any?]) { + let url = URL(string: fromPluginMap["url"] as? String ?? "about:blank")! + let mimeType = fromPluginMap["mimeType"] as? String + let expectedContentLength = fromPluginMap["expectedContentLength"] as? Int64 ?? 0 + let textEncodingName = fromPluginMap["textEncodingName"] as? String + self.init(url: url, mimeType: mimeType, expectedContentLength: Int(expectedContentLength), textEncodingName: textEncodingName) + } + + public func toMap () -> [String:Any?] { + let httpResponse: HTTPURLResponse? = self as? HTTPURLResponse + return [ + "expectedContentLength": expectedContentLength, + "mimeType": mimeType, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName, + "url": url?.absoluteString, + "headers": httpResponse?.allHeaderFields, + "statusCode": httpResponse?.statusCode + ] + } +} diff --git a/macos/Classes/Types/UserScript.swift b/macos/Classes/Types/UserScript.swift new file mode 100644 index 00000000..63f30576 --- /dev/null +++ b/macos/Classes/Types/UserScript.swift @@ -0,0 +1,71 @@ +// +// InAppWebViewUserScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +public class UserScript : WKUserScript { + var groupName: String? + + private var contentWorldWrapper: Any? + @available(macOS 11.0, *) + var contentWorld: WKContentWorld { + get { + if let value = contentWorldWrapper as? WKContentWorld { + return value + } + return .page + } + set { contentWorldWrapper = newValue } + } + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.groupName = groupName + } + + @available(macOS 11.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(macOS 11.0, *) + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.groupName = groupName + self.contentWorld = contentWorld + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> UserScript? { + guard let map = map else { + return nil + } + + let contentWorldMap = map["contentWorld"] as? [String:Any?] + if #available(macOS 11.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: windowId)! + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool, + in: contentWorld + ) + } + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool + ) + } +} diff --git a/macos/Classes/Types/WKContentWorld.swift b/macos/Classes/Types/WKContentWorld.swift new file mode 100644 index 00000000..f94cc81a --- /dev/null +++ b/macos/Classes/Types/WKContentWorld.swift @@ -0,0 +1,41 @@ +// +// WKContentWorld.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(macOS 11.0, *) +extension WKContentWorld { + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + private static var _windowId = [String: Int64?]() + + var windowId: Int64? { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKContentWorld._windowId[tmpAddress] ?? nil + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKContentWorld._windowId[tmpAddress] = newValue + } + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> WKContentWorld? { + guard let map = map else { + return nil + } + var name = map["name"] as! String + name = windowId != nil && name != "page" ? + WKUserContentController.WINDOW_ID_PREFIX + String(windowId!) + "-" + name : + name + let contentWorld = Util.getContentWorld(name: name) + contentWorld.windowId = name != "page" ? windowId : nil + return contentWorld + } +} diff --git a/macos/Classes/Types/WKFrameInfo.swift b/macos/Classes/Types/WKFrameInfo.swift new file mode 100644 index 00000000..042fe419 --- /dev/null +++ b/macos/Classes/Types/WKFrameInfo.swift @@ -0,0 +1,26 @@ +// +// WKFrameInfo.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKFrameInfo { + + public func toMap () -> [String:Any?] { + var securityOrigin: [String:Any?]? = nil + if #available(iOS 9.0, *) { + securityOrigin = self.securityOrigin.toMap() + } + // fix: self.request throws EXC_BREAKPOINT when coming from WKNavigationAction.sourceFrame + let request: URLRequest? = self.value(forKey: "request") as? URLRequest + return [ + "isMainFrame": isMainFrame, + "request": request?.toMap(), + "securityOrigin": securityOrigin + ] + } +} diff --git a/macos/Classes/Types/WKNavigationAction.swift b/macos/Classes/Types/WKNavigationAction.swift new file mode 100644 index 00000000..f93080f7 --- /dev/null +++ b/macos/Classes/Types/WKNavigationAction.swift @@ -0,0 +1,28 @@ +// +// WKNavigationAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationAction { + public func toMap () -> [String:Any?] { + var shouldPerformDownload: Bool? = nil + if #available(macOS 11.3, *) { + shouldPerformDownload = self.shouldPerformDownload + } + return [ + "request": request.toMap(), + "isForMainFrame": targetFrame?.isMainFrame ?? false, + "hasGesture": nil, + "isRedirect": nil, + "navigationType": navigationType.rawValue, + "sourceFrame": sourceFrame.toMap(), + "targetFrame": targetFrame?.toMap(), + "shouldPerformDownload": shouldPerformDownload + ] + } +} diff --git a/macos/Classes/Types/WKNavigationResponse.swift b/macos/Classes/Types/WKNavigationResponse.swift new file mode 100644 index 00000000..0e685291 --- /dev/null +++ b/macos/Classes/Types/WKNavigationResponse.swift @@ -0,0 +1,19 @@ +// +// WKNavigationResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationResponse { + public func toMap () -> [String:Any?] { + return [ + "response": response.toMap(), + "isForMainFrame": isForMainFrame, + "canShowMIMEType": canShowMIMEType, + ] + } +} diff --git a/macos/Classes/Types/WKSecurityOrigin.swift b/macos/Classes/Types/WKSecurityOrigin.swift new file mode 100644 index 00000000..a7e8c714 --- /dev/null +++ b/macos/Classes/Types/WKSecurityOrigin.swift @@ -0,0 +1,20 @@ +// +// WKSecurityOrigin.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(iOS 9.0, *) +extension WKSecurityOrigin { + public func toMap () -> [String:Any?] { + return [ + "host": host, + "port": port, + "protocol": self.protocol + ] + } +} diff --git a/macos/Classes/Types/WKUserContentController.swift b/macos/Classes/Types/WKUserContentController.swift new file mode 100644 index 00000000..fb17db60 --- /dev/null +++ b/macos/Classes/Types/WKUserContentController.swift @@ -0,0 +1,352 @@ +// +// UserContentController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit +import OrderedSet + +extension WKUserContentController { + static var WINDOW_ID_PREFIX = "WINDOW-ID-" + + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + @available(macOS 11.0, *) + private static var _contentWorlds = [String: Set]() + @available(macOS 11.0, *) + var contentWorlds: Set { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._contentWorlds[tmpAddress] ?? [] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._contentWorlds[tmpAddress] = newValue + } + } + + private static var _userOnlyScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var userOnlyScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._userOnlyScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._userOnlyScripts[tmpAddress] = newValue + } + } + + private static var _pluginScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var pluginScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._pluginScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._pluginScripts[tmpAddress] = newValue + } + } + + public func initialize () { + if #available(macOS 11.0, *) { + contentWorlds = Set([WKContentWorld.page]) + } + pluginScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + userOnlyScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + } + + public func dispose (windowId: Int64?) { + if windowId == nil { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + if #available(macOS 11.0, *) { + contentWorlds.removeAll() + WKUserContentController._contentWorlds.removeValue(forKey: tmpAddress) + } + pluginScripts.removeAll() + WKUserContentController._pluginScripts.removeValue(forKey: tmpAddress) + userOnlyScripts.removeAll() + WKUserContentController._userOnlyScripts.removeValue(forKey: tmpAddress) + } + else if #available(macOS 11.0, *), let windowId = windowId { + let contentWorldsToRemove = contentWorlds.filter({ $0.windowId == windowId }) + for contentWorld in contentWorldsToRemove { + contentWorlds.remove(contentWorld) + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + public func sync(scriptMessageHandler: WKScriptMessageHandler) { + let pluginScriptsList = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in pluginScriptsList { + if !containsPluginScript(with: pluginScript.groupName!) { + addUserScript(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + add(scriptMessageHandler, name: messageHandlerName) + } + } + if #available(macOS 11.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in contentWorlds { + let pluginScriptWithContentWorld = pluginScript.copyAndSet(contentWorld: contentWorld) + if !containsPluginScript(with: pluginScriptWithContentWorld.groupName!, in: contentWorld) { + addUserScript(pluginScriptWithContentWorld) + for messageHandlerName in pluginScriptWithContentWorld.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + } + } + } + + let userOnlyScriptsList = userOnlyScripts.compactMap({ $0.value }).joined() + for userOnlyScript in userOnlyScriptsList { + if !userScripts.contains(userOnlyScript) { + addUserScript(userOnlyScript) + } + } + } + + public func addUserOnlyScript(_ userOnlyScript: UserScript) { + if #available(macOS 11.0, *) { + contentWorlds.insert(userOnlyScript.contentWorld) + } + userOnlyScripts[userOnlyScript.injectionTime]!.append(userOnlyScript) + } + + public func addUserOnlyScripts(_ userOnlyScripts: [UserScript]) { + for userOnlyScript in userOnlyScripts { + addUserOnlyScript(userOnlyScript) + } + } + + public func addPluginScript(_ pluginScript: PluginScript) { + if #available(macOS 11.0, *) { + contentWorlds.insert(pluginScript.contentWorld) + } + pluginScripts[pluginScript.injectionTime]!.append(pluginScript) + } + + public func addPluginScripts(_ pluginScripts: [PluginScript]) { + for pluginScript in pluginScripts { + addPluginScript(pluginScript) + } + } + + public func getPluginScriptsRequiredInAllContentWorlds() -> [PluginScript] { + return pluginScripts.compactMap({ $0.value }) + .joined() + .filter({ $0.injectionTime == .atDocumentStart && $0.requiredInAllContentWorlds }) + } + + @available(macOS 11.0, *) + public func generateCodeForScriptEvaluation(scriptMessageHandler: WKScriptMessageHandler, source: String, contentWorld: WKContentWorld) -> String { + let (inserted, _) = contentWorlds.insert(contentWorld) + if inserted { + var generatedCode = "" + let pluginScriptsRequired = getPluginScriptsRequiredInAllContentWorlds() + for pluginScript in pluginScriptsRequired { + generatedCode += pluginScript.source + "\n" + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + if let windowId = contentWorld.windowId { + generatedCode += "\(WINDOW_ID_VARIABLE_JS_SOURCE) = \(String(windowId));\n" + } + return generatedCode + "\n" + source + } + return source + } + + public func removeUserOnlyScript(_ userOnlyScript: UserScript) { + userOnlyScripts[userOnlyScript.injectionTime]!.remove(userOnlyScript) + removeUserScript(scriptToRemove: userOnlyScript) + } + + public func removeUserOnlyScript(at index: Int, injectionTime: WKUserScriptInjectionTime) { + let scriptToRemove = userOnlyScripts[injectionTime]![index] + userOnlyScripts[injectionTime]!.removeObject(at: index) + removeUserScript(scriptToRemove: scriptToRemove) + } + + public func removeAllUserOnlyScripts() { + let allUserOnlyScripts = Array(userOnlyScripts.compactMap({ $0.value }).joined()) + + userOnlyScripts[.atDocumentStart]!.removeAllObjects() + userOnlyScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allUserOnlyScripts) + } + + public func removePluginScript(_ pluginScript: PluginScript) { + pluginScripts[pluginScript.injectionTime]!.remove(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + if #available(macOS 11.0, *) { + for contentWorld in contentWorlds { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + } + } + } + removeUserScript(scriptToRemove: pluginScript) + } + + public func removeAllPluginScripts() { + let allPluginScripts = Array(pluginScripts.compactMap({ $0.value }).joined()) + + pluginScripts[.atDocumentStart]!.removeAllObjects() + pluginScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allPluginScripts) + } + + public func removeAllPluginScriptMessageHandlers() { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in allPluginScripts { + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + } + } + if #available(macOS 11.0, *) { + removeAllScriptMessageHandlers() + for contentWorld in contentWorlds { + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + @available(macOS 11.0, *) + public func resetContentWorlds(windowId: Int64?) { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + let contentWorldsFiltered = contentWorlds.filter({ $0.windowId == windowId && $0 != WKContentWorld.page }) + for contentWorld in contentWorldsFiltered { + var found = false + for script in allUserOnlyScripts { + if script.contentWorld == contentWorld { + found = true + break + } + } + if !found { + contentWorlds.remove(contentWorld) + } + } + } + + private func removeUserScript(scriptToRemove: WKUserScript, shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if script != scriptToRemove { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + private func removeUserScripts(scriptsToRemove: [WKUserScript], shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if !userScripts.contains(script) { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + public func removeUserOnlyScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [UserScript] = [] + for script in allUserOnlyScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func removePluginScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [PluginScript] = [] + for script in allPluginScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func containsPluginScript(with groupName: String) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName { + return true + } + } + return false + } + + @available(macOS 11.0, *) + public func containsPluginScript(with groupName: String, in contentWorld: WKContentWorld) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName, script.contentWorld == contentWorld { + return true + } + } + return false + } + + @available(macOS 11.0, *) + public func getContentWorlds(with windowId: Int64?) -> Set { + var contentWorldsFiltered = Set([WKContentWorld.page]) + let contentWorlds = Array(self.contentWorlds) + for contentWorld in contentWorlds { + if contentWorld.windowId == windowId { + contentWorldsFiltered.insert(contentWorld) + } + } + return contentWorldsFiltered + } + + // use a copy of self.userScripts to avoid EXC_BREAKPOINT at runtime if self.userScripts gets removed when another code is looping them + private func useCopyOfUserScripts() -> [WKUserScript] { + return Array(self.userScripts) + } +} diff --git a/macos/Classes/Types/WKWindowFeatures.swift b/macos/Classes/Types/WKWindowFeatures.swift new file mode 100644 index 00000000..469e2be1 --- /dev/null +++ b/macos/Classes/Types/WKWindowFeatures.swift @@ -0,0 +1,24 @@ +// +// WKWindowFeatures.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKWindowFeatures { + public func toMap () -> [String:Any?] { + return [ + "allowsResizing": allowsResizing, + "height": height, + "menuBarVisibility": menuBarVisibility, + "statusBarVisibility": statusBarVisibility, + "toolbarsVisibility": toolbarsVisibility, + "width": width, + "x": x, + "y": y + ] + } +} diff --git a/macos/Classes/Types/WebMessage.swift b/macos/Classes/Types/WebMessage.swift new file mode 100644 index 00000000..378bd67f --- /dev/null +++ b/macos/Classes/Types/WebMessage.swift @@ -0,0 +1,28 @@ +// +// WebMessage.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessage : NSObject { + var data: String? + var ports: [WebMessagePort]? + + public init(data: String?, ports: [WebMessagePort]?) { + super.init() + self.data = data + self.ports = ports + } + + public func dispose() { + ports?.removeAll() + } + + deinit { + debugPrint("WebMessage - dealloc") + dispose() + } +} diff --git a/macos/Classes/Types/WebMessagePort.swift b/macos/Classes/Types/WebMessagePort.swift new file mode 100644 index 00000000..9e823e92 --- /dev/null +++ b/macos/Classes/Types/WebMessagePort.swift @@ -0,0 +1,123 @@ +// +// WebMessagePort.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessagePort : NSObject { + var name: String + var webMessageChannel: WebMessageChannel? + var isClosed = false + var isTransferred = false + var isStarted = false + + public init(name: String, webMessageChannel: WebMessageChannel) { + self.name = name + super.init() + self.webMessageChannel = webMessageChannel + } + + public func setWebMessageCallback(completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + self.isStarted = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let index = name == "port1" ? 0 : 1 + webView.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).onmessage = function (event) { + window.webkit.messageHandlers["onWebMessagePortMessageReceived"].postMessage({ + "webMessageChannelId": "\(webMessageChannel.id)", + "index": \(String(index)), + "message": event.data + }); + } + } + })(); + """) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func postMessage(message: WebMessage, completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port == self { + throw NSError(domain: "Source port cannot be transferred", code: 0) + } + 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 data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).postMessage('\(data)', \(portsString)); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + message.dispose() + } + + public func close(completionHandler: ((Any?) -> Void)? = nil) throws { + if isTransferred { + throw NSError(domain: "Port is already transferred", code: 0) + } + isClosed = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).close(); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func dispose() { + isClosed = true + webMessageChannel = nil + } + + deinit { + debugPrint("WebMessagePort - dealloc") + dispose() + } +} diff --git a/macos/Classes/Types/WebResourceError.swift b/macos/Classes/Types/WebResourceError.swift new file mode 100644 index 00000000..3183944b --- /dev/null +++ b/macos/Classes/Types/WebResourceError.swift @@ -0,0 +1,25 @@ +// +// WebResourceError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation + +public class WebResourceError: NSObject { + var type: Int + var errorDescription: String + + public init(type: Int, errorDescription: String) { + self.type = type + self.errorDescription = errorDescription + } + + public func toMap () -> [String:Any?] { + return [ + "type": type, + "description": errorDescription + ] + } +} diff --git a/macos/Classes/Types/WebResourceRequest.swift b/macos/Classes/Types/WebResourceRequest.swift new file mode 100644 index 00000000..ad7635c4 --- /dev/null +++ b/macos/Classes/Types/WebResourceRequest.swift @@ -0,0 +1,53 @@ +// +// WebResourceRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceRequest: NSObject { + var url: URL + var headers: [AnyHashable:Any]? + var isRedirect = false + var hasGesture = false + var isForMainFrame = true + var method = "GET" + + public init(url: URL, headers: [AnyHashable:Any]?) { + self.url = url + self.headers = headers + } + + public init(url: URL, headers: [AnyHashable:Any]?, isForMainFrame: Bool) { + self.url = url + self.headers = headers + self.isForMainFrame = isForMainFrame + } + + public init(fromURLRequest: URLRequest) { + self.url = fromURLRequest.url ?? URL(string: "about:blank")! + self.headers = fromURLRequest.allHTTPHeaderFields + self.method = fromURLRequest.httpMethod ?? "GET" + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.url = response?.url ?? URL(string: "about:blank")! + self.headers = response?.allHeaderFields + self.isForMainFrame = fromWKNavigationResponse.isForMainFrame + } + + public func toMap () -> [String:Any?] { + return [ + "url": url.absoluteString, + "headers": headers, + "isRedirect": isRedirect, + "hasGesture": hasGesture, + "isForMainFrame": isForMainFrame, + "method": method + ] + } +} diff --git a/macos/Classes/Types/WebResourceResponse.swift b/macos/Classes/Types/WebResourceResponse.swift new file mode 100644 index 00000000..4fcf13e7 --- /dev/null +++ b/macos/Classes/Types/WebResourceResponse.swift @@ -0,0 +1,47 @@ +// +// WebResourceResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceResponse: NSObject { + var contentType: String + var contentEncoding: String + var data: Data? + var headers: [AnyHashable:Any]? + var statusCode: Int? + var reasonPhrase: String? + + public init(contentType: String, contentEncoding: String, data: Data?, + headers: [AnyHashable:Any]?, statusCode: Int?, reasonPhrase: String?) { + self.contentType = contentType + self.contentEncoding = contentEncoding + self.data = data + self.headers = headers + self.statusCode = statusCode + self.reasonPhrase = reasonPhrase + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.contentType = response?.mimeType ?? "" + self.contentEncoding = response?.textEncodingName ?? "" + self.headers = response?.allHeaderFields + self.statusCode = response?.statusCode + } + + public func toMap () -> [String:Any?] { + return [ + "contentType": contentType, + "contentEncoding": contentEncoding, + "data": data, + "headers": headers, + "statusCode": statusCode, + "reasonPhrase": reasonPhrase + ] + } +} diff --git a/macos/Classes/Types/WebViewTransport.swift b/macos/Classes/Types/WebViewTransport.swift new file mode 100644 index 00000000..7e21f7e6 --- /dev/null +++ b/macos/Classes/Types/WebViewTransport.swift @@ -0,0 +1,18 @@ +// +// WebViewTransport.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class WebViewTransport: NSObject { + var webView: InAppWebView + var request: URLRequest + + init(webView: InAppWebView, request: URLRequest) { + self.webView = webView + self.request = request + } +} diff --git a/macos/Classes/Util.swift b/macos/Classes/Util.swift new file mode 100644 index 00000000..d248748e --- /dev/null +++ b/macos/Classes/Util.swift @@ -0,0 +1,140 @@ +// +// Util.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 12/02/21. +// + +import Foundation +import WebKit +import FlutterMacOS + +var SharedLastTouchPointTimestamp: [InAppWebView: Int64] = [:] + +public class Util { + public static func getUrlAsset(assetFilePath: String) throws -> URL { +// let key = SwiftFlutterPlugin.instance?.registrar?.lookupKey(forAsset: assetFilePath) + guard let assetURL = Bundle.main.url(forResource: assetFilePath, withExtension: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetURL + } + + public static func getAbsPathAsset(assetFilePath: String) throws -> String { +// let key = SwiftFlutterPlugin.instance?.registrar?.lookupKey(forAsset: assetFilePath) + guard let assetAbsPath = Bundle.main.path(forResource: assetFilePath, ofType: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetAbsPath + } + + public static func convertToDictionary(text: String) -> [String: Any]? { + if let data = text.data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + print(error.localizedDescription) + } + } + return nil + } + + public static func JSONStringify(value: Any, prettyPrinted: Bool = false) -> String { + let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : .init(rawValue: 0) + if JSONSerialization.isValidJSONObject(value) { + let data = try? JSONSerialization.data(withJSONObject: value, options: options) + if data != nil { + if let string = String(data: data!, encoding: .utf8) { + return string + } + } + } + return "" + } + + @available(macOS 11.0, *) + public static func getContentWorld(name: String) -> WKContentWorld { + switch name { + case "defaultClient": + return WKContentWorld.defaultClient + case "page": + return WKContentWorld.page + default: + return WKContentWorld.world(name: name) + } + } + + public static func isIPv4(address: String) -> Bool { + var sin = sockaddr_in() + return address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 + } + + public static func isIPv6(address: String) -> Bool { + var sin6 = sockaddr_in6() + return address.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 + } + + public static func isIpAddress(address: String) -> Bool { + return Util.isIPv6(address: address) || Util.isIPv4(address: address) + } + + public static func normalizeIPv6(address: String) throws -> String { + if !Util.isIPv6(address: address) { + throw NSError(domain: "Invalid address: \(address)", code: 0) + } + var ipString = address + // replace ipv4 address if any + let ipv4Regex = try! NSRegularExpression(pattern: "(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)") + if let match = ipv4Regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) { + if let ipv6PartRange = Range(match.range(at: 1), in: address) { + ipString = String(address[ipv6PartRange]) + } + if let ipv4Range = Range(match.range(at: 2), in: address) { + let ipv4 = address[ipv4Range] + let ipv4Splitted = ipv4.split(separator: ".") + var ipv4Converted = Array(repeating: "0000", count: 4) + for i in 0...3 { + let byte = Int(ipv4Splitted[i])! + let hex = ("0" + String(byte, radix: 16)) + var offset = hex.count - 3 + offset = offset < 0 ? 0 : offset + let fromIndex = hex.index(hex.startIndex, offsetBy: offset) + let toIndex = hex.index(hex.startIndex, offsetBy: hex.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + ipv4Converted[i] = String(hex[indexRange]) + } + ipString += ipv4Converted[0] + ipv4Converted[1] + ":" + ipv4Converted[2] + ipv4Converted[3] + } + } + + // take care of leading and trailing :: + let regex = try! NSRegularExpression(pattern: "^:|:$") + ipString = regex.stringByReplacingMatches(in: ipString, options: [], range: NSRange(location: 0, length: ipString.count), withTemplate: "") + + let ipv6 = ipString.split(separator: ":", omittingEmptySubsequences: false) + var fullIPv6 = Array(repeating: "0000", count: ipv6.count) + + for (i, hex) in ipv6.enumerated() { + if !hex.isEmpty { + // normalize leading zeros + let hexString = String("0000" + hex) + var offset = hexString.count - 5 + offset = offset < 0 ? 0 : offset + let fromIndex = hexString.index(hexString.startIndex, offsetBy: offset) + let toIndex = hexString.index(hexString.startIndex, offsetBy: hexString.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + fullIPv6[i] = String(hexString[indexRange]) + } else { + // normalize grouped zeros :: + var zeros: [String] = [] + for _ in ipv6.count...8 { + zeros.append("0000") + } + fullIPv6[i] = zeros.joined(separator: ":") + } + } + + return fullIPv6.joined(separator: ":") + } + +} diff --git a/macos/Classes/WKProcessPoolManager.swift b/macos/Classes/WKProcessPoolManager.swift new file mode 100755 index 00000000..038fb4d7 --- /dev/null +++ b/macos/Classes/WKProcessPoolManager.swift @@ -0,0 +1,13 @@ +// +// WKProcessPoolManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/11/2019. +// + +import Foundation +import WebKit + +public class WKProcessPoolManager { + static let sharedProcessPool = WKProcessPool() +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift new file mode 100644 index 00000000..f25806e6 --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift @@ -0,0 +1,99 @@ +// +// WebAuthenticationSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices +import FlutterMacOS + +public class WebAuthenticationSession : NSObject, ASWebAuthenticationPresentationContextProviding, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_webauthenticationsession_" + var id: String + var url: URL + var callbackURLScheme: String? + var settings: WebAuthenticationSessionSettings + var session: Any? + var channelDelegate: WebAuthenticationSessionChannelDelegate? + private var _canStart = true + + public init(id: String, url: URL, callbackURLScheme: String?, settings: WebAuthenticationSessionSettings) { + self.id = id + self.url = url + self.settings = settings + super.init() + self.callbackURLScheme = callbackURLScheme + if #available(macOS 10.15, *) { + let session = ASWebAuthenticationSession(url: self.url, callbackURLScheme: self.callbackURLScheme, completionHandler: self.completionHandler) + session.presentationContextProvider = self + self.session = session + } + let channel = FlutterMethodChannel(name: WebAuthenticationSession.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = WebAuthenticationSessionChannelDelegate(webAuthenticationSession: self, channel: channel) + } + + public func prepare() { + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + session.prefersEphemeralWebBrowserSession = settings.prefersEphemeralWebBrowserSession + } + } + + public func completionHandler(url: URL?, error: Error?) -> Void { + channelDelegate?.onComplete(url: url, errorCode: error?._code) + } + + public func canStart() -> Bool { + guard let session = session else { + return false + } + if #available(macOS 10.15.4, *), let session = session as? ASWebAuthenticationSession { + return session.canStart + } + return _canStart + } + + public func start() -> Bool { + guard let session = session else { + return false + } + var started = false + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + started = session.start() + } + if started { + _canStart = false + } + return started + } + + public func cancel() { + guard let session = session else { + return + } + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + session.cancel() + } + } + + @available(macOS 10.15, *) + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return NSApplication.shared.windows.first { $0.isKeyWindow } ?? ASPresentationAnchor() + } + + public func dispose() { + cancel() + channelDelegate?.dispose() + channelDelegate = nil + session = nil + WebAuthenticationSessionManager.sessions[id] = nil + } + + deinit { + debugPrint("WebAuthenticationSession - dealloc") + dispose() + } +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift new file mode 100644 index 00000000..7c476a6a --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift @@ -0,0 +1,74 @@ +// +// WebAuthenticationSessionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebAuthenticationSessionChannelDelegate : ChannelDelegate { + private weak var webAuthenticationSession: WebAuthenticationSession? + + public init(webAuthenticationSession: WebAuthenticationSession, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webAuthenticationSession = webAuthenticationSession + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // let arguments = call.arguments as? NSDictionary + switch call.method { + case "canStart": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.canStart()) + } else { + result(false) + } + break + case "start": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.start()) + } else { + result(false) + } + break + case "cancel": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.cancel() + result(true) + } else { + result(false) + } + break + case "dispose": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(url: URL?, errorCode: Int?) { + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "errorCode": errorCode + ] + channel?.invokeMethod("onComplete", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webAuthenticationSession = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift new file mode 100644 index 00000000..9f418422 --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift @@ -0,0 +1,78 @@ +// +// WebAuthenticationSessionManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class WebAuthenticationSessionManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_webauthenticationsession" + static var registrar: FlutterPluginRegistrar? + static var sessions: [String: WebAuthenticationSession?] = [:] + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: WebAuthenticationSessionManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + WebAuthenticationSessionManager.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "create": + let id = arguments!["id"] as! String + let url = arguments!["url"] as! String + let callbackURLScheme = arguments!["callbackURLScheme"] as? String + let initialSettings = arguments!["initialSettings"] as! [String: Any?] + create(id: id, url: url, callbackURLScheme: callbackURLScheme, settings: initialSettings, result: result) + break + case "isAvailable": + if #available(iOS 11.0, *) { + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func create(id: String, url: String, callbackURLScheme: String?, settings: [String: Any?], result: @escaping FlutterResult) { + if #available(iOS 11.0, *) { + let sessionUrl = URL(string: url) ?? URL(string: "about:blank")! + let initialSettings = WebAuthenticationSessionSettings() + let _ = initialSettings.parse(settings: settings) + let session = WebAuthenticationSession(id: id, url: sessionUrl, callbackURLScheme: callbackURLScheme, settings: initialSettings) + session.prepare() + WebAuthenticationSessionManager.sessions[id] = session + result(true) + return + } + + result(FlutterError.init(code: "WebAuthenticationSessionManager", message: "WebAuthenticationSession is not available!", details: nil)) + } + + public override func dispose() { + super.dispose() + WebAuthenticationSessionManager.registrar = nil + let sessions = WebAuthenticationSessionManager.sessions.values + sessions.forEach { (session: WebAuthenticationSession?) in + session?.cancel() + session?.dispose() + } + WebAuthenticationSessionManager.sessions.removeAll() + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift new file mode 100644 index 00000000..ac262a31 --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift @@ -0,0 +1,28 @@ +// +// WebAuthenticationSessionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices + +@objcMembers +public class WebAuthenticationSessionSettings: ISettings { + + var prefersEphemeralWebBrowserSession = false + + override init(){ + super.init() + } + + override func getRealSettings(obj: WebAuthenticationSession?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if #available(macOS 10.15, *), let session = obj?.session as? ASWebAuthenticationSession { + realOptions["prefersEphemeralWebBrowserSession"] = session.prefersEphemeralWebBrowserSession + } + return realOptions + } +} diff --git a/macos/flutter_inappwebview.podspec b/macos/flutter_inappwebview.podspec new file mode 100644 index 00000000..97601f6f --- /dev/null +++ b/macos/flutter_inappwebview.podspec @@ -0,0 +1,27 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_inappwebview.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_inappwebview' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.resources = 'Storyboards/**/*.storyboard' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.dependency 'OrderedSet', '~>5.0' +end diff --git a/pubspec.yaml b/pubspec.yaml index 73b6b19f..0d6e3792 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview description: A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. -version: 6.0.0-beta.2 +version: 6.0.0-beta.3 homepage: https://inappwebview.dev/ repository: https://github.com/pichillilorenzo/flutter_inappwebview issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues @@ -39,6 +39,8 @@ flutter: pluginClass: InAppWebViewFlutterPlugin ios: pluginClass: InAppWebViewFlutterPlugin + macos: + pluginClass: InAppWebViewFlutterPlugin web: pluginClass: FlutterInAppWebViewWebPlatform fileName: flutter_inappwebview.dart