From 872fd7e8c495b955d03336f27dcc9291dce4a267 Mon Sep 17 00:00:00 2001 From: Ionut Daniel Fagadau Date: Mon, 20 Oct 2025 22:26:55 +0200 Subject: [PATCH 1/5] feat(macos-provider-support): add support --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 8 + .../Swift/AuthProvider/OAuthProvider.swift | 96 +++++++++ .../Utilities/AuthDefaultUIDelegate.swift | 92 ++++++++- .../Swift/Utilities/AuthUIDelegate.swift | 39 ++++ .../Swift/Utilities/AuthURLPresenter.swift | 143 ++++++++++++++ .../Sources/Swift/Utilities/AuthWebView.swift | 63 ++++++ .../Utilities/AuthWebViewController.swift | 182 ++++++++++++++++++ 7 files changed, 621 insertions(+), 2 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index d198f5418f5..29b171f443a 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -1664,6 +1664,9 @@ extension Auth: AuthInterop { settings = AuthSettings() GULAppDelegateSwizzler.proxyOriginalDelegateIncludingAPNSMethods() GULSceneDelegateSwizzler.proxyOriginalSceneDelegate() + #elseif os(macOS) + authURLPresenter = AuthURLPresenter() + settings = AuthSettings() #endif requestConfiguration = AuthRequestConfiguration(apiKey: apiKey, appID: app.options.googleAppID, @@ -2370,6 +2373,11 @@ extension Auth: AuthInterop { /// An object that takes care of presenting URLs via the auth instance. var authURLPresenter: AuthWebViewControllerDelegate + #elseif os(macOS) + + /// An object that takes care of presenting URLs via the auth instance. + var authURLPresenter: AuthWebViewControllerDelegate + #endif // TARGET_OS_IOS // MARK: Private properties diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift index 761f57cf5d1..0db655e14e4 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift @@ -320,6 +320,102 @@ import Foundation } #endif + #if os(macOS) + /// Used to obtain an auth credential via a web flow on macOS. + /// + /// This method is available on macOS only. + /// - Parameter uiDelegate: An optional UI delegate used to present the web flow. + /// - Parameter completion: Optionally; a block which is invoked asynchronously on the main + /// thread when the web flow is completed. + open func getCredentialWith(_ uiDelegate: AuthUIDelegate?, + completion: ((AuthCredential?, Error?) -> Void)? = nil) { + guard let urlTypes = auth.mainBundleUrlTypes, + AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme, + urlTypes: urlTypes) else { + fatalError( + "Please register custom URL scheme \(callbackScheme) in the app's Info.plist file." + ) + } + kAuthGlobalWorkQueue.async { [weak self] in + guard let self = self else { return } + let eventID = AuthWebUtils.randomString(withLength: 10) + let sessionID = AuthWebUtils.randomString(withLength: 10) + + let callbackOnMainThread: ((AuthCredential?, Error?) -> Void) = { credential, error in + if let completion { + DispatchQueue.main.async { + completion(credential, error) + } + } + } + Task { + do { + guard let headfulLiteURL = try await self.getHeadfulLiteUrl(eventID: eventID, + sessionID: sessionID) else { + fatalError( + "FirebaseAuth Internal Error: Both error and headfulLiteURL return are nil" + ) + } + let callbackMatcher: (URL?) -> Bool = { callbackURL in + AuthWebUtils.isExpectedCallbackURL(callbackURL, + eventID: eventID, + authType: "signInWithRedirect", + callbackScheme: self.callbackScheme) + } + self.auth.authURLPresenter.present(headfulLiteURL, + uiDelegate: uiDelegate, + callbackMatcher: callbackMatcher) { callbackURL, error in + if let error { + callbackOnMainThread(nil, error) + return + } + guard let callbackURL else { + fatalError("FirebaseAuth Internal Error: Both error and callbackURL return are nil") + } + let (oAuthResponseURLString, error) = self.oAuthResponseForURL(url: callbackURL) + if let error { + callbackOnMainThread(nil, error) + return + } + guard let oAuthResponseURLString else { + fatalError( + "FirebaseAuth Internal Error: Both error and oAuthResponseURLString return are nil" + ) + } + let credential = OAuthCredential(withProviderID: self.providerID, + sessionID: sessionID, + OAuthResponseURLString: oAuthResponseURLString) + callbackOnMainThread(credential, nil) + } + } catch { + callbackOnMainThread(nil, error) + } + } + } + } + + /// Used to obtain an auth credential via a web flow on macOS. + /// This method is available on macOS only. + /// - Parameter uiDelegate: An optional UI delegate used to present the web flow. + /// - Parameter completionHandler: Optionally; a block which is invoked + /// asynchronously on the main thread when the web flow is + /// completed. + @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) + @objc(getCredentialWithUIDelegate:completion:) + @MainActor + open func credential(with uiDelegate: AuthUIDelegate?) async throws -> AuthCredential { + return try await withCheckedThrowingContinuation { continuation in + getCredentialWith(uiDelegate) { credential, error in + if let credential = credential { + continuation.resume(returning: credential) + } else { + continuation.resume(throwing: error!) // TODO: Change to ?? and generate unknown error + } + } + } + } + #endif + /// Creates an `AuthCredential` for the Sign in with Apple OAuth 2 provider identified by ID /// token, raw nonce, and full name.This method is specific to the Sign in with Apple OAuth 2 /// provider as this provider requires the full name to be passed explicitly. diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift index 4106977a5e1..078aa20ca87 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift @@ -81,10 +81,98 @@ viewController?.present(viewControllerToPresent, animated: flag, completion: completion) } - func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - viewController?.dismiss(animated: flag, completion: completion) + public func dismiss(completion: (() -> Void)?) { + // Store a reference to the window so we can close it after the view controller is dismissed + let window = currentWebWindow + currentWebWindow = nil + + // Close the window + DispatchQueue.main.async { + window?.close() + completion?() } + } private let viewController: UIViewController? } + +#elseif os(macOS) + + import Foundation + import AppKit + #if COCOAPODS + internal import GoogleUtilities + #else + internal import GoogleUtilities_Environment + #endif + + /// Custom window class for OAuth flow + final class AuthWebWindow: NSWindow { + weak var authViewController: NSViewController? + + override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { + super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) + setupWindow() + } + + private func setupWindow() { + title = "Sign In" + isReleasedWhenClosed = false + center() + level = .floating + styleMask = [.titled, .closable, .resizable] + } + + override func performClose(_ sender: Any?) { + // Notify the auth view controller that user canceled + if let authVC = authViewController as? AuthWebViewController { + authVC.handleWindowClose() + } + super.performClose(sender) + } + } + + /// Class responsible for providing a default AuthUIDelegate. + /// + /// This class should be used in the case that a UIDelegate was expected and necessary to + /// continue a given flow, but none was provided. + final class AuthDefaultUIDelegate: NSObject, AuthUIDelegate { + private var authWindow: AuthWebWindow? + + /// Returns a default AuthUIDelegate object. + /// - Returns: The default AuthUIDelegate object. + @MainActor static func defaultUIDelegate() -> AuthUIDelegate? { + if GULAppEnvironmentUtil.isAppExtension() { + // macOS App extensions should not access NSApplication.shared. + return nil + } + + return AuthDefaultUIDelegate() + } + + func present(_ viewControllerToPresent: NSViewController, + completion: (() -> Void)? = nil) { + // Create a new window for the OAuth flow + let windowRect = NSRect(x: 0, y: 0, width: 800, height: 600) + authWindow = AuthWebWindow( + contentRect: windowRect, + styleMask: [.titled, .closable, .resizable], + backing: .buffered, + defer: false + ) + + authWindow?.authViewController = viewControllerToPresent + authWindow?.contentViewController = viewControllerToPresent + authWindow?.makeKeyAndOrderFront(nil) + + completion?() + } + + func dismiss(completion: (() -> Void)? = nil) { + authWindow?.close() + authWindow = nil + completion?() + } + } + #endif diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift index 94025574ab4..aaced936578 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift @@ -53,4 +53,43 @@ return dismiss(animated: flag, completion: nil) } } + +#elseif os(macOS) + + import Foundation + import AppKit + + /// A protocol to handle user interface interactions for Firebase Auth. + /// + /// This protocol is available on macOS only. + @objc(FIRAuthUIDelegate) public protocol AuthUIDelegate: NSObjectProtocol { + /// If implemented, this method will be invoked when Firebase Auth needs to display a view + /// controller. + /// - Parameter viewControllerToPresent: The view controller to be presented. + /// - Parameter completion: The block to execute after the presentation finishes. + /// This block has no return value and takes no parameters. + @objc(presentViewController:completion:) + func present(_ viewControllerToPresent: NSViewController, + completion: (() -> Void)?) + + /// If implemented, this method will be invoked when Firebase Auth needs to dismiss a view + /// controller. + /// - Parameter completion: The block to execute after the dismissal finishes. + /// This block has no return value and takes no parameters. + @objc(dismissViewControllerWithCompletion:) + func dismiss(completion: (() -> Void)?) + } + + // Extension to support default argument variations. + extension AuthUIDelegate { + func present(_ viewControllerToPresent: NSViewController, + completion: (() -> Void)? = nil) { + return present(viewControllerToPresent, completion: nil) + } + + func dismiss(completion: (() -> Void)? = nil) { + return dismiss(completion: nil) + } + } + #endif diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift index c59ced1cfe1..6af4c80401f 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift @@ -186,4 +186,147 @@ } } } + +#elseif os(macOS) + + import Foundation + import AppKit + import WebKit + + /// A Class responsible for presenting URL via WKWebView on macOS. + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + class AuthURLPresenter: NSObject, AuthWebViewControllerDelegate { + /// Presents an URL to interact with user. + /// - Parameter url: The URL to present. + /// - Parameter uiDelegate: The UI delegate to present view controller. + /// - Parameter completion: A block to be called either synchronously if the presentation fails + /// to start, or asynchronously in future on an unspecified thread once the presentation + /// finishes. + func present(_ url: URL, + uiDelegate: AuthUIDelegate?, + callbackMatcher: @escaping (URL?) -> Bool, + completion: @escaping (URL?, Error?) -> Void) { + if isPresenting { + // Unable to start a new presentation on top of another. + // Invoke the new completion closure and leave the old one as-is + // to be invoked when the presentation finishes. + DispatchQueue.main.async { + completion(nil, AuthErrorUtils.webContextCancelledError(message: nil)) + } + return + } + isPresenting = true + self.callbackMatcher = callbackMatcher + self.completion = completion + DispatchQueue.main.async { + self.uiDelegate = uiDelegate ?? AuthDefaultUIDelegate.defaultUIDelegate() + self.webViewController = AuthWebViewController(url: url, delegate: self) + if let webViewController = self.webViewController { + if let fakeUIDelegate = self.fakeUIDelegate { + fakeUIDelegate.present(webViewController) + } else { + self.uiDelegate?.present(webViewController) + } + } + } + } + + /// Determines if a URL was produced by the currently presented URL. + /// - Parameter url: The URL to handle. + /// - Returns: Whether the URL could be handled or not. + func canHandle(url: URL) -> Bool { + if isPresenting, + let callbackMatcher = callbackMatcher, + callbackMatcher(url) { + finishPresentation(withURL: url, error: nil) + return true + } + return false + } + + // MARK: AuthWebViewControllerDelegate + + func webViewControllerDidCancel(_ controller: AuthWebViewController) { + kAuthGlobalWorkQueue.async { + if self.webViewController == controller { + self.finishPresentation(withURL: nil, + error: AuthErrorUtils.webContextCancelledError(message: nil)) + } + } + } + + func webViewController(_ controller: AuthWebViewController, canHandle url: URL) -> Bool { + var result = false + kAuthGlobalWorkQueue.sync { + if self.webViewController == controller { + result = self.canHandle(url: url) + } + } + return result + } + + func webViewController(_ controller: AuthWebViewController, + didFailWithError error: Error) { + kAuthGlobalWorkQueue.async { + if self.webViewController == controller { + self.finishPresentation(withURL: nil, error: error) + } + } + } + + /// Whether or not some web-based content is being presented. + /// + /// Accesses to this property are serialized on the global Auth work queue + /// and thus this variable should not be read or written outside of the work queue. + private var isPresenting: Bool = false + + /// The callback URL matcher for the current presentation, if one is active. + private var callbackMatcher: ((URL) -> Bool)? + + /// The `AuthWebViewController` used for the current presentation, if any. + private var webViewController: AuthWebViewController? + + /// The UIDelegate used to present the view controller. + var uiDelegate: AuthUIDelegate? + + /// The completion handler for the current presentation, if one is active. + /// + /// Accesses to this variable are serialized on the global Auth work queue + /// and thus this variable should not be read or written outside of the work queue. + /// + /// This variable is also used as a flag to indicate a presentation is active. + var completion: ((URL?, Error?) -> Void)? + + /// Test-only option to validate the calls to the uiDelegate. + var fakeUIDelegate: AuthUIDelegate? + + // MARK: Private methods + + private func finishPresentation(withURL url: URL?, error: Error?) { + callbackMatcher = nil + let uiDelegate = self.uiDelegate + self.uiDelegate = nil + let completion = self.completion + self.completion = nil + let webViewController = self.webViewController + self.webViewController = nil + + if webViewController != nil { + DispatchQueue.main.async { + uiDelegate?.dismiss { + self.isPresenting = false + if let completion { + completion(url, error) + } + } + } + } else { + isPresenting = false + if let completion { + completion(url, error) + } + } + } + } + #endif diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift index 8950052112e..a3a6749c4b7 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift @@ -73,4 +73,67 @@ return UIActivityIndicatorView(style: .medium) } } + +#elseif os(macOS) + + import AppKit + import WebKit + + /// A class responsible for creating a WKWebView for use within Firebase Auth. + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + class AuthWebView: NSView { + lazy var webView: WKWebView = createWebView() + lazy var spinner: NSProgressIndicator = createSpinner() + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + wantsLayer = true + layer?.backgroundColor = NSColor.white.cgColor + initializeSubviews() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func initializeSubviews() { + let webView = createWebView() + let spinner = createSpinner() + + // The order of the following controls z-order. + addSubview(webView) + addSubview(spinner) + + layoutSubviews() + self.webView = webView + self.spinner = spinner + } + + override func resizeSubviews(withOldSize oldSize: NSSize) { + super.resizeSubviews(withOldSize: oldSize) + layoutSubviews() + } + + private func layoutSubviews() { + let height = bounds.size.height + let width = bounds.size.width + webView.frame = NSRect(x: 0, y: 0, width: width, height: height) + spinner.frame = NSRect(x: (width - 32) / 2, y: (height - 32) / 2, width: 32, height: 32) + } + + private func createWebView() -> WKWebView { + let webView = WKWebView(frame: .zero) + webView.wantsLayer = true + return webView + } + + private func createSpinner() -> NSProgressIndicator { + let spinner = NSProgressIndicator() + spinner.style = .spinning + spinner.isIndeterminate = true + return spinner + } + } + #endif diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift index ee954b0029f..62fd2e0d36b 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift @@ -129,4 +129,186 @@ delegate?.webViewController(self, didFailWithError: error) } } + +#elseif os(macOS) + + import Foundation + import AppKit + import WebKit + + /// Defines a delegate for AuthWebViewController + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + protocol AuthWebViewControllerDelegate: AnyObject { + /// Notifies the delegate that the web view controller is being cancelled by the user. + /// - Parameter webViewController: The web view controller in question. + func webViewControllerDidCancel(_ controller: AuthWebViewController) + + /// Determines if a URL should be handled by the delegate. + /// - Parameter url: The URL to handle. + /// - Returns: Whether the URL could be handled or not. + func webViewController(_ controller: AuthWebViewController, canHandle url: URL) -> Bool + + /// Notifies the delegate that the web view controller failed to load a page. + /// - Parameter webViewController: The web view controller in question. + /// - Parameter error: The error that has occurred. + func webViewController(_ controller: AuthWebViewController, didFailWithError error: Error) + + /// Presents an URL to interact with user. + /// - Parameter url: The URL to present. + /// - Parameter uiDelegate: The UI delegate to present view controller. + /// - Parameter completion: A block to be called either synchronously if the presentation fails + /// to start, or asynchronously in future on an unspecified thread once the presentation + /// finishes. + func present(_ url: URL, + uiDelegate: AuthUIDelegate?, + callbackMatcher: @escaping (URL?) -> Bool, + completion: @escaping (URL?, Error?) -> Void) + } + + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + class AuthWebViewController: NSViewController, + WKNavigationDelegate { + // MARK: - Properties + + private var url: URL + weak var delegate: AuthWebViewControllerDelegate? + private weak var webView: AuthWebView? + + // MARK: - Initialization + + init(url: URL, delegate: AuthWebViewControllerDelegate) { + self.url = url + self.delegate = delegate + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Lifecycle + + override func loadView() { + let webView = AuthWebView(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) + webView.webView.navigationDelegate = self + view = webView + self.webView = webView + } + + override func viewDidLoad() { + super.viewDidLoad() + title = "Sign In" + + // Add cancel button to toolbar or navigation if available + setupCancelButton() + } + + override func viewDidAppear() { + super.viewDidAppear() + webView?.webView.load(URLRequest(url: url)) + + // Setup toolbar after view appears and window is available + DispatchQueue.main.async { + self.setupToolbar() + } + } + + // MARK: - Public Methods + + func handleWindowClose() { + // Called when user closes the window manually + delegate?.webViewControllerDidCancel(self) + } + + // MARK: - Private Methods + + private func setupCancelButton() { + // This method is kept for compatibility but toolbar setup is moved to setupToolbar() + } + + private func setupToolbar() { + guard let window = view.window else { return } + + let toolbar = NSToolbar(identifier: "AuthWebViewToolbar") + toolbar.delegate = self + toolbar.allowsUserCustomization = false + toolbar.autosavesConfiguration = false + toolbar.displayMode = .iconOnly + window.toolbar = toolbar + } + + // MARK: - Actions + + @objc private func cancel() { + delegate?.webViewControllerDidCancel(self) + } + + // MARK: - WKNavigationDelegate + + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction) async + -> WKNavigationActionPolicy { + _ = delegate?.webViewController( + self, + canHandle: navigationAction.request.url ?? url + ) + return .allow + } + + func webView(_ webView: WKWebView, + didStartProvisionalNavigation navigation: WKNavigation!) { + self.webView?.spinner.isHidden = false + self.webView?.spinner.startAnimation(nil) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.webView?.spinner.isHidden = true + self.webView?.spinner.stopAnimation(nil) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, + withError error: Error) { + if (error as NSError).domain == NSURLErrorDomain, + (error as NSError).code == NSURLErrorCancelled { + // It's okay for the page to be redirected before it is completely loaded. See b/32028062 . + return + } + // Forward notification to our delegate. + self.webView(webView, didFinish: navigation) + delegate?.webViewController(self, didFailWithError: error) + } + } + + // MARK: - NSToolbarDelegate + + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + extension AuthWebViewController: NSToolbarDelegate { + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + if itemIdentifier.rawValue == "cancel" { + let item = NSToolbarItem(itemIdentifier: itemIdentifier) + item.label = "Cancel" + item.paletteLabel = "Cancel" + item.target = self + item.action = #selector(cancel) + if #available(macOS 11.0, *) { + item.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Cancel") + } else { + // For macOS 10.15, use text-based button + item.label = "Cancel" + } + return item + } + return nil + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.flexibleSpace, NSToolbarItem.Identifier("cancel")] + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return toolbarDefaultItemIdentifiers(toolbar) + } + } + #endif From 1337baa9c74142b4a9811187c8256f441f869894 Mon Sep 17 00:00:00 2001 From: Ionut Daniel Fagadau Date: Mon, 20 Oct 2025 22:57:57 +0200 Subject: [PATCH 2/5] feat(macos-provider-support): remove ugly button --- .../Utilities/AuthDefaultUIDelegate.swift | 4 +- .../Sources/Swift/Utilities/AuthWebView.swift | 2 + .../Utilities/AuthWebViewController.swift | 64 +------------------ 3 files changed, 5 insertions(+), 65 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift index 078aa20ca87..6e6a3bbfe30 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift @@ -120,7 +120,7 @@ isReleasedWhenClosed = false center() level = .floating - styleMask = [.titled, .closable, .resizable] + styleMask = [.titled, .closable, .miniaturizable, .resizable] } override func performClose(_ sender: Any?) { @@ -156,7 +156,7 @@ let windowRect = NSRect(x: 0, y: 0, width: 800, height: 600) authWindow = AuthWebWindow( contentRect: windowRect, - styleMask: [.titled, .closable, .resizable], + styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false ) diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift index a3a6749c4b7..5c865b98e86 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift @@ -122,6 +122,8 @@ spinner.frame = NSRect(x: (width - 32) / 2, y: (height - 32) / 2, width: 32, height: 32) } + + private func createWebView() -> WKWebView { let webView = WKWebView(frame: .zero) webView.wantsLayer = true diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift index 62fd2e0d36b..68c76140c7a 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift @@ -199,48 +199,17 @@ override func viewDidLoad() { super.viewDidLoad() title = "Sign In" - - // Add cancel button to toolbar or navigation if available - setupCancelButton() } override func viewDidAppear() { super.viewDidAppear() webView?.webView.load(URLRequest(url: url)) - - // Setup toolbar after view appears and window is available - DispatchQueue.main.async { - self.setupToolbar() - } } // MARK: - Public Methods func handleWindowClose() { - // Called when user closes the window manually - delegate?.webViewControllerDidCancel(self) - } - - // MARK: - Private Methods - - private func setupCancelButton() { - // This method is kept for compatibility but toolbar setup is moved to setupToolbar() - } - - private func setupToolbar() { - guard let window = view.window else { return } - - let toolbar = NSToolbar(identifier: "AuthWebViewToolbar") - toolbar.delegate = self - toolbar.allowsUserCustomization = false - toolbar.autosavesConfiguration = false - toolbar.displayMode = .iconOnly - window.toolbar = toolbar - } - - // MARK: - Actions - - @objc private func cancel() { + // Called when user closes the window manually using standard macOS controls delegate?.webViewControllerDidCancel(self) } @@ -280,35 +249,4 @@ } } - // MARK: - NSToolbarDelegate - - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - extension AuthWebViewController: NSToolbarDelegate { - func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { - if itemIdentifier.rawValue == "cancel" { - let item = NSToolbarItem(itemIdentifier: itemIdentifier) - item.label = "Cancel" - item.paletteLabel = "Cancel" - item.target = self - item.action = #selector(cancel) - if #available(macOS 11.0, *) { - item.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Cancel") - } else { - // For macOS 10.15, use text-based button - item.label = "Cancel" - } - return item - } - return nil - } - - func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.flexibleSpace, NSToolbarItem.Identifier("cancel")] - } - - func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return toolbarDefaultItemIdentifiers(toolbar) - } - } - #endif From ec4bb24664da1c64ad950bdb8e1c5568d8d2412a Mon Sep 17 00:00:00 2001 From: Ionut Daniel Fagadau Date: Tue, 21 Oct 2025 09:03:35 +0200 Subject: [PATCH 3/5] feat(macos-provider-support): restore iOS logic mistakenly modified --- .../Swift/Utilities/AuthDefaultUIDelegate.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift index 6e6a3bbfe30..81ad5b9567c 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift @@ -81,17 +81,9 @@ viewController?.present(viewControllerToPresent, animated: flag, completion: completion) } - public func dismiss(completion: (() -> Void)?) { - // Store a reference to the window so we can close it after the view controller is dismissed - let window = currentWebWindow - currentWebWindow = nil - - // Close the window - DispatchQueue.main.async { - window?.close() - completion?() + func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + viewController?.dismiss(animated: flag, completion: completion) } - } private let viewController: UIViewController? } From 12b43724fd19be723ef1dc064089f1a7e6378290 Mon Sep 17 00:00:00 2001 From: Ionut Daniel Fagadau Date: Wed, 22 Oct 2025 23:08:23 +0200 Subject: [PATCH 4/5] feat(macos-provider-support): fix duplicate webview creation & correctly handle close --- .../Sources/Swift/Utilities/AuthDefaultUIDelegate.swift | 8 ++++---- FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift | 5 ----- .../Sources/Swift/Utilities/AuthWebViewController.swift | 7 +------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift index 81ad5b9567c..3e51398e7ea 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift @@ -114,13 +114,13 @@ level = .floating styleMask = [.titled, .closable, .miniaturizable, .resizable] } - - override func performClose(_ sender: Any?) { - // Notify the auth view controller that user canceled + + override func close() { if let authVC = authViewController as? AuthWebViewController { authVC.handleWindowClose() } - super.performClose(sender) + + super.close() } } diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift index 5c865b98e86..416f8450fcc 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift @@ -98,9 +98,6 @@ } private func initializeSubviews() { - let webView = createWebView() - let spinner = createSpinner() - // The order of the following controls z-order. addSubview(webView) addSubview(spinner) @@ -122,8 +119,6 @@ spinner.frame = NSRect(x: (width - 32) / 2, y: (height - 32) / 2, width: 32, height: 32) } - - private func createWebView() -> WKWebView { let webView = WKWebView(frame: .zero) webView.wantsLayer = true diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift index 68c76140c7a..f504e9b2897 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift @@ -196,13 +196,8 @@ self.webView = webView } - override func viewDidLoad() { - super.viewDidLoad() - title = "Sign In" - } - override func viewDidAppear() { - super.viewDidAppear() + super.viewDidAppear() webView?.webView.load(URLRequest(url: url)) } From c96411c9846ef2c6a9064afa7d13d13502c96ddb Mon Sep 17 00:00:00 2001 From: Ionut Daniel Fagadau Date: Wed, 22 Oct 2025 23:15:52 +0200 Subject: [PATCH 5/5] feat(macos-provider-support): format --- .../Utilities/AuthDefaultUIDelegate.swift | 21 ++++++++++--------- .../Swift/Utilities/AuthUIDelegate.swift | 2 +- .../Swift/Utilities/AuthURLPresenter.swift | 4 ++-- .../Sources/Swift/Utilities/AuthWebView.swift | 4 ++-- .../Utilities/AuthWebViewController.swift | 6 +++--- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift index 3e51398e7ea..aa6420ccdab 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift @@ -90,8 +90,8 @@ #elseif os(macOS) - import Foundation import AppKit + import Foundation #if COCOAPODS internal import GoogleUtilities #else @@ -101,12 +101,13 @@ /// Custom window class for OAuth flow final class AuthWebWindow: NSWindow { weak var authViewController: NSViewController? - - override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { + + override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, + backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) setupWindow() } - + private func setupWindow() { title = "Sign In" isReleasedWhenClosed = false @@ -114,12 +115,12 @@ level = .floating styleMask = [.titled, .closable, .miniaturizable, .resizable] } - + override func close() { if let authVC = authViewController as? AuthWebViewController { authVC.handleWindowClose() } - + super.close() } } @@ -130,7 +131,7 @@ /// continue a given flow, but none was provided. final class AuthDefaultUIDelegate: NSObject, AuthUIDelegate { private var authWindow: AuthWebWindow? - + /// Returns a default AuthUIDelegate object. /// - Returns: The default AuthUIDelegate object. @MainActor static func defaultUIDelegate() -> AuthUIDelegate? { @@ -138,7 +139,7 @@ // macOS App extensions should not access NSApplication.shared. return nil } - + return AuthDefaultUIDelegate() } @@ -152,11 +153,11 @@ backing: .buffered, defer: false ) - + authWindow?.authViewController = viewControllerToPresent authWindow?.contentViewController = viewControllerToPresent authWindow?.makeKeyAndOrderFront(nil) - + completion?() } diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift index aaced936578..305d39f72c4 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift @@ -56,8 +56,8 @@ #elseif os(macOS) - import Foundation import AppKit + import Foundation /// A protocol to handle user interface interactions for Firebase Auth. /// diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift index 6af4c80401f..d44dde01bf5 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift @@ -189,8 +189,8 @@ #elseif os(macOS) - import Foundation import AppKit + import Foundation import WebKit /// A Class responsible for presenting URL via WKWebView on macOS. @@ -310,7 +310,7 @@ self.completion = nil let webViewController = self.webViewController self.webViewController = nil - + if webViewController != nil { DispatchQueue.main.async { uiDelegate?.dismiss { diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift index 416f8450fcc..b48a3b97ad2 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift @@ -103,8 +103,8 @@ addSubview(spinner) layoutSubviews() - self.webView = webView - self.spinner = spinner + webView = webView + spinner = spinner } override func resizeSubviews(withOldSize oldSize: NSSize) { diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift index f504e9b2897..82e6632d9f8 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift @@ -132,8 +132,8 @@ #elseif os(macOS) - import Foundation import AppKit + import Foundation import WebKit /// Defines a delegate for AuthWebViewController @@ -197,12 +197,12 @@ } override func viewDidAppear() { - super.viewDidAppear() + super.viewDidAppear() webView?.webView.load(URLRequest(url: url)) } // MARK: - Public Methods - + func handleWindowClose() { // Called when user closes the window manually using standard macOS controls delegate?.webViewControllerDidCancel(self)