From e44685fb0193011350d760616f54be787c039a3f Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Tue, 6 Feb 2024 15:15:33 +0800 Subject: [PATCH 01/11] Decouple wechat from open url --- .../com/authgear/flutter/AuthgearPlugin.kt | 5 ++- ios/Classes/SwiftAuthgearPlugin.swift | 4 ++- lib/src/container.dart | 35 ++++++++++++++----- lib/src/native.dart | 34 +++++++++--------- 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt index ca3c8d3..84708b5 100644 --- a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt +++ b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt @@ -125,8 +125,11 @@ class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginReg } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - this.storeWechat(call) when (call.method) { + "registerWechatRedirectURI" -> { + this.storeWechat(call) + result.success(null) + } "authenticate" -> { val url = Uri.parse(call.argument("url")) val redirectURI = Uri.parse(call.argument("redirectURI")) diff --git a/ios/Classes/SwiftAuthgearPlugin.swift b/ios/Classes/SwiftAuthgearPlugin.swift index 4ea3275..d605e49 100644 --- a/ios/Classes/SwiftAuthgearPlugin.swift +++ b/ios/Classes/SwiftAuthgearPlugin.swift @@ -26,8 +26,10 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - self.storeWechat(arguments: call.arguments) switch call.method { + case "registerWechatRedirectURI": + self.storeWechat(arguments: call.arguments) + result(nil) case "authenticate": let arguments = call.arguments as! Dictionary let urlString = arguments["url"] as! String diff --git a/lib/src/container.dart b/lib/src/container.dart index 121ae13..ecec46c 100644 --- a/lib/src/container.dart +++ b/lib/src/container.dart @@ -255,12 +255,17 @@ class Authgear implements AuthgearHttpClientDelegate { wechatRedirectURI: wechatRedirectURI, page: page, )); + + if (wechatRedirectURI != null) { + await native.registerWechatRedirectURI( + onWechatRedirectURI: _onWechatRedirectURI, + wechatRedirectURI: wechatRedirectURI); + } + final resultURL = await native.authenticate( url: authRequest.url.toString(), redirectURI: authRequest.redirectURI, preferEphemeral: !isSsoEnabled, - wechatRedirectURI: wechatRedirectURI, - onWechatRedirectURI: _onWechatRedirectURI, ); return await internalFinishAuthentication( url: Uri.parse(resultURL), @@ -317,12 +322,16 @@ class Authgear implements AuthgearHttpClientDelegate { final request = await internalCreateReauthenticateRequest(idTokenHint, options); + if (wechatRedirectURI != null) { + await native.registerWechatRedirectURI( + onWechatRedirectURI: _onWechatRedirectURI, + wechatRedirectURI: wechatRedirectURI); + } + final resultURL = await native.authenticate( url: request.url.toString(), redirectURI: redirectURI, preferEphemeral: !isSsoEnabled, - wechatRedirectURI: wechatRedirectURI, - onWechatRedirectURI: _onWechatRedirectURI, ); final xDeviceInfo = await _getXDeviceInfo(); return await _finishReauthentication( @@ -387,10 +396,15 @@ class Authgear implements AuthgearHttpClientDelegate { redirectURI: url, wechatRedirectURI: wechatRedirectURI, ); + + if (wechatRedirectURI != null) { + await native.registerWechatRedirectURI( + onWechatRedirectURI: _onWechatRedirectURI, + wechatRedirectURI: wechatRedirectURI); + } + await native.openURL( url: targetURL.toString(), - wechatRedirectURI: wechatRedirectURI, - onWechatRedirectURI: _onWechatRedirectURI, ); } @@ -667,11 +681,16 @@ class Authgear implements AuthgearHttpClientDelegate { final config = await _apiClient.fetchOIDCConfiguration(); final authenticationURL = Uri.parse(config.authorizationEndpoint) .replace(queryParameters: oidcRequest.toQueryParameters()); + + if (wechatRedirectURI != null) { + await native.registerWechatRedirectURI( + onWechatRedirectURI: _onWechatRedirectURI, + wechatRedirectURI: wechatRedirectURI); + } + final resultURL = await native.authenticate( url: authenticationURL.toString(), redirectURI: redirectURI, - wechatRedirectURI: wechatRedirectURI, - onWechatRedirectURI: _onWechatRedirectURI, preferEphemeral: !isSsoEnabled, ); final userInfo = await internalFinishAuthentication( diff --git a/lib/src/native.dart b/lib/src/native.dart index 6b5008e..b0a8d84 100644 --- a/lib/src/native.dart +++ b/lib/src/native.dart @@ -12,12 +12,9 @@ String _getNextWechatMethodChannelName() { return "flutter_authgear:wechat:$counter"; } -Future authenticate({ - required String url, - required String redirectURI, - required bool preferEphemeral, +Future registerWechatRedirectURI({ required void Function(Uri) onWechatRedirectURI, - required String? wechatRedirectURI, + required String wechatRedirectURI, }) async { try { final wechatMethodChannelName = _getNextWechatMethodChannelName(); @@ -26,12 +23,25 @@ Future authenticate({ final uri = Uri.parse(call.arguments); onWechatRedirectURI(uri); }); + return await _channel.invokeMethod("registerWechatRedirectURI", { + "wechatRedirectURI": wechatRedirectURI, + "wechatMethodChannel": wechatMethodChannelName, + }); + } on PlatformException catch (e) { + throw wrapException(e); + } +} + +Future authenticate({ + required String url, + required String redirectURI, + required bool preferEphemeral, +}) async { + try { return await _channel.invokeMethod("authenticate", { "url": url, "redirectURI": redirectURI, "preferEphemeral": preferEphemeral, - "wechatRedirectURI": wechatRedirectURI, - "wechatMethodChannel": wechatMethodChannelName, }); } on PlatformException catch (e) { throw wrapException(e); @@ -40,20 +50,10 @@ Future authenticate({ Future openURL({ required String url, - required void Function(Uri) onWechatRedirectURI, - required String? wechatRedirectURI, }) async { try { - final wechatMethodChannelName = _getNextWechatMethodChannelName(); - final wechatMethodChannel = MethodChannel(wechatMethodChannelName); - wechatMethodChannel.setMethodCallHandler((call) async { - final uri = Uri.parse(call.arguments); - onWechatRedirectURI(uri); - }); await _channel.invokeMethod("openURL", { "url": url, - "wechatRedirectURI": wechatRedirectURI, - "wechatMethodChannel": wechatMethodChannelName, }); } on PlatformException catch (e) { throw wrapException(e); From 2c4a90550ee8be68a5df9ebecb6cac39c7c2d338 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Tue, 6 Feb 2024 15:44:55 +0800 Subject: [PATCH 02/11] Introduce WebView --- lib/flutter_authgear.dart | 4 ++++ lib/src/container.dart | 16 ++++++++++------ lib/src/webview.dart | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 lib/src/webview.dart diff --git a/lib/flutter_authgear.dart b/lib/flutter_authgear.dart index 8b01fe3..61d0ec6 100644 --- a/lib/flutter_authgear.dart +++ b/lib/flutter_authgear.dart @@ -44,3 +44,7 @@ export 'src/exception.dart' BiometricNoPasscodeException, BiometricNoEnrollmentException, BiometricLockoutException; +export 'src/webview.dart' + show + WebView, + DefaultWebView; diff --git a/lib/src/container.dart b/lib/src/container.dart index ecec46c..0c6166a 100644 --- a/lib/src/container.dart +++ b/lib/src/container.dart @@ -12,6 +12,7 @@ import 'base64.dart'; import 'id_token.dart'; import 'experimental.dart'; import 'native.dart' as native; +import 'webview.dart' show WebView, DefaultWebView; class SessionStateChangeEvent { final SessionStateChangeReason reason; @@ -139,6 +140,7 @@ class Authgear implements AuthgearHttpClientDelegate { final TokenStorage _tokenStorage; final ContainerStorage _storage; + final WebView _webView; late final APIClient _apiClient; late final AuthgearExperimental experimental; @@ -188,7 +190,9 @@ class Authgear implements AuthgearHttpClientDelegate { this.isSsoEnabled = false, this.sendWechatAuthRequest, TokenStorage? tokenStorage, + WebView? webView, }) : _tokenStorage = tokenStorage ?? PersistentTokenStorage(), + _webView = webView ?? DefaultWebView(), _storage = PersistentContainerStorage() { final plainHttpClient = Client(); final authgearHttpClient = AuthgearHttpClient(this, plainHttpClient); @@ -262,10 +266,10 @@ class Authgear implements AuthgearHttpClientDelegate { wechatRedirectURI: wechatRedirectURI); } - final resultURL = await native.authenticate( + final resultURL = await _webView.openAuthorizationURL( url: authRequest.url.toString(), redirectURI: authRequest.redirectURI, - preferEphemeral: !isSsoEnabled, + shareCookiesWithDeviceBrowser: isSsoEnabled, ); return await internalFinishAuthentication( url: Uri.parse(resultURL), @@ -328,10 +332,10 @@ class Authgear implements AuthgearHttpClientDelegate { wechatRedirectURI: wechatRedirectURI); } - final resultURL = await native.authenticate( + final resultURL = await _webView.openAuthorizationURL( url: request.url.toString(), redirectURI: redirectURI, - preferEphemeral: !isSsoEnabled, + shareCookiesWithDeviceBrowser: isSsoEnabled, ); final xDeviceInfo = await _getXDeviceInfo(); return await _finishReauthentication( @@ -688,10 +692,10 @@ class Authgear implements AuthgearHttpClientDelegate { wechatRedirectURI: wechatRedirectURI); } - final resultURL = await native.authenticate( + final resultURL = await _webView.openAuthorizationURL( url: authenticationURL.toString(), redirectURI: redirectURI, - preferEphemeral: !isSsoEnabled, + shareCookiesWithDeviceBrowser: isSsoEnabled, ); final userInfo = await internalFinishAuthentication( url: Uri.parse(resultURL), diff --git a/lib/src/webview.dart b/lib/src/webview.dart new file mode 100644 index 0000000..cc51ec4 --- /dev/null +++ b/lib/src/webview.dart @@ -0,0 +1,24 @@ +import 'native.dart' as native; + +abstract class WebView { + Future openAuthorizationURL( + {required String url, + required String redirectURI, + required bool shareCookiesWithDeviceBrowser}); +} + +class DefaultWebView implements WebView { + @override + Future openAuthorizationURL({ + required String url, + required String redirectURI, + required bool shareCookiesWithDeviceBrowser, + }) { + final preferEphemeral = !shareCookiesWithDeviceBrowser; + return native.authenticate( + url: url, + redirectURI: redirectURI, + preferEphemeral: preferEphemeral, + ); + } +} From b9f915c84555fa1ac6824ec68a4e804660b1d04c Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Tue, 6 Feb 2024 15:52:19 +0800 Subject: [PATCH 03/11] Rename native method authenticate to openAuthorizeURL --- .../src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt | 2 +- ios/Classes/SwiftAuthgearPlugin.swift | 6 +++--- lib/src/native.dart | 4 ++-- lib/src/webview.dart | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt index 84708b5..3e19234 100644 --- a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt +++ b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt @@ -130,7 +130,7 @@ class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginReg this.storeWechat(call) result.success(null) } - "authenticate" -> { + "openAuthorizeURL" -> { val url = Uri.parse(call.argument("url")) val redirectURI = Uri.parse(call.argument("redirectURI")) // Custom tabs do not support incognito mode for now. diff --git a/ios/Classes/SwiftAuthgearPlugin.swift b/ios/Classes/SwiftAuthgearPlugin.swift index d605e49..a78fe62 100644 --- a/ios/Classes/SwiftAuthgearPlugin.swift +++ b/ios/Classes/SwiftAuthgearPlugin.swift @@ -30,14 +30,14 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr case "registerWechatRedirectURI": self.storeWechat(arguments: call.arguments) result(nil) - case "authenticate": + case "openAuthorizeURL": let arguments = call.arguments as! Dictionary let urlString = arguments["url"] as! String let redirectURIString = arguments["redirectURI"] as! String let preferEphemeral = arguments["preferEphemeral"] as! Bool let url = URL(string: urlString)! let redirectURI = URL(string: redirectURIString)! - self.authenticate(url: url, redirectURI: redirectURI, preferEphemeral: preferEphemeral, result: result) + self.openAuthorizeURL(url: url, redirectURI: redirectURI, preferEphemeral: preferEphemeral, result: result) case "openURL": let arguments = call.arguments as! Dictionary let urlString = arguments["url"] as! String @@ -155,7 +155,7 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr return true } - private func authenticate(url: URL, redirectURI: URL, preferEphemeral: Bool, result: @escaping FlutterResult) { + private func openAuthorizeURL(url: URL, redirectURI: URL, preferEphemeral: Bool, result: @escaping FlutterResult) { var sessionToKeepAlive: Any? = nil let completionHandler = { (url: URL?, error: Error?) in sessionToKeepAlive = nil diff --git a/lib/src/native.dart b/lib/src/native.dart index b0a8d84..f3aef95 100644 --- a/lib/src/native.dart +++ b/lib/src/native.dart @@ -32,13 +32,13 @@ Future registerWechatRedirectURI({ } } -Future authenticate({ +Future openAuthorizeURL({ required String url, required String redirectURI, required bool preferEphemeral, }) async { try { - return await _channel.invokeMethod("authenticate", { + return await _channel.invokeMethod("openAuthorizeURL", { "url": url, "redirectURI": redirectURI, "preferEphemeral": preferEphemeral, diff --git a/lib/src/webview.dart b/lib/src/webview.dart index cc51ec4..681dc3d 100644 --- a/lib/src/webview.dart +++ b/lib/src/webview.dart @@ -15,7 +15,7 @@ class DefaultWebView implements WebView { required bool shareCookiesWithDeviceBrowser, }) { final preferEphemeral = !shareCookiesWithDeviceBrowser; - return native.authenticate( + return native.openAuthorizeURL( url: url, redirectURI: redirectURI, preferEphemeral: preferEphemeral, From 19073759298918a4ededc735230ffc88838fa41f Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Tue, 6 Feb 2024 18:48:50 +0800 Subject: [PATCH 04/11] Add PlatformWebView for iOS --- ios/Classes/AGWKWebViewController.swift | 198 ++++++++++++++++++++++++ ios/Classes/SwiftAuthgearPlugin.swift | 94 ++++++++++- lib/flutter_authgear.dart | 6 +- lib/src/native.dart | 22 +++ lib/src/webview.dart | 67 ++++++++ 5 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 ios/Classes/AGWKWebViewController.swift diff --git a/ios/Classes/AGWKWebViewController.swift b/ios/Classes/AGWKWebViewController.swift new file mode 100644 index 0000000..2e11889 --- /dev/null +++ b/ios/Classes/AGWKWebViewController.swift @@ -0,0 +1,198 @@ +import Foundation +import UIKit +import WebKit + +let AGWKWebViewControllerErrorDomain: String = "AGWKWebViewController" +let AGWKWebViewControllerErrorCodeCanceledLogin: Int = 1 + +protocol AGWKWebViewControllerPresentationContextProviding: AnyObject { + func presentationAnchor(for: AGWKWebViewController) -> UIWindow +} + +class AGWKWebViewController: UIViewController, WKNavigationDelegate { + typealias CompletionHandler = (URL?, Error?) -> Void + weak var presentationContextProvider: AGWKWebViewControllerPresentationContextProviding? + var backgroundColor: UIColor? + var navigationBarBackgroundColor: UIColor? + var navigationBarButtonTintColor: UIColor? + + private let url: URL + private let redirectURI: URL + private var completionHandler: CompletionHandler? + private let webView: WKWebView + private var result: URL? + + private var defaultBackgroundColor: UIColor { + get { + if #available(iOS 12.0, *) { + switch (self.traitCollection.userInterfaceStyle) { + case .dark: + return UIColor.black + default: + return UIColor.white + } + } + return UIColor.white + } + } + + init(url: URL, redirectURI: URL, completionHandler: @escaping CompletionHandler) { + self.url = url + self.redirectURI = redirectURI + self.completionHandler = completionHandler + + let configuration = WKWebViewConfiguration() + self.webView = WKWebView(frame: .zero, configuration: configuration) + self.webView.translatesAutoresizingMaskIntoConstraints = false + self.webView.allowsBackForwardNavigationGestures = true + + super.init(nibName: nil, bundle: nil) + + self.webView.navigationDelegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + // Configure background color + if let backgroundColor = self.backgroundColor { + self.view.backgroundColor = backgroundColor + } else { + self.view.backgroundColor = self.defaultBackgroundColor + } + + // Configure layout + self.view.addSubview(self.webView) + if #available(iOS 11.0, *) { + self.webView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true + self.webView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true + self.webView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true + self.webView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true + } + + // Configure the bounce behavior + self.webView.scrollView.bounces = false + self.webView.scrollView.alwaysBounceVertical = false + self.webView.scrollView.alwaysBounceHorizontal = false + + // Configure navigation bar appearance + if #available(iOS 13.0, *) { + let appearance = UINavigationBarAppearance() + appearance.configureWithTransparentBackground() + if let navigationBarBackgroundColor = self.navigationBarBackgroundColor { + appearance.backgroundColor = navigationBarBackgroundColor + } + self.navigationItem.standardAppearance = appearance + self.navigationItem.compactAppearance = appearance + self.navigationItem.scrollEdgeAppearance = appearance + if #available(iOS 15.0, *) { + self.navigationItem.compactScrollEdgeAppearance = appearance + } + } + + // Configure back button + self.navigationItem.hidesBackButton = true + var backButton: UIBarButtonItem + if #available(iOS 13.0, *) { + let backButtonImage = UIImage(systemName: "chevron.backward") + backButton = UIBarButtonItem(image: backButtonImage, style: .plain, target: self, action: #selector(onTapBackButton)) + } else { + backButton = UIBarButtonItem(title: "<", style: .plain, target: self, action: #selector(onTapBackButton)) + } + if let navigationBarButtonTintColor = self.navigationBarButtonTintColor { + backButton.tintColor = navigationBarButtonTintColor + } + self.navigationItem.leftBarButtonItem = backButton + + // Configure close button + var closeButton: UIBarButtonItem + if #available(iOS 13.0, *) { + let closeButtonImage = UIImage(systemName: "xmark") + closeButton = UIBarButtonItem(image: closeButtonImage, style: .plain, target: self, action: #selector(onTapCloseButton)) + } else { + closeButton = UIBarButtonItem(title: "X", style: .plain, target: self, action: #selector(onTapCloseButton)) + } + if let navigationBarButtonTintColor = self.navigationBarButtonTintColor { + closeButton.tintColor = navigationBarButtonTintColor + } + self.navigationItem.rightBarButtonItem = closeButton + + let request = URLRequest(url: self.url) + self.webView.load(request) + } + + override func viewDidDisappear(_ animated: Bool) { + // We only call completion handler here because + // The view controller could be swiped to dismiss. + // viewDidDisappear is the most rebust way to detect whether the view controller is dismissed. + if let result = self.result { + self.completionHandler?(result, nil) + } else { + let err = NSError(domain: AGWKWebViewControllerErrorDomain, code: AGWKWebViewControllerErrorCodeCanceledLogin) + self.completionHandler?(nil, err) + } + self.completionHandler = nil + } + + @objc private func onTapBackButton() { + if (self.webView.canGoBack) { + _ = self.webView.goBack() + } else { + self.cancel() + } + } + + @objc private func onTapCloseButton() { + self.cancel() + } + + func cancel() { + self.dismissSelf() + } + + func start() { + if let presentationAnchor = self.presentationContextProvider?.presentationAnchor(for: self) { + let navigationController = UINavigationController(rootViewController: self) + // Use the configured modal presentation style. + navigationController.modalPresentationStyle = self.modalPresentationStyle + presentationAnchor.rootViewController?.present(navigationController, animated: true) + } + } + + private func dismissSelf() { + self.navigationController?.presentingViewController?.dismiss(animated: true) + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let navigationURL = navigationAction.request.url { + var parts = URLComponents(url: navigationURL, resolvingAgainstBaseURL: false) + parts?.query = nil + parts?.fragment = nil + if let partsString = parts?.string { + if partsString == self.redirectURI.absoluteString { + decisionHandler(.cancel) + self.result = navigationURL + self.dismissSelf() + return + } + } + } + + if #available(iOS 14.5, *) { + if navigationAction.shouldPerformDownload { + decisionHandler(.download) + return + } else { + decisionHandler(.allow) + return + } + } else { + decisionHandler(.allow) + return + } + } +} diff --git a/ios/Classes/SwiftAuthgearPlugin.swift b/ios/Classes/SwiftAuthgearPlugin.swift index a78fe62..fc9a925 100644 --- a/ios/Classes/SwiftAuthgearPlugin.swift +++ b/ios/Classes/SwiftAuthgearPlugin.swift @@ -4,7 +4,7 @@ import AuthenticationServices import LocalAuthentication import CommonCrypto -public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPresentationContextProviding { +public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPresentationContextProviding, AGWKWebViewControllerPresentationContextProviding { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "flutter_authgear", binaryMessenger: registrar.messenger()) let instance = SwiftAuthgearPlugin(binaryMessenger: registrar.messenger()) @@ -38,6 +38,23 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr let url = URL(string: urlString)! let redirectURI = URL(string: redirectURIString)! self.openAuthorizeURL(url: url, redirectURI: redirectURI, preferEphemeral: preferEphemeral, result: result) + case "openAuthorizeURLWithWebView": + let arguments = call.arguments as! Dictionary + let url = URL(string: arguments["url"] as! String)! + let redirectURI = URL(string: arguments["redirectURI"] as! String)! + let modalPresentationStyle = UIModalPresentationStyle.from(string: arguments["modalPresentationStyle"] as? String) + let backgroundColor = UIColor(argb: arguments["backgroundColor"] as? String) + let navigationBarBackgroundColor = UIColor(argb: arguments["navigationBarBackgroundColor"] as? String) + let navigationBarButtonTintColor = UIColor(argb: arguments["navigationBarButtonTintColor"] as? String) + self.openAuthorizeURLWithWebView( + url: url, + redirectURI: redirectURI, + modalPresentationStyle: modalPresentationStyle, + backgroundColor: backgroundColor, + navigationBarBackgroundColor: navigationBarBackgroundColor, + navigationBarButtonTintColor: navigationBarButtonTintColor, + result: result + ) case "openURL": let arguments = call.arguments as! Dictionary let urlString = arguments["url"] as! String @@ -197,6 +214,43 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr } } + private func openAuthorizeURLWithWebView( + url: URL, + redirectURI: URL, + modalPresentationStyle: UIModalPresentationStyle, + backgroundColor: UIColor?, + navigationBarBackgroundColor: UIColor?, + navigationBarButtonTintColor: UIColor?, + result: @escaping FlutterResult + ) { + let controller = AGWKWebViewController(url: url, redirectURI: redirectURI) { resultURL, error in + if let error = error { + let nsError = error as NSError + if nsError.domain == AGWKWebViewControllerErrorDomain && nsError.code == AGWKWebViewControllerErrorCodeCanceledLogin { + result(FlutterError.cancel) + return + } + + self.handleError(result: result, error: error) + return + } + + if let resultURL = resultURL { + result(resultURL.absoluteString) + return + } + + result(FlutterError.unreachable) + return + } + controller.backgroundColor = backgroundColor + controller.navigationBarBackgroundColor = navigationBarBackgroundColor + controller.navigationBarButtonTintColor = navigationBarButtonTintColor + controller.modalPresentationStyle = modalPresentationStyle + controller.presentationContextProvider = self + controller.start() + } + private func openURL(url: URL, result: @escaping FlutterResult) { var sessionToKeepAlive: Any? = nil let completionHandler = { (url: URL?, error: Error?) in @@ -768,6 +822,10 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr UIApplication.shared.windows.filter { $0.isKeyWindow }.first! } + func presentationAnchor(for: AGWKWebViewController) -> UIWindow { + UIApplication.shared.windows.filter { $0.isKeyWindow }.first! + } + private func handleError(result: FlutterResult, error: Error) { let nsError = error as NSError result(FlutterError( @@ -905,3 +963,37 @@ fileprivate extension UIUserInterfaceIdiom { } } } + +fileprivate extension UIModalPresentationStyle { + static func from(string: String?) -> UIModalPresentationStyle { + if let string = string { + switch string { + case "fullScreen": + return .fullScreen + case "pageSheet": + return .pageSheet + default: + break + } + } + if #available(iOS 13.0, *) { + return .automatic + } else { + return .fullScreen + } + } +} + +fileprivate extension UIColor { + convenience init?(argb: String?) { + guard let argb = argb else { + return nil + } + let argbInt = UInt32(argb, radix: 16)! + let a = CGFloat((argbInt >> 24) & 0xFF) / 255.0 + let r = CGFloat((argbInt >> 16) & 0xFF) / 255.0 + let g = CGFloat((argbInt >> 8) & 0xFF) / 255.0 + let b = CGFloat(argbInt & 0xFF) / 255.0 + self.init(red: r, green: g, blue: b, alpha: a) + } +} diff --git a/lib/flutter_authgear.dart b/lib/flutter_authgear.dart index 61d0ec6..e742e13 100644 --- a/lib/flutter_authgear.dart +++ b/lib/flutter_authgear.dart @@ -47,4 +47,8 @@ export 'src/exception.dart' export 'src/webview.dart' show WebView, - DefaultWebView; + DefaultWebView, + ModalPresentationStyle, + PlatformWebViewOptions, + PlatformWebViewOptionsIOS, + PlatformWebView; diff --git a/lib/src/native.dart b/lib/src/native.dart index f3aef95..58ac144 100644 --- a/lib/src/native.dart +++ b/lib/src/native.dart @@ -48,6 +48,28 @@ Future openAuthorizeURL({ } } +Future openAuthorizeURLWithWebView({ + required String url, + required String redirectURI, + String? modalPresentationStyle, + String? backgroundColor, + String? navigationBarBackgroundColor, + String? navigationBarButtonTintColor, +}) async { + try { + return await _channel.invokeMethod("openAuthorizeURLWithWebView", { + "url": url, + "redirectURI": redirectURI, + "modalPresentationStyle": modalPresentationStyle, + "backgroundColor": backgroundColor, + "navigationBarBackgroundColor": navigationBarBackgroundColor, + "navigationBarButtonTintColor": navigationBarButtonTintColor, + }); + } on PlatformException catch (e) { + throw wrapException(e); + } +} + Future openURL({ required String url, }) async { diff --git a/lib/src/webview.dart b/lib/src/webview.dart index 681dc3d..1e8426c 100644 --- a/lib/src/webview.dart +++ b/lib/src/webview.dart @@ -22,3 +22,70 @@ class DefaultWebView implements WebView { ); } } + +enum ModalPresentationStyle { + automatic, + fullScreen, + pageSheet, +} + +extension ModalPresentationStyleExtension on ModalPresentationStyle { + String get value { + switch (this) { + case ModalPresentationStyle.automatic: + return "automatic"; + case ModalPresentationStyle.fullScreen: + return "fullScreen"; + case ModalPresentationStyle.pageSheet: + return "pageSheet"; + } + } +} + +class PlatformWebViewOptionsIOS { + final ModalPresentationStyle? modalPresentationStyle; + final int? backgroundColor; + final int? navigationBarBackgroundColor; + final int? navigationBarButtonTintColor; + + PlatformWebViewOptionsIOS({ + this.modalPresentationStyle, + this.backgroundColor, + this.navigationBarBackgroundColor, + this.navigationBarButtonTintColor, + }); +} + +class PlatformWebViewOptions { + final PlatformWebViewOptionsIOS? ios; + + PlatformWebViewOptions({ + this.ios, + }); +} + +class PlatformWebView implements WebView { + final PlatformWebViewOptions? options; + + PlatformWebView({ + this.options, + }); + + @override + Future openAuthorizationURL({ + required String url, + required String redirectURI, + required bool shareCookiesWithDeviceBrowser, + }) { + return native.openAuthorizeURLWithWebView( + url: url, + redirectURI: redirectURI, + modalPresentationStyle: options?.ios?.modalPresentationStyle?.value, + backgroundColor: options?.ios?.backgroundColor?.toRadixString(16), + navigationBarBackgroundColor: + options?.ios?.navigationBarBackgroundColor?.toRadixString(16), + navigationBarButtonTintColor: + options?.ios?.navigationBarButtonTintColor?.toRadixString(16), + ); + } +} From 5d27410f6ef832fa702c056dc7bddce3b70e49f4 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Tue, 6 Feb 2024 18:53:39 +0800 Subject: [PATCH 05/11] Demo PlatformWebView --- example/lib/main.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 7904517..c147d7a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -231,6 +231,7 @@ class _MyAppState extends State { bool _loading = false; bool _useTransientTokenStorage = false; bool _isSsoEnabled = false; + bool _useWebKitWebView = false; bool _isBiometricEnabled = false; bool get _unconfigured { return _authgear.endpoint != _endpointController.text || @@ -387,6 +388,18 @@ class _MyAppState extends State { }); }, )), + Container( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: SwitchWithLabel( + label: "Use WebKit WebView", + value: _useWebKitWebView, + onChanged: (newValue) { + setState(() { + _useWebKitWebView = newValue; + }); + }, + )), Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -739,6 +752,15 @@ class _MyAppState extends State { clientID: clientID, isSsoEnabled: _isSsoEnabled, tokenStorage: _useTransientTokenStorage ? TransientTokenStorage() : null, + webView: _useWebKitWebView + ? PlatformWebView( + options: PlatformWebViewOptions( + ios: PlatformWebViewOptionsIOS( + modalPresentationStyle: ModalPresentationStyle.fullScreen, + ), + ), + ) + : null, sendWechatAuthRequest: _sendWechatAuthRequest, ); _sub?.cancel(); From 4dc30bf432abca56339ebe1e5f92aed768ab464b Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Tue, 6 Feb 2024 20:10:53 +0800 Subject: [PATCH 06/11] Add PlatformWebView for Android --- android/src/main/AndroidManifest.xml | 6 + .../com/authgear/flutter/AuthgearPlugin.kt | 34 ++- .../authgear/flutter/WebKitWebViewActivity.kt | 251 ++++++++++++++++++ .../src/main/res/drawable/ic_arrow_back.xml | 11 + android/src/main/res/drawable/ic_close.xml | 10 + lib/flutter_authgear.dart | 1 + lib/src/native.dart | 4 + lib/src/webview.dart | 16 ++ 8 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt create mode 100644 android/src/main/res/drawable/ic_arrow_back.xml create mode 100644 android/src/main/res/drawable/ic_close.xml diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 05e0205..34ab685 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -12,6 +12,12 @@ android:launchMode="singleTask" android:theme="@style/AuthgearTheme" android:configChanges="orientation|screenSize" /> + diff --git a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt index 3e19234..5b5ff7b 100644 --- a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt +++ b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageInfo +import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Handler @@ -32,7 +33,7 @@ import io.flutter.plugin.common.PluginRegistry import org.json.JSONObject import java.security.* import java.security.interfaces.RSAPublicKey -import java.util.UUID +import java.util.* class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginRegistry.ActivityResultListener { @@ -145,6 +146,24 @@ class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginReg val intent = OAuthCoordinatorActivity.createAuthorizationIntent(activity, url) activity.startActivityForResult(intent, requestCode) } + "openAuthorizeURLWithWebView" -> { + val url = Uri.parse(call.argument("url")) + val redirectURI = Uri.parse(call.argument("redirectURI")) + val actionBarBackgroundColor = this.readColorInt(call, "actionBarBackgroundColor") + val actionBarButtonTintColor = this.readColorInt(call, "actionBarButtonTintColor") + val options = WebKitWebViewActivity.Options(url, redirectURI) + options.actionBarBackgroundColor = actionBarBackgroundColor + options.actionBarButtonTintColor = actionBarButtonTintColor + + val requestCode = startActivityHandles.push(StartActivityHandle(TAG_AUTHENTICATION, result)) + val activity = activityBinding?.activity + if (activity == null) { + result.noActivity() + return + } + val intent = WebKitWebViewActivity.createIntent(activity, options) + activity.startActivityForResult(intent, requestCode) + } "openURL" -> { val url = Uri.parse(call.argument("url")) val requestCode = startActivityHandles.push(StartActivityHandle(TAG_OPEN_URL, result)) @@ -784,6 +803,19 @@ class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginReg val sig = signature.sign() return "$strToSign.${sig.base64URLEncode()}" } + + private fun readColorInt(call: MethodCall, key: String): Int? { + val s: String? = call.argument(key) + if (s != null) { + val l = s.toLong(16) + val a = (l shr 24 and 0xff).toInt() + val r = (l shr 16 and 0xff).toInt() + val g = (l shr 8 and 0xff).toInt() + val b = (l and 0xff).toInt() + return Color.argb(a, r, g, b) + } + return null + } } internal fun Result.noActivity() { diff --git a/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt b/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt new file mode 100644 index 0000000..f93b4a8 --- /dev/null +++ b/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt @@ -0,0 +1,251 @@ +package com.authgear.flutter + +import android.annotation.TargetApi +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.webkit.* +import androidx.annotation.* +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.DrawableCompat + +class WebKitWebViewActivity: AppCompatActivity() { + companion object { + private const val MENU_ID_CANCEL = 1 + private const val KEY_OPTIONS = "KEY_OPTIONS" + private const val TAG_FILE_CHOOSER = 1 + + fun createIntent(ctx: Context, options: Options): Intent { + val intent = Intent(ctx, WebKitWebViewActivity::class.java) + intent.putExtra(KEY_OPTIONS, options.toBundle()) + return intent + } + } + + private lateinit var mWebView: WebView + private var result: Uri? = null + private val handles = StartActivityHandles>>() + + class Options { + var url: Uri + var redirectURI: Uri + var actionBarBackgroundColor: Int? = null + var actionBarButtonTintColor: Int? = null + + constructor(url: Uri, redirectURI: Uri) { + this.url = url + this.redirectURI = redirectURI + } + + internal constructor(bundle: Bundle) { + this.url = bundle.getParcelable("url")!! + this.redirectURI = bundle.getParcelable("redirectURI")!! + if (bundle.containsKey("actionBarBackgroundColor")) { + this.actionBarBackgroundColor = bundle.getInt("actionBarBackgroundColor") + } + if (bundle.containsKey("actionBarButtonTintColor")) { + this.actionBarButtonTintColor = bundle.getInt("actionBarButtonTintColor") + } + } + + fun toBundle(): Bundle { + val bundle = Bundle() + bundle.putParcelable("url", this.url) + bundle.putParcelable("redirectURI", this.redirectURI) + this.actionBarBackgroundColor?.let { + bundle.putInt("actionBarBackgroundColor", it) + } + this.actionBarButtonTintColor?.let { + bundle.putInt("actionBarButtonTintColor", it) + } + return bundle + } + } + + private class MyWebViewClient constructor(private val activity: WebKitWebViewActivity) : + WebViewClient() { + + @TargetApi(Build.VERSION_CODES.N) + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val uri = request?.url!! + if (this.shouldOverrideUrlLoading(uri)) { + return true + } + return super.shouldOverrideUrlLoading(view, request) + } + + @SuppressWarnings("deprecation") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + val uri = Uri.parse(url!!)!! + if (this.shouldOverrideUrlLoading(uri)) { + return true + } + return super.shouldOverrideUrlLoading(view, url) + } + + private fun shouldOverrideUrlLoading(uri: Uri): Boolean { + if (this.checkRedirectURI(uri)) { + return true; + } + return false; + } + + private fun checkRedirectURI(uri: Uri): Boolean { + val redirectURI = this.activity.getOptions().redirectURI + val withoutQuery = this.removeQueryAndFragment(uri) + if (withoutQuery.toString() == redirectURI.toString()) { + this.activity.result = uri + this.activity.callSetResult() + this.activity.finish() + return true + } + return false; + } + + private fun removeQueryAndFragment(uri: Uri): Uri { + return uri.buildUpon().query(null).fragment(null).build() + } + } + + private class MyWebChromeClient constructor(private val activity: WebKitWebViewActivity) : + WebChromeClient() { + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>?, + fileChooserParams: FileChooserParams? + ): Boolean { + val handle = StartActivityHandle(TAG_FILE_CHOOSER, filePathCallback!!) + val requestCode = this.activity.handles.push(handle) + val intent = fileChooserParams!!.createIntent() + this.activity.startActivityForResult(intent, requestCode) + return true + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val options = this.getOptions() + + // Do not show title. + supportActionBar?.setDisplayShowTitleEnabled(false) + + // Configure navigation bar background color. + options.actionBarBackgroundColor?.let { + supportActionBar?.setBackgroundDrawable(ColorDrawable(it)) + } + + // Show back button. + supportActionBar?.displayOptions = ActionBar.DISPLAY_SHOW_HOME or ActionBar.DISPLAY_HOME_AS_UP + + // Configure the back button. + var backButtonDrawable = getDrawableCompat(R.drawable.ic_arrow_back) + if (options.actionBarButtonTintColor != null) { + backButtonDrawable = + tintDrawable(backButtonDrawable, options.actionBarButtonTintColor!!) + } + supportActionBar?.setHomeAsUpIndicator(backButtonDrawable) + + // Configure web view. + this.mWebView = WebView(this) + this.setContentView(this.mWebView) + this.mWebView.setWebViewClient(MyWebViewClient(this)) + this.mWebView.setWebChromeClient(MyWebChromeClient(this)) + val webSettings: WebSettings = this.mWebView.getSettings() + webSettings.javaScriptEnabled = true + + this.mWebView.loadUrl(options.url.toString()) + } + + override fun onBackPressed() { + if (mWebView.canGoBack()) { + mWebView.goBack() + } else { + callSetResult() + super.onBackPressed() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val options = getOptions() + + // Configure the close button. + var drawable = getDrawableCompat(R.drawable.ic_close) + if (options.actionBarButtonTintColor != null) { + drawable = tintDrawable(drawable, options.actionBarButtonTintColor!!) + } + menu.add(Menu.NONE, MENU_ID_CANCEL, Menu.NONE, android.R.string.cancel) + .setIcon(drawable) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(@NonNull item: MenuItem): Boolean { + if (item.getItemId() === android.R.id.home) { + onBackPressed() + return true + } + if (item.getItemId() === MENU_ID_CANCEL) { + callSetResult() + finish() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + val handle = handles.pop(requestCode) + ?: return + when (handle.tag) { + TAG_FILE_CHOOSER -> when (resultCode) { + Activity.RESULT_CANCELED -> handle.value.onReceiveValue(null) + Activity.RESULT_OK -> if (data != null && data.data != null) { + handle.value.onReceiveValue(arrayOf(data.data!!)) + } else { + handle.value.onReceiveValue(null) + } + } + } + } + + private fun getOptions(): Options { + val bundle: Bundle = this.intent.getParcelableExtra(KEY_OPTIONS)!! + return Options(bundle) + } + + private fun callSetResult() { + if (this.result == null) { + this.setResult(Activity.RESULT_CANCELED) + } else { + val intent = Intent() + intent.data = this.result + this.setResult(Activity.RESULT_OK, intent) + } + } + + private fun getDrawableCompat(@DrawableRes id: Int): Drawable { + return ResourcesCompat.getDrawable(resources, id, null)!! + } + + private fun tintDrawable(drawable: Drawable, @ColorInt color: Int): Drawable { + val newDrawable: Drawable = + DrawableCompat.wrap(drawable).constantState!!.newDrawable().mutate() + DrawableCompat.setTint(newDrawable, color) + return newDrawable + } +} \ No newline at end of file diff --git a/android/src/main/res/drawable/ic_arrow_back.xml b/android/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..0e2e863 --- /dev/null +++ b/android/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/src/main/res/drawable/ic_close.xml b/android/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..7a0ff35 --- /dev/null +++ b/android/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/lib/flutter_authgear.dart b/lib/flutter_authgear.dart index e742e13..0906b26 100644 --- a/lib/flutter_authgear.dart +++ b/lib/flutter_authgear.dart @@ -51,4 +51,5 @@ export 'src/webview.dart' ModalPresentationStyle, PlatformWebViewOptions, PlatformWebViewOptionsIOS, + PlatformWebViewOptionsAndroid, PlatformWebView; diff --git a/lib/src/native.dart b/lib/src/native.dart index 58ac144..b735474 100644 --- a/lib/src/native.dart +++ b/lib/src/native.dart @@ -55,6 +55,8 @@ Future openAuthorizeURLWithWebView({ String? backgroundColor, String? navigationBarBackgroundColor, String? navigationBarButtonTintColor, + String? actionBarBackgroundColor, + String? actionBarButtonTintColor, }) async { try { return await _channel.invokeMethod("openAuthorizeURLWithWebView", { @@ -64,6 +66,8 @@ Future openAuthorizeURLWithWebView({ "backgroundColor": backgroundColor, "navigationBarBackgroundColor": navigationBarBackgroundColor, "navigationBarButtonTintColor": navigationBarButtonTintColor, + "actionBarBackgroundColor": actionBarBackgroundColor, + "actionBarButtonTintColor": actionBarButtonTintColor, }); } on PlatformException catch (e) { throw wrapException(e); diff --git a/lib/src/webview.dart b/lib/src/webview.dart index 1e8426c..10c2b1c 100644 --- a/lib/src/webview.dart +++ b/lib/src/webview.dart @@ -56,11 +56,23 @@ class PlatformWebViewOptionsIOS { }); } +class PlatformWebViewOptionsAndroid { + final int? actionBarBackgroundColor; + final int? actionBarButtonTintColor; + + PlatformWebViewOptionsAndroid({ + this.actionBarBackgroundColor, + this.actionBarButtonTintColor, + }); +} + class PlatformWebViewOptions { final PlatformWebViewOptionsIOS? ios; + final PlatformWebViewOptionsAndroid? android; PlatformWebViewOptions({ this.ios, + this.android, }); } @@ -86,6 +98,10 @@ class PlatformWebView implements WebView { options?.ios?.navigationBarBackgroundColor?.toRadixString(16), navigationBarButtonTintColor: options?.ios?.navigationBarButtonTintColor?.toRadixString(16), + actionBarBackgroundColor: + options?.android?.actionBarBackgroundColor?.toRadixString(16), + actionBarButtonTintColor: + options?.android?.actionBarButtonTintColor?.toRadixString(16), ); } } From 322d1e2d5763ebb6ee5ea55bfbab852e31cd69ed Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 7 Feb 2024 12:14:51 +0800 Subject: [PATCH 07/11] Rename WebView to UIImplementation --- example/lib/main.dart | 8 +++--- lib/flutter_authgear.dart | 14 +++++----- lib/src/container.dart | 15 ++++++----- .../{webview.dart => ui_implementation.dart} | 26 +++++++++---------- 4 files changed, 32 insertions(+), 31 deletions(-) rename lib/src/{webview.dart => ui_implementation.dart} (78%) diff --git a/example/lib/main.dart b/example/lib/main.dart index c147d7a..82e8f10 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -752,10 +752,10 @@ class _MyAppState extends State { clientID: clientID, isSsoEnabled: _isSsoEnabled, tokenStorage: _useTransientTokenStorage ? TransientTokenStorage() : null, - webView: _useWebKitWebView - ? PlatformWebView( - options: PlatformWebViewOptions( - ios: PlatformWebViewOptionsIOS( + uiImplementation: _useWebKitWebView + ? WebKitWebViewUIImplementation( + options: WebKitWebViewUIImplementationOptions( + ios: WebKitWebViewUIImplementationOptionsIOS( modalPresentationStyle: ModalPresentationStyle.fullScreen, ), ), diff --git a/lib/flutter_authgear.dart b/lib/flutter_authgear.dart index 0906b26..a353961 100644 --- a/lib/flutter_authgear.dart +++ b/lib/flutter_authgear.dart @@ -44,12 +44,12 @@ export 'src/exception.dart' BiometricNoPasscodeException, BiometricNoEnrollmentException, BiometricLockoutException; -export 'src/webview.dart' +export 'src/ui_implementation.dart' show - WebView, - DefaultWebView, + UIImplementation, + DeviceBrowserUIImplementation, ModalPresentationStyle, - PlatformWebViewOptions, - PlatformWebViewOptionsIOS, - PlatformWebViewOptionsAndroid, - PlatformWebView; + WebKitWebViewUIImplementation, + WebKitWebViewUIImplementationOptions, + WebKitWebViewUIImplementationOptionsIOS, + WebKitWebViewUIImplementationOptionsAndroid; diff --git a/lib/src/container.dart b/lib/src/container.dart index 0c6166a..b6eb1cf 100644 --- a/lib/src/container.dart +++ b/lib/src/container.dart @@ -12,7 +12,8 @@ import 'base64.dart'; import 'id_token.dart'; import 'experimental.dart'; import 'native.dart' as native; -import 'webview.dart' show WebView, DefaultWebView; +import 'ui_implementation.dart' + show UIImplementation, DeviceBrowserUIImplementation; class SessionStateChangeEvent { final SessionStateChangeReason reason; @@ -140,7 +141,7 @@ class Authgear implements AuthgearHttpClientDelegate { final TokenStorage _tokenStorage; final ContainerStorage _storage; - final WebView _webView; + final UIImplementation _uiImplementation; late final APIClient _apiClient; late final AuthgearExperimental experimental; @@ -190,9 +191,9 @@ class Authgear implements AuthgearHttpClientDelegate { this.isSsoEnabled = false, this.sendWechatAuthRequest, TokenStorage? tokenStorage, - WebView? webView, + UIImplementation? uiImplementation, }) : _tokenStorage = tokenStorage ?? PersistentTokenStorage(), - _webView = webView ?? DefaultWebView(), + _uiImplementation = uiImplementation ?? DeviceBrowserUIImplementation(), _storage = PersistentContainerStorage() { final plainHttpClient = Client(); final authgearHttpClient = AuthgearHttpClient(this, plainHttpClient); @@ -266,7 +267,7 @@ class Authgear implements AuthgearHttpClientDelegate { wechatRedirectURI: wechatRedirectURI); } - final resultURL = await _webView.openAuthorizationURL( + final resultURL = await _uiImplementation.openAuthorizationURL( url: authRequest.url.toString(), redirectURI: authRequest.redirectURI, shareCookiesWithDeviceBrowser: isSsoEnabled, @@ -332,7 +333,7 @@ class Authgear implements AuthgearHttpClientDelegate { wechatRedirectURI: wechatRedirectURI); } - final resultURL = await _webView.openAuthorizationURL( + final resultURL = await _uiImplementation.openAuthorizationURL( url: request.url.toString(), redirectURI: redirectURI, shareCookiesWithDeviceBrowser: isSsoEnabled, @@ -692,7 +693,7 @@ class Authgear implements AuthgearHttpClientDelegate { wechatRedirectURI: wechatRedirectURI); } - final resultURL = await _webView.openAuthorizationURL( + final resultURL = await _uiImplementation.openAuthorizationURL( url: authenticationURL.toString(), redirectURI: redirectURI, shareCookiesWithDeviceBrowser: isSsoEnabled, diff --git a/lib/src/webview.dart b/lib/src/ui_implementation.dart similarity index 78% rename from lib/src/webview.dart rename to lib/src/ui_implementation.dart index 10c2b1c..500638a 100644 --- a/lib/src/webview.dart +++ b/lib/src/ui_implementation.dart @@ -1,13 +1,13 @@ import 'native.dart' as native; -abstract class WebView { +abstract class UIImplementation { Future openAuthorizationURL( {required String url, required String redirectURI, required bool shareCookiesWithDeviceBrowser}); } -class DefaultWebView implements WebView { +class DeviceBrowserUIImplementation implements UIImplementation { @override Future openAuthorizationURL({ required String url, @@ -42,13 +42,13 @@ extension ModalPresentationStyleExtension on ModalPresentationStyle { } } -class PlatformWebViewOptionsIOS { +class WebKitWebViewUIImplementationOptionsIOS { final ModalPresentationStyle? modalPresentationStyle; final int? backgroundColor; final int? navigationBarBackgroundColor; final int? navigationBarButtonTintColor; - PlatformWebViewOptionsIOS({ + WebKitWebViewUIImplementationOptionsIOS({ this.modalPresentationStyle, this.backgroundColor, this.navigationBarBackgroundColor, @@ -56,30 +56,30 @@ class PlatformWebViewOptionsIOS { }); } -class PlatformWebViewOptionsAndroid { +class WebKitWebViewUIImplementationOptionsAndroid { final int? actionBarBackgroundColor; final int? actionBarButtonTintColor; - PlatformWebViewOptionsAndroid({ + WebKitWebViewUIImplementationOptionsAndroid({ this.actionBarBackgroundColor, this.actionBarButtonTintColor, }); } -class PlatformWebViewOptions { - final PlatformWebViewOptionsIOS? ios; - final PlatformWebViewOptionsAndroid? android; +class WebKitWebViewUIImplementationOptions { + final WebKitWebViewUIImplementationOptionsIOS? ios; + final WebKitWebViewUIImplementationOptionsAndroid? android; - PlatformWebViewOptions({ + WebKitWebViewUIImplementationOptions({ this.ios, this.android, }); } -class PlatformWebView implements WebView { - final PlatformWebViewOptions? options; +class WebKitWebViewUIImplementation implements UIImplementation { + final WebKitWebViewUIImplementationOptions? options; - PlatformWebView({ + WebKitWebViewUIImplementation({ this.options, }); From 453f623233589f3976f995129286dc0477a7a4fc Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 7 Feb 2024 17:53:37 +0800 Subject: [PATCH 08/11] Extend the edges of WKWebView to that of the screen --- ios/Classes/AGWKWebViewController.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ios/Classes/AGWKWebViewController.swift b/ios/Classes/AGWKWebViewController.swift index 2e11889..60ff677 100644 --- a/ios/Classes/AGWKWebViewController.swift +++ b/ios/Classes/AGWKWebViewController.swift @@ -68,10 +68,13 @@ class AGWKWebViewController: UIViewController, WKNavigationDelegate { // Configure layout self.view.addSubview(self.webView) if #available(iOS 11.0, *) { - self.webView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true + // Extend the web view to the top edge of the screen. + // WKWebView magically offset the content so that the content is not covered by the navigation bar initially. + self.webView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true self.webView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true self.webView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true - self.webView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true + // Extend the web view to the bottom edge of the screen. + self.webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true } // Configure the bounce behavior From 6d7f38602ac73d7c7d73485b8301696677329872 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Thu, 8 Feb 2024 07:46:51 +0800 Subject: [PATCH 09/11] Remove backgroundColor --- ios/Classes/AGWKWebViewController.swift | 22 ---------------------- ios/Classes/SwiftAuthgearPlugin.swift | 4 ---- lib/src/native.dart | 2 -- lib/src/ui_implementation.dart | 3 --- 4 files changed, 31 deletions(-) diff --git a/ios/Classes/AGWKWebViewController.swift b/ios/Classes/AGWKWebViewController.swift index 60ff677..945e26d 100644 --- a/ios/Classes/AGWKWebViewController.swift +++ b/ios/Classes/AGWKWebViewController.swift @@ -12,7 +12,6 @@ protocol AGWKWebViewControllerPresentationContextProviding: AnyObject { class AGWKWebViewController: UIViewController, WKNavigationDelegate { typealias CompletionHandler = (URL?, Error?) -> Void weak var presentationContextProvider: AGWKWebViewControllerPresentationContextProviding? - var backgroundColor: UIColor? var navigationBarBackgroundColor: UIColor? var navigationBarButtonTintColor: UIColor? @@ -22,20 +21,6 @@ class AGWKWebViewController: UIViewController, WKNavigationDelegate { private let webView: WKWebView private var result: URL? - private var defaultBackgroundColor: UIColor { - get { - if #available(iOS 12.0, *) { - switch (self.traitCollection.userInterfaceStyle) { - case .dark: - return UIColor.black - default: - return UIColor.white - } - } - return UIColor.white - } - } - init(url: URL, redirectURI: URL, completionHandler: @escaping CompletionHandler) { self.url = url self.redirectURI = redirectURI @@ -58,13 +43,6 @@ class AGWKWebViewController: UIViewController, WKNavigationDelegate { override func viewDidLoad() { super.viewDidLoad() - // Configure background color - if let backgroundColor = self.backgroundColor { - self.view.backgroundColor = backgroundColor - } else { - self.view.backgroundColor = self.defaultBackgroundColor - } - // Configure layout self.view.addSubview(self.webView) if #available(iOS 11.0, *) { diff --git a/ios/Classes/SwiftAuthgearPlugin.swift b/ios/Classes/SwiftAuthgearPlugin.swift index fc9a925..ebd3e08 100644 --- a/ios/Classes/SwiftAuthgearPlugin.swift +++ b/ios/Classes/SwiftAuthgearPlugin.swift @@ -43,14 +43,12 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr let url = URL(string: arguments["url"] as! String)! let redirectURI = URL(string: arguments["redirectURI"] as! String)! let modalPresentationStyle = UIModalPresentationStyle.from(string: arguments["modalPresentationStyle"] as? String) - let backgroundColor = UIColor(argb: arguments["backgroundColor"] as? String) let navigationBarBackgroundColor = UIColor(argb: arguments["navigationBarBackgroundColor"] as? String) let navigationBarButtonTintColor = UIColor(argb: arguments["navigationBarButtonTintColor"] as? String) self.openAuthorizeURLWithWebView( url: url, redirectURI: redirectURI, modalPresentationStyle: modalPresentationStyle, - backgroundColor: backgroundColor, navigationBarBackgroundColor: navigationBarBackgroundColor, navigationBarButtonTintColor: navigationBarButtonTintColor, result: result @@ -218,7 +216,6 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr url: URL, redirectURI: URL, modalPresentationStyle: UIModalPresentationStyle, - backgroundColor: UIColor?, navigationBarBackgroundColor: UIColor?, navigationBarButtonTintColor: UIColor?, result: @escaping FlutterResult @@ -243,7 +240,6 @@ public class SwiftAuthgearPlugin: NSObject, FlutterPlugin, ASWebAuthenticationPr result(FlutterError.unreachable) return } - controller.backgroundColor = backgroundColor controller.navigationBarBackgroundColor = navigationBarBackgroundColor controller.navigationBarButtonTintColor = navigationBarButtonTintColor controller.modalPresentationStyle = modalPresentationStyle diff --git a/lib/src/native.dart b/lib/src/native.dart index b735474..a2a6fb0 100644 --- a/lib/src/native.dart +++ b/lib/src/native.dart @@ -52,7 +52,6 @@ Future openAuthorizeURLWithWebView({ required String url, required String redirectURI, String? modalPresentationStyle, - String? backgroundColor, String? navigationBarBackgroundColor, String? navigationBarButtonTintColor, String? actionBarBackgroundColor, @@ -63,7 +62,6 @@ Future openAuthorizeURLWithWebView({ "url": url, "redirectURI": redirectURI, "modalPresentationStyle": modalPresentationStyle, - "backgroundColor": backgroundColor, "navigationBarBackgroundColor": navigationBarBackgroundColor, "navigationBarButtonTintColor": navigationBarButtonTintColor, "actionBarBackgroundColor": actionBarBackgroundColor, diff --git a/lib/src/ui_implementation.dart b/lib/src/ui_implementation.dart index 500638a..657b8ef 100644 --- a/lib/src/ui_implementation.dart +++ b/lib/src/ui_implementation.dart @@ -44,13 +44,11 @@ extension ModalPresentationStyleExtension on ModalPresentationStyle { class WebKitWebViewUIImplementationOptionsIOS { final ModalPresentationStyle? modalPresentationStyle; - final int? backgroundColor; final int? navigationBarBackgroundColor; final int? navigationBarButtonTintColor; WebKitWebViewUIImplementationOptionsIOS({ this.modalPresentationStyle, - this.backgroundColor, this.navigationBarBackgroundColor, this.navigationBarButtonTintColor, }); @@ -93,7 +91,6 @@ class WebKitWebViewUIImplementation implements UIImplementation { url: url, redirectURI: redirectURI, modalPresentationStyle: options?.ios?.modalPresentationStyle?.value, - backgroundColor: options?.ios?.backgroundColor?.toRadixString(16), navigationBarBackgroundColor: options?.ios?.navigationBarBackgroundColor?.toRadixString(16), navigationBarButtonTintColor: From 68007439539b4034a454b0fa15c96d5153e2c5f9 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 21 Feb 2024 20:01:25 +0800 Subject: [PATCH 10/11] Restore Android web view state --- .../authgear/flutter/WebKitWebViewActivity.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt b/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt index f93b4a8..9dddb03 100644 --- a/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt +++ b/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt @@ -22,6 +22,7 @@ class WebKitWebViewActivity: AppCompatActivity() { companion object { private const val MENU_ID_CANCEL = 1 private const val KEY_OPTIONS = "KEY_OPTIONS" + private const val KEY_WEB_VIEW_STATE = "KEY_WEB_VIEW_STATE" private const val TAG_FILE_CHOOSER = 1 fun createIntent(ctx: Context, options: Options): Intent { @@ -168,7 +169,24 @@ class WebKitWebViewActivity: AppCompatActivity() { val webSettings: WebSettings = this.mWebView.getSettings() webSettings.javaScriptEnabled = true - this.mWebView.loadUrl(options.url.toString()) + if (savedInstanceState == null) { + this.mWebView.loadUrl(options.url.toString()) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val webViewBundle = Bundle() + this.mWebView.saveState(webViewBundle) + outState.putBundle(KEY_WEB_VIEW_STATE, webViewBundle) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + val bundle = savedInstanceState.getBundle(KEY_WEB_VIEW_STATE) + if (bundle != null) { + this.mWebView.restoreState(bundle) + } } override fun onBackPressed() { From 80b7f1db833ce2a5e502b0154a20fa5e9de30a2c Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 21 Feb 2024 20:01:40 +0800 Subject: [PATCH 11/11] Allow WebKitWebViewActivity and MainActivity to switch orientation --- android/src/main/AndroidManifest.xml | 3 +-- example/android/app/src/main/AndroidManifest.xml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 34ab685..1474fb1 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -16,8 +16,7 @@ android:name=".WebKitWebViewActivity" android:exported="false" android:launchMode="singleTask" - android:theme="@style/AuthgearTheme" - android:configChanges="orientation|screenSize" /> + android:theme="@style/AuthgearTheme" /> diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a1a6af5..971f1d4 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" - android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">