diff --git a/CHANGELOG.md b/CHANGELOG.md index 475b5028..53bf87f1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Deprecated old classes/properties/methods to make them eventually compatible with other Platforms and WebView engines. - Added Web support - Added `ProxyController` for Android +- Added `WebAuthenticationSession` for iOS - Added `pauseAllMediaPlayback`, `setAllMediaPlaybackSuspended`, `closeAllMediaPresentations`, `requestMediaPlaybackState`, `isInFullscreen`, `getCameraCaptureState`, `setCameraCaptureState`, `getMicrophoneCaptureState`, `setMicrophoneCaptureState` WebView controller methods - Added `underPageBackgroundColor`, `isTextInteractionEnabled`, `isSiteSpecificQuirksModeEnabled`, `upgradeKnownHostsToHTTPS`, `forceDarkStrategy` WebView settings - Added `onCameraCaptureStateChanged`, `onMicrophoneCaptureStateChanged` WebView events @@ -11,7 +12,6 @@ - Updated `getMetaThemeColor` on iOS 15.0+ - Deprecated `onLoadError` for `onReceivedError`. `onReceivedError` will be called also for subframes - Deprecated `onLoadHttpError` for `onReceivedError`. `onReceivedHttpError` will be called also for subframes -- Deprecated `onLoadResourceCustomScheme` for `onLoadResourceWithCustomScheme` ### BREAKING CHANGES diff --git a/example/assets/web-auth.html b/example/assets/web-auth.html new file mode 100755 index 00000000..37716b14 --- /dev/null +++ b/example/assets/web-auth.html @@ -0,0 +1,15 @@ + + + + + + + Web Auth + + +
+ + +
+ + \ No newline at end of file diff --git a/example/lib/headless_in_app_webview.screen.dart b/example/lib/headless_in_app_webview.screen.dart index f460b659..8cc3c185 100755 --- a/example/lib/headless_in_app_webview.screen.dart +++ b/example/lib/headless_in_app_webview.screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'main.dart'; @@ -86,12 +85,14 @@ class _HeadlessInAppWebViewExampleScreenState Center( child: ElevatedButton( onPressed: () async { - try { + if (headlessWebView?.isRunning() ?? false) { await headlessWebView?.webViewController.evaluateJavascript( source: """console.log('Here is the message!');"""); - } on MissingPluginException { - print( - "HeadlessInAppWebView is not running. Click on \"Run HeadlessInAppWebView\"!"); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + 'HeadlessInAppWebView is not running. Click on "Run HeadlessInAppWebView"!'), + )); } }, child: Text("Send console.log message")), diff --git a/example/lib/main.dart b/example/lib/main.dart index 66fad45d..9550c8f1 100755 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,10 +8,11 @@ import 'package:flutter_inappwebview_example/chrome_safari_browser_example.scree import 'package:flutter_inappwebview_example/headless_in_app_webview.screen.dart'; import 'package:flutter_inappwebview_example/in_app_webiew_example.screen.dart'; import 'package:flutter_inappwebview_example/in_app_browser_example.screen.dart'; +import 'package:flutter_inappwebview_example/web_authentication_session_example.screen.dart'; // import 'package:path_provider/path_provider.dart'; // import 'package:permission_handler/permission_handler.dart'; -// InAppLocalhostServer localhostServer = new InAppLocalhostServer(); +InAppLocalhostServer localhostServer = new InAppLocalhostServer(); Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -25,6 +26,8 @@ Future main() async { await InAppWebViewController.setWebContentsDebuggingEnabled(true); } + await localhostServer.start(); + runApp(MyApp()); } @@ -51,6 +54,12 @@ Drawer myDrawer({required BuildContext context}) { Navigator.pushReplacementNamed(context, '/ChromeSafariBrowser'); }, ), + ListTile( + title: Text('WebAuthenticationSession'), + onTap: () { + Navigator.pushReplacementNamed(context, '/WebAuthenticationSession'); + }, + ), ListTile( title: Text('InAppWebView'), onTap: () { @@ -91,6 +100,7 @@ class _MyAppState extends State { '/InAppBrowser': (context) => InAppBrowserExampleScreen(), '/ChromeSafariBrowser': (context) => ChromeSafariBrowserExampleScreen(), '/HeadlessInAppWebView': (context) => HeadlessInAppWebViewExampleScreen(), + '/WebAuthenticationSession': (context) => WebAuthenticationSessionExampleScreen(), }); } } diff --git a/example/lib/web_authentication_session_example.screen.dart b/example/lib/web_authentication_session_example.screen.dart new file mode 100755 index 00000000..bb88d8b0 --- /dev/null +++ b/example/lib/web_authentication_session_example.screen.dart @@ -0,0 +1,108 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'main.dart'; + +class WebAuthenticationSessionExampleScreen extends StatefulWidget { + @override + _WebAuthenticationSessionExampleScreenState createState() => + _WebAuthenticationSessionExampleScreenState(); +} + +class _WebAuthenticationSessionExampleScreenState + extends State { + WebAuthenticationSession? session; + String? token; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + session?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + "WebAuthenticationSession", + )), + drawer: myDrawer(context: context), + body: SafeArea( + child: Column(children: [ + Center( + child: Container( + padding: EdgeInsets.all(20.0), + child: Text("Token: $token"), + )), + session != null + ? Container() + : Center( + child: ElevatedButton( + onPressed: () async { + if (session == null && + !kIsWeb && + defaultTargetPlatform == TargetPlatform.iOS && + await WebAuthenticationSession.isAvailable()) { + session = await WebAuthenticationSession.create( + url: Uri.parse( + "http://localhost:8080/assets/web-auth.html"), + callbackURLScheme: "test", + onComplete: (url, error) async { + if (url != null) { + setState(() { + token = url.queryParameters["token"]; + }); + } + }); + setState(() {}); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + 'Cannot create Web Authentication Session!'), + )); + } + }, + child: Text("Create Web Auth Session")), + ), + session == null + ? Container() + : Center( + child: ElevatedButton( + onPressed: () async { + var started = false; + if (await session?.canStart() ?? false) { + started = await session?.start() ?? false; + } + if (!started) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + 'Cannot start Web Authentication Session!'), + )); + } + }, + child: Text("Start Web Auth Session")), + ), + session == null + ? Container() + : Center( + child: ElevatedButton( + onPressed: () async { + await session?.dispose(); + setState(() { + token = null; + session = null; + }); + }, + child: Text("Dispose Web Auth Session")), + ) + ]), + )); + } +} diff --git a/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift b/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift index d4f726aa..1c4a3b79 100755 --- a/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift +++ b/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift @@ -27,7 +27,7 @@ public class ChromeSafariBrowserManager: ChannelDelegate { switch call.method { case "open": - let id: String = arguments!["id"] as! String + let id = arguments!["id"] as! String let url = arguments!["url"] as! String let settings = arguments!["settings"] as! [String: Any?] let menuItemList = arguments!["menuItemList"] as! [[String: Any]] @@ -77,6 +77,8 @@ public class ChromeSafariBrowserManager: ChannelDelegate { flutterViewController.present(safari, animated: true) { result(true) } + + ChromeSafariBrowserManager.browsers[id] = safari } return } diff --git a/ios/Classes/SafariViewController/CustomUIActivity.swift b/ios/Classes/SafariViewController/CustomUIActivity.swift new file mode 100644 index 00000000..b661a9ba --- /dev/null +++ b/ios/Classes/SafariViewController/CustomUIActivity.swift @@ -0,0 +1,53 @@ +// +// CustomUIActivity.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation + +class CustomUIActivity : UIActivity { + var viewId: String + var id: Int64 + var url: URL + var title: String? + var type: UIActivity.ActivityType? + var label: String? + var image: UIImage? + + init(viewId: String, id: Int64, url: URL, title: String?, label: String?, type: UIActivity.ActivityType?, image: UIImage?) { + self.viewId = viewId + self.id = id + self.url = url + self.title = title + self.label = label + self.type = type + self.image = image + } + + override class var activityCategory: UIActivity.Category { + return .action + } + + override var activityType: UIActivity.ActivityType? { + return type + } + + override var activityTitle: String? { + return label + } + + override var activityImage: UIImage? { + return image + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + override func perform() { + let browser = ChromeSafariBrowserManager.browsers[viewId] + browser??.channelDelegate?.onChromeSafariBrowserMenuItemActionPerform(id: id, url: url, title: title) + } +} diff --git a/ios/Classes/SafariViewController/SafariViewController.swift b/ios/Classes/SafariViewController/SafariViewController.swift index cf8fed37..ee88280d 100755 --- a/ios/Classes/SafariViewController/SafariViewController.swift +++ b/ios/Classes/SafariViewController/SafariViewController.swift @@ -39,11 +39,6 @@ public class SafariViewController: SFSafariViewController, SFSafariViewControlle self.delegate = self } - deinit { - debugPrint("SafariViewController - dealloc") - dispose() - } - public override func viewWillAppear(_ animated: Bool) { // prepareSafariBrowser() super.viewWillAppear(animated) @@ -127,49 +122,9 @@ public class SafariViewController: SFSafariViewController, SFSafariViewControlle delegate = nil ChromeSafariBrowserManager.browsers[id] = nil } -} - -class CustomUIActivity : UIActivity { - var viewId: String - var id: Int64 - var url: URL - var title: String? - var type: UIActivity.ActivityType? - var label: String? - var image: UIImage? - init(viewId: String, id: Int64, url: URL, title: String?, label: String?, type: UIActivity.ActivityType?, image: UIImage?) { - self.viewId = viewId - self.id = id - self.url = url - self.title = title - self.label = label - self.type = type - self.image = image - } - - override class var activityCategory: UIActivity.Category { - return .action - } - - override var activityType: UIActivity.ActivityType? { - return type - } - - override var activityTitle: String? { - return label - } - - override var activityImage: UIImage? { - return image - } - - override func canPerform(withActivityItems activityItems: [Any]) -> Bool { - return true - } - - override func perform() { - let browser = ChromeSafariBrowserManager.browsers[viewId] - browser??.channelDelegate?.onChromeSafariBrowserMenuItemActionPerform(id: id, url: url, title: title) + deinit { + debugPrint("SafariViewController - dealloc") + dispose() } } diff --git a/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift b/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift index 2ace93cb..dcccd803 100644 --- a/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift +++ b/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift @@ -18,16 +18,16 @@ public class SafariViewControllerChannelDelegate : ChannelDelegate { public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // let arguments = call.arguments as? NSDictionary switch call.method { - case "close": - if let safariViewController = safariViewController { - safariViewController.close(result: result) - } else { - result(false) - } - break - default: - result(FlutterMethodNotImplemented) - break + case "close": + if let safariViewController = safariViewController { + safariViewController.close(result: result) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break } } diff --git a/ios/Classes/SwiftFlutterPlugin.swift b/ios/Classes/SwiftFlutterPlugin.swift index e5fbe685..cb0ca7ac 100755 --- a/ios/Classes/SwiftFlutterPlugin.swift +++ b/ios/Classes/SwiftFlutterPlugin.swift @@ -34,6 +34,7 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { var inAppBrowserManager: InAppBrowserManager? var headlessInAppWebViewManager: HeadlessInAppWebViewManager? var chromeSafariBrowserManager: ChromeSafariBrowserManager? + var webAuthenticationSessionManager: WebAuthenticationSessionManager? var webViewControllers: [String: InAppBrowserWebViewController?] = [:] var safariViewControllers: [String: Any?] = [:] @@ -56,6 +57,7 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { if #available(iOS 9.0, *) { myWebStorageManager = MyWebStorageManager(registrar: registrar) } + webAuthenticationSessionManager = WebAuthenticationSessionManager(registrar: registrar) } public static func register(with registrar: FlutterPluginRegistrar) { @@ -83,5 +85,7 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { (myWebStorageManager as! MyWebStorageManager?)?.dispose() myWebStorageManager = nil } + webAuthenticationSessionManager?.dispose() + webAuthenticationSessionManager = nil } } diff --git a/ios/Classes/WebAuthenticationSession/WebAuthenticationSession.swift b/ios/Classes/WebAuthenticationSession/WebAuthenticationSession.swift new file mode 100644 index 00000000..d69ad535 --- /dev/null +++ b/ios/Classes/WebAuthenticationSession/WebAuthenticationSession.swift @@ -0,0 +1,106 @@ +// +// WebAuthenticationSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices + +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(iOS 12.0, *) { + let session = ASWebAuthenticationSession(url: self.url, callbackURLScheme: self.callbackURLScheme, completionHandler: self.completionHandler) + if #available(iOS 13.0, *) { + session.presentationContextProvider = self + } + self.session = session + } else if #available(iOS 11.0, *) { + self.session = SFAuthenticationSession(url: self.url, callbackURLScheme: self.callbackURLScheme, completionHandler: self.completionHandler) + } + 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(iOS 13.0, *), 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(iOS 13.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(iOS 12.0, *), let session = session as? ASWebAuthenticationSession { + started = session.start() + } else if #available(iOS 11.0, *), let session = session as? SFAuthenticationSession { + started = session.start() + } + if started { + _canStart = false + } + return started + } + + public func cancel() { + guard let session = session else { + return + } + if #available(iOS 12.0, *), let session = session as? ASWebAuthenticationSession { + session.cancel() + } else if #available(iOS 11.0, *), let session = session as? SFAuthenticationSession { + session.cancel() + } + } + + @available(iOS 12.0, *) + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return UIApplication.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/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift b/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift new file mode 100644 index 00000000..ef517bc6 --- /dev/null +++ b/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift @@ -0,0 +1,73 @@ +// +// WebAuthenticationSessionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation + +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/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift b/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift new file mode 100644 index 00000000..bfb21b9a --- /dev/null +++ b/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift @@ -0,0 +1,78 @@ +// +// WebAuthenticationSessionManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Flutter +import UIKit +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/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift b/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift new file mode 100644 index 00000000..b737381e --- /dev/null +++ b/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift @@ -0,0 +1,30 @@ +// +// 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(iOS 12.0, *), let session = obj?.session as? ASWebAuthenticationSession { + if #available(iOS 13.0, *) { + realOptions["prefersEphemeralWebBrowserSession"] = session.prefersEphemeralWebBrowserSession + } + } + return realOptions + } +} diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index 688b9c5b..2effd347 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -62,7 +62,7 @@ class ChromeSafariBrowser { id = IdGenerator.generate(); this._channel = MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser_$id'); - this._channel.setMethodCallHandler(handleMethod); + this._channel.setMethodCallHandler(_handleMethod); _isOpened = false; } @@ -87,7 +87,7 @@ class ChromeSafariBrowser { } } - Future handleMethod(MethodCall call) async { + Future _handleMethod(MethodCall call) async { _debugLog(call.method, call.arguments); switch (call.method) { @@ -131,7 +131,9 @@ class ChromeSafariBrowser { ChromeSafariBrowserSettings? settings}) async { assert(url.toString().isNotEmpty); this.throwIsAlreadyOpened(message: 'Cannot open $url!'); - if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) { + if (!kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS)) { assert(['http', 'https'].contains(url.scheme), 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); } diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index 9da0c544..4051a3e7 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -72,7 +72,6 @@ class InAppBrowser { ///The default value is [WebViewImplementation.NATIVE]. final WebViewImplementation implementation; - /// InAppBrowser( {this.windowId, this.initialUserScripts, @@ -80,13 +79,13 @@ class InAppBrowser { id = IdGenerator.generate(); this._channel = MethodChannel('com.pichillilorenzo/flutter_inappbrowser_$id'); - this._channel.setMethodCallHandler(handleMethod); + this._channel.setMethodCallHandler(_handleMethod); _isOpened = false; webViewController = new InAppWebViewController.fromInAppBrowser( this._channel, this, this.initialUserScripts); } - Future handleMethod(MethodCall call) async { + Future _handleMethod(MethodCall call) async { switch (call.method) { case "onBrowserCreated": this._isOpened = true; diff --git a/lib/src/in_app_webview/headless_in_app_webview.dart b/lib/src/in_app_webview/headless_in_app_webview.dart index 9aac14a6..02aaa295 100644 --- a/lib/src/in_app_webview/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -17,7 +17,7 @@ import '../util.dart'; ///Class that represents a WebView in headless mode. ///It can be used to run a WebView in background without attaching an `InAppWebView` to the widget tree. /// -///Remember to dispose it when you don't need it anymore. +///**NOTE**: Remember to dispose it when you don't need it anymore. /// ///**Supported Platforms/Implementations**: ///- Android native WebView @@ -455,11 +455,11 @@ class HeadlessInAppWebView implements WebView { ///Use [onLoadResourceWithCustomScheme] instead. @Deprecated('Use onLoadResourceWithCustomScheme instead') @override - final Future Function( + Future Function( InAppWebViewController controller, Uri url)? onLoadResourceCustomScheme; @override - final Future Function( + Future Function( InAppWebViewController controller, WebResourceRequest request)? onLoadResourceWithCustomScheme; @override diff --git a/lib/src/main.dart b/lib/src/main.dart index 3afb37c8..9f53aab5 100644 --- a/lib/src/main.dart +++ b/lib/src/main.dart @@ -14,4 +14,5 @@ export 'http_auth_credentials_database.dart'; export 'context_menu.dart'; export 'pull_to_refresh/main.dart'; export 'web_message/main.dart'; +export 'web_authentication_session/main.dart'; export 'debug_logging_settings.dart'; diff --git a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart index 12e37900..b8f9aeee 100644 --- a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart +++ b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -40,7 +40,7 @@ class PullToRefreshController { this.settings = settings ?? PullToRefreshSettings(); } - Future handleMethod(MethodCall call) async { + Future _handleMethod(MethodCall call) async { switch (call.method) { case "onRefresh": if (onRefresh != null) onRefresh!(); @@ -166,6 +166,6 @@ class PullToRefreshController { void initMethodChannel(dynamic id) { this._channel = MethodChannel( 'com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_$id'); - this._channel?.setMethodCallHandler(handleMethod); + this._channel?.setMethodCallHandler(_handleMethod); } } diff --git a/lib/src/types/main.dart b/lib/src/types/main.dart index 0152a459..73ad6b2e 100644 --- a/lib/src/types/main.dart +++ b/lib/src/types/main.dart @@ -148,4 +148,5 @@ export 'media_capture_state.dart'; export 'proxy_rule.dart'; export 'proxy_scheme_filter.dart'; export 'force_dark_strategy.dart'; -export 'url_request_attribution.dart'; \ No newline at end of file +export 'url_request_attribution.dart'; +export 'web_authentication_session_error.dart'; \ No newline at end of file diff --git a/lib/src/types/web_authentication_session_error.dart b/lib/src/types/web_authentication_session_error.dart new file mode 100644 index 00000000..d4220297 --- /dev/null +++ b/lib/src/types/web_authentication_session_error.dart @@ -0,0 +1,57 @@ +///Class that represents the error code for a web authentication session error. +class WebAuthenticationSessionError { + final int _value; + + const WebAuthenticationSessionError._internal(this._value); + + ///Set of all values of [WebAuthenticationSessionError]. + static final Set values = [ + WebAuthenticationSessionError.CANCELED_LOGIN, + WebAuthenticationSessionError.PRESENTATION_CONTEXT_NOT_PROVIDED, + WebAuthenticationSessionError.PRESENTATION_CONTEXT_INVALID + ].toSet(); + + ///Gets a possible [WebAuthenticationSessionError] instance from an [int] value. + static WebAuthenticationSessionError? fromValue(int? value) { + if (value != null) { + try { + return WebAuthenticationSessionError.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets [int] value. + int toValue() => _value; + + @override + String toString() { + switch (_value) { + case 1: + return "CANCELED_LOGIN"; + case 2: + return "PRESENTATION_CONTEXT_NOT_PROVIDED"; + case 3: + return "PRESENTATION_CONTEXT_INVALID"; + default: + return "UNKNOWN"; + } + } + + ///The login has been canceled. + static final CANCELED_LOGIN = WebAuthenticationSessionError._internal(1); + + ///A context wasn’t provided. + static final PRESENTATION_CONTEXT_NOT_PROVIDED = WebAuthenticationSessionError._internal(2); + + ///The context was invalid. + static final PRESENTATION_CONTEXT_INVALID = WebAuthenticationSessionError._internal(3); + + bool operator ==(value) => value == _value; + + @override + int get hashCode => _value.hashCode; +} diff --git a/lib/src/web_authentication_session/main.dart b/lib/src/web_authentication_session/main.dart new file mode 100644 index 00000000..9f942e6e --- /dev/null +++ b/lib/src/web_authentication_session/main.dart @@ -0,0 +1,2 @@ +export 'web_authenticate_session.dart'; +export 'web_authenticate_session_settings.dart'; diff --git a/lib/src/web_authentication_session/web_authenticate_session.dart b/lib/src/web_authentication_session/web_authenticate_session.dart new file mode 100755 index 00000000..d0f2d422 --- /dev/null +++ b/lib/src/web_authentication_session/web_authenticate_session.dart @@ -0,0 +1,193 @@ +import 'dart:async'; +import 'dart:developer' as developer; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import '../util.dart'; +import '../debug_logging_settings.dart'; +import '../types/main.dart'; + +import 'web_authenticate_session_settings.dart'; + +///A completion handler for the [WebAuthenticationSession]. +typedef WebAuthenticationSessionCompletionHandler = Future Function(Uri? url, WebAuthenticationSessionError? error)?; + +///A session that an app uses to authenticate a user through a web service. +/// +///It is implemented using [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) on iOS 12.0+ +///and [SFAuthenticationSession](https://developer.apple.com/documentation/safariservices/sfauthenticationsession) on iOS 11.0. +/// +///Use an [WebAuthenticationSession] instance to authenticate a user through a web service, including one run by a third party. +///Initialize the session with a URL that points to the authentication webpage. +///A browser loads and displays the page, from which the user can authenticate. +///In iOS, the browser is a secure, embedded web view. +///In macOS, the system opens the user’s default browser if it supports web authentication sessions, or Safari otherwise. +/// +///On completion, the service sends a callback URL to the session with an authentication token, and the session passes this URL back to the app through a completion handler. +///[WebAuthenticationSession] ensures that only the calling app’s session receives the authentication callback, even when more than one app registers the same callback URL scheme. +/// +///**NOTE**: Remember to dispose it when you don't need it anymore. +/// +///**NOTE for iOS**: Available only on iOS 11.0+. +/// +///**Supported Platforms/Implementations**: +///- iOS +class WebAuthenticationSession { + ///Debug settings. + static DebugLoggingSettings debugLoggingSettings = DebugLoggingSettings(); + + ///ID used internally. + late final String id; + + ///A URL with the `http` or `https` scheme pointing to the authentication webpage. + final Uri url; + + ///The custom URL scheme that the app expects in the callback URL. + final String? callbackURLScheme; + + ///Initial settings. + late final WebAuthenticationSessionSettings? initialSettings; + + ///A completion handler the session calls when it completes successfully, or when the user cancels the session. + WebAuthenticationSessionCompletionHandler onComplete; + + late MethodChannel _channel; + static const MethodChannel _sharedChannel = const MethodChannel( + 'com.pichillilorenzo/flutter_webauthenticationsession'); + + ///Used to create and initialize a session. + static Future create( + {required Uri url, + String? callbackURLScheme, + WebAuthenticationSessionCompletionHandler onComplete, + WebAuthenticationSessionSettings? initialSettings}) async { + var session = WebAuthenticationSession._create( + url: url, + callbackURLScheme: callbackURLScheme, + onComplete: onComplete, + initialSettings: initialSettings); + initialSettings = + session.initialSettings ?? WebAuthenticationSessionSettings(); + Map args = {}; + args.putIfAbsent("id", () => session.id); + args.putIfAbsent("url", () => session.url.toString()); + args.putIfAbsent("callbackURLScheme", () => session.callbackURLScheme); + args.putIfAbsent("initialSettings", () => initialSettings?.toMap()); + await _sharedChannel.invokeMethod('create', args); + return session; + } + + WebAuthenticationSession._create( + {required this.url, + this.callbackURLScheme, + this.onComplete, + WebAuthenticationSessionSettings? initialSettings}) { + assert(url.toString().isNotEmpty); + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + assert(['http', 'https'].contains(url.scheme), + 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); + } + + id = IdGenerator.generate(); + this.initialSettings = + initialSettings ?? WebAuthenticationSessionSettings(); + this._channel = MethodChannel( + 'com.pichillilorenzo/flutter_webauthenticationsession_$id'); + this._channel.setMethodCallHandler(_handleMethod); + } + + _debugLog(String method, dynamic args) { + if (WebAuthenticationSession.debugLoggingSettings.enabled) { + for (var regExp + in WebAuthenticationSession.debugLoggingSettings.excludeFilter) { + if (regExp.hasMatch(method)) return; + } + var maxLogMessageLength = + WebAuthenticationSession.debugLoggingSettings.maxLogMessageLength; + String message = "WebAuthenticationSession ID " + + id + + " calling \"" + + method.toString() + + "\" using " + + args.toString(); + if (maxLogMessageLength >= 0 && message.length > maxLogMessageLength) { + message = message.substring(0, maxLogMessageLength) + "..."; + } + developer.log(message, name: this.runtimeType.toString()); + } + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onComplete": + String? url = call.arguments["url"]; + Uri? uri = url != null ? Uri.parse(url) : null; + var error = WebAuthenticationSessionError.fromValue( + call.arguments["errorCode"]); + if (onComplete != null) { + onComplete!(uri, error); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + ///Indicates whether the session can begin. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - ASWebAuthenticationSession.canStart](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3516277-canstart)) + Future canStart() async { + Map args = {}; + return await _channel.invokeMethod('canStart', args); + } + + ///Starts a web authentication session. + /// + ///Returns a boolean value indicating whether the web authentication session started successfully. + /// + ///Only call this method once for a given [WebAuthenticationSession] instance after initialization. + ///Calling the [start] method on a canceled session results in a failure. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - ASWebAuthenticationSession.start](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/2990953-start)) + Future start() async { + Map args = {}; + return await _channel.invokeMethod('start', args); + } + + ///Cancels a web authentication session. + /// + ///If the session has already presented a view with the authentication webpage, calling this method dismisses that view. + ///Calling [cancel] on an already canceled session has no effect. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - ASWebAuthenticationSession.cancel](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/2990951-cancel)) + Future cancel() async { + Map args = {}; + await _channel.invokeMethod("cancel", args); + } + + ///Disposes a web authentication session. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Future dispose() async { + Map args = {}; + await _channel.invokeMethod("dispose", args); + } + + ///Returns `true` if [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) + ///or [SFAuthenticationSession](https://developer.apple.com/documentation/safariservices/sfauthenticationsession) is available. + ///Otherwise returns `false`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + static Future isAvailable() async { + Map args = {}; + return await _sharedChannel.invokeMethod("isAvailable", args); + } +} diff --git a/lib/src/web_authentication_session/web_authenticate_session_settings.dart b/lib/src/web_authentication_session/web_authenticate_session_settings.dart new file mode 100755 index 00000000..d416d660 --- /dev/null +++ b/lib/src/web_authentication_session/web_authenticate_session_settings.dart @@ -0,0 +1,55 @@ +import 'package:flutter/foundation.dart'; +import 'web_authenticate_session.dart'; + +///Class that represents the settings that can be used for a [WebAuthenticationSession]. +class WebAuthenticationSessionSettings { + ///A Boolean value that indicates whether the session should ask the browser for a private authentication session. + /// + ///Set [prefersEphemeralWebBrowserSession] to `true` to request that the browser + ///doesn’t share cookies or other browsing data between the authentication session and the user’s normal browser session. + ///Whether the request is honored depends on the user’s default web browser. + ///Safari always honors the request. + /// + ///The value of this property is `false` by default. + /// + ///Set this property before you call [WebAuthenticationSession.start]. Otherwise it has no effect. + /// + ///**NOTE for iOS**: Available only on iOS 13.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool prefersEphemeralWebBrowserSession; + + WebAuthenticationSessionSettings( + {this.prefersEphemeralWebBrowserSession = false}); + + Map toMap() { + return { + "prefersEphemeralWebBrowserSession": prefersEphemeralWebBrowserSession + }; + } + + static WebAuthenticationSessionSettings fromMap(Map map) { + WebAuthenticationSessionSettings settings = + new WebAuthenticationSessionSettings(); + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + settings.prefersEphemeralWebBrowserSession = + map["prefersEphemeralWebBrowserSession"]; + } + return settings; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + WebAuthenticationSessionSettings copy() { + return WebAuthenticationSessionSettings.fromMap(this.toMap()); + } +} diff --git a/lib/src/web_message/web_message_channel.dart b/lib/src/web_message/web_message_channel.dart index 0a005626..d9e5d6a4 100644 --- a/lib/src/web_message/web_message_channel.dart +++ b/lib/src/web_message/web_message_channel.dart @@ -19,7 +19,7 @@ class WebMessageChannel { {required this.id, required this.port1, required this.port2}) { this._channel = MethodChannel( 'com.pichillilorenzo/flutter_inappwebview_web_message_channel_$id'); - this._channel.setMethodCallHandler(handleMethod); + this._channel.setMethodCallHandler(_handleMethod); } static WebMessageChannel? fromMap(Map? map) { @@ -35,7 +35,7 @@ class WebMessageChannel { return webMessageChannel; } - Future handleMethod(MethodCall call) async { + Future _handleMethod(MethodCall call) async { switch (call.method) { case "onMessage": int index = call.arguments["index"]; diff --git a/lib/src/web_message/web_message_listener.dart b/lib/src/web_message/web_message_listener.dart index e72ace03..9bb2cff8 100644 --- a/lib/src/web_message/web_message_listener.dart +++ b/lib/src/web_message/web_message_listener.dart @@ -36,10 +36,10 @@ class WebMessageListener { "allowedOriginRules cannot contain empty strings"); this._channel = MethodChannel( 'com.pichillilorenzo/flutter_inappwebview_web_message_listener_$jsObjectName'); - this._channel.setMethodCallHandler(handleMethod); + this._channel.setMethodCallHandler(_handleMethod); } - Future handleMethod(MethodCall call) async { + Future _handleMethod(MethodCall call) async { switch (call.method) { case "onPostMessage": if (_replyProxy == null) {