Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
81 changes: 81 additions & 0 deletions FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,85 @@

private let viewController: UIViewController?
}

#elseif os(macOS)

import AppKit
import Foundation
#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, .miniaturizable, .resizable]
}

override func close() {
if let authVC = authViewController as? AuthWebViewController {
authVC.handleWindowClose()
}

super.close()
}
}

/// 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, .miniaturizable, .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
39 changes: 39 additions & 0 deletions FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,43 @@
return dismiss(animated: flag, completion: nil)
}
}

#elseif os(macOS)

import AppKit
import Foundation

/// 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
Loading
Loading