diff --git a/Sources/RxAuthSwift/OAuthManager.swift b/Sources/RxAuthSwift/OAuthManager.swift index 76a5c59..62c3bff 100644 --- a/Sources/RxAuthSwift/OAuthManager.swift +++ b/Sources/RxAuthSwift/OAuthManager.swift @@ -470,8 +470,8 @@ public final class OAuthManager: Sendable { let allowedCredentialIDs = options.allowedCredentialIDs.compactMap(Base64URL.decode) - #if os(macOS) - let assertion = try await MacOSPasskeyAuthenticator().authenticate( + #if os(macOS) || os(iOS) + let assertion = try await PlatformPasskeyAuthenticator().authenticate( relyingPartyIdentifier: relyingPartyIdentifier, challenge: challenge, allowedCredentialIDs: allowedCredentialIDs @@ -544,8 +544,8 @@ public final class OAuthManager: Sendable { throw OAuthError.authenticationFailed("Invalid passkey registration user ID") } - #if os(macOS) - let registration = try await MacOSPasskeyRegistrationAuthenticator().register( + #if os(macOS) || os(iOS) + let registration = try await PlatformPasskeyRegistrationAuthenticator().register( relyingPartyIdentifier: relyingPartyIdentifier, challenge: challenge, name: options.username ?? trimmedUsername, @@ -621,8 +621,8 @@ public final class OAuthManager: Sendable { throw OAuthError.passkeyUnavailable } - #if os(macOS) - let creation = try await MacOSPasskeyAccountCreationAuthenticator().createAccount( + #if os(macOS) || os(iOS) + let creation = try await PlatformPasskeyAccountCreationAuthenticator().createAccount( relyingPartyIdentifier: relyingPartyIdentifier, challenge: challenge, userID: userID, @@ -672,7 +672,7 @@ public final class OAuthManager: Sendable { /// the system passkey UI (`MacOSPasskeyRegistrationAuthenticator`) and /// propagates errors so the UI can react. private func performInteractivePasskeyUpgrade() async throws { - #if os(macOS) + #if os(macOS) || os(iOS) guard supportsPasskeyUpgrade, let challengeURL = configuration.passkeyUpgradeChallengeURL, let verificationURL = configuration.passkeyUpgradeVerificationURL @@ -713,7 +713,7 @@ public final class OAuthManager: Sendable { let displayName = options.username ?? currentUser?.email ?? currentUser?.name ?? "Account" - let registration = try await MacOSPasskeyRegistrationAuthenticator().register( + let registration = try await PlatformPasskeyRegistrationAuthenticator().register( relyingPartyIdentifier: relyingPartyIdentifier, challenge: challenge, name: displayName, @@ -757,7 +757,7 @@ public final class OAuthManager: Sendable { } private func attemptAutomaticPasskeyUpgrade() async { - #if os(macOS) + #if os(macOS) || os(iOS) guard supportsPasskeyUpgrade, let challengeURL = configuration.passkeyUpgradeChallengeURL, let verificationURL = configuration.passkeyUpgradeVerificationURL @@ -803,7 +803,7 @@ public final class OAuthManager: Sendable { let displayName = options.username ?? currentUser?.email ?? currentUser?.name ?? "Account" - guard let registration = await MacOSPasskeyConditionalUpgradeAuthenticator().upgrade( + guard let registration = await PlatformPasskeyConditionalUpgradeAuthenticator().upgrade( relyingPartyIdentifier: relyingPartyIdentifier, challenge: challenge, name: displayName, @@ -1348,7 +1348,7 @@ private struct PasskeyAccountCreationVerifyRequest: Encodable { } } -#if os(macOS) +#if os(macOS) || os(iOS) private extension PasskeyAccountCreationVerifyRequest { init( clientID: String, @@ -1393,7 +1393,7 @@ private struct PasskeyUpgradeVerificationRequest: Encodable { } } -#if os(macOS) +#if os(macOS) || os(iOS) private extension PasskeyUpgradeVerificationRequest { init(sessionID: String?, name: String?, registration: PasskeyRegistration) { let credentialID = Base64URL.encode(registration.credentialID) diff --git a/Sources/RxAuthSwift/Platform/MacOSPasskeyAuthenticator.swift b/Sources/RxAuthSwift/Platform/PasskeyAuthenticator.swift similarity index 86% rename from Sources/RxAuthSwift/Platform/MacOSPasskeyAuthenticator.swift rename to Sources/RxAuthSwift/Platform/PasskeyAuthenticator.swift index 50c8292..e22459e 100644 --- a/Sources/RxAuthSwift/Platform/MacOSPasskeyAuthenticator.swift +++ b/Sources/RxAuthSwift/Platform/PasskeyAuthenticator.swift @@ -1,12 +1,32 @@ -#if canImport(AuthenticationServices) && os(macOS) -import AppKit +#if canImport(AuthenticationServices) && (os(macOS) || os(iOS)) import AuthenticationServices import Foundation +#if os(macOS) +import AppKit +#elseif os(iOS) +import UIKit +#endif + +/// Resolves the platform presentation anchor for an `ASAuthorizationController`. +/// macOS uses the key/main window; iOS uses the foreground window scene's window. +@MainActor +private func resolvePresentationAnchor() -> ASPresentationAnchor { + #if os(macOS) + return NSApplication.shared.keyWindow + ?? NSApplication.shared.mainWindow + ?? ASPresentationAnchor() + #else + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + let scene = scenes.first { $0.activationState == .foregroundActive } ?? scenes.first + return scene?.keyWindow ?? scene?.windows.first ?? ASPresentationAnchor() + #endif +} + @MainActor -final class MacOSPasskeyAuthenticator: NSObject { +final class PlatformPasskeyAuthenticator: NSObject { private var continuation: CheckedContinuation? - private var retainedSelf: MacOSPasskeyAuthenticator? + private var retainedSelf: PlatformPasskeyAuthenticator? func authenticate( relyingPartyIdentifier: String, @@ -46,7 +66,7 @@ final class MacOSPasskeyAuthenticator: NSObject { } } -extension MacOSPasskeyAuthenticator: ASAuthorizationControllerDelegate { +extension PlatformPasskeyAuthenticator: ASAuthorizationControllerDelegate { func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization @@ -79,11 +99,9 @@ extension MacOSPasskeyAuthenticator: ASAuthorizationControllerDelegate { } } -extension MacOSPasskeyAuthenticator: ASAuthorizationControllerPresentationContextProviding { +extension PlatformPasskeyAuthenticator: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - NSApplication.shared.keyWindow - ?? NSApplication.shared.mainWindow - ?? ASPresentationAnchor() + resolvePresentationAnchor() } } @@ -96,9 +114,9 @@ struct PasskeyAssertion { } @MainActor -final class MacOSPasskeyRegistrationAuthenticator: NSObject { +final class PlatformPasskeyRegistrationAuthenticator: NSObject { private var continuation: CheckedContinuation? - private var retainedSelf: MacOSPasskeyRegistrationAuthenticator? + private var retainedSelf: PlatformPasskeyRegistrationAuthenticator? func register( relyingPartyIdentifier: String, @@ -140,7 +158,7 @@ final class MacOSPasskeyRegistrationAuthenticator: NSObject { } } -extension MacOSPasskeyRegistrationAuthenticator: ASAuthorizationControllerDelegate { +extension PlatformPasskeyRegistrationAuthenticator: ASAuthorizationControllerDelegate { func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization @@ -171,11 +189,9 @@ extension MacOSPasskeyRegistrationAuthenticator: ASAuthorizationControllerDelega } } -extension MacOSPasskeyRegistrationAuthenticator: ASAuthorizationControllerPresentationContextProviding { +extension PlatformPasskeyRegistrationAuthenticator: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - NSApplication.shared.keyWindow - ?? NSApplication.shared.mainWindow - ?? ASPresentationAnchor() + resolvePresentationAnchor() } } @@ -192,9 +208,9 @@ struct PasskeyRegistration { /// candidates, user denial, biometrics off, etc.) resolves to `nil` so the /// caller can treat it as a no-op without disturbing the sign-up flow. @MainActor -final class MacOSPasskeyConditionalUpgradeAuthenticator: NSObject { +final class PlatformPasskeyConditionalUpgradeAuthenticator: NSObject { private var continuation: CheckedContinuation? - private var retainedSelf: MacOSPasskeyConditionalUpgradeAuthenticator? + private var retainedSelf: PlatformPasskeyConditionalUpgradeAuthenticator? func upgrade( relyingPartyIdentifier: String, @@ -231,7 +247,7 @@ final class MacOSPasskeyConditionalUpgradeAuthenticator: NSObject { } } -extension MacOSPasskeyConditionalUpgradeAuthenticator: ASAuthorizationControllerDelegate { +extension PlatformPasskeyConditionalUpgradeAuthenticator: ASAuthorizationControllerDelegate { func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization @@ -255,11 +271,9 @@ extension MacOSPasskeyConditionalUpgradeAuthenticator: ASAuthorizationController } } -extension MacOSPasskeyConditionalUpgradeAuthenticator: ASAuthorizationControllerPresentationContextProviding { +extension PlatformPasskeyConditionalUpgradeAuthenticator: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - NSApplication.shared.keyWindow - ?? NSApplication.shared.mainWindow - ?? ASPresentationAnchor() + resolvePresentationAnchor() } } @@ -297,9 +311,9 @@ struct PasskeyAccountCreation { /// iCloud, the user confirms with biometrics, and the system generates the /// passkey. The caller never collects a username — it arrives back here. @MainActor -final class MacOSPasskeyAccountCreationAuthenticator: NSObject { +final class PlatformPasskeyAccountCreationAuthenticator: NSObject { private var continuation: CheckedContinuation? - private var retainedSelf: MacOSPasskeyAccountCreationAuthenticator? + private var retainedSelf: PlatformPasskeyAccountCreationAuthenticator? func createAccount( relyingPartyIdentifier: String, @@ -339,7 +353,7 @@ final class MacOSPasskeyAccountCreationAuthenticator: NSObject { } } -extension MacOSPasskeyAccountCreationAuthenticator: ASAuthorizationControllerDelegate { +extension PlatformPasskeyAccountCreationAuthenticator: ASAuthorizationControllerDelegate { func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization @@ -384,11 +398,9 @@ extension MacOSPasskeyAccountCreationAuthenticator: ASAuthorizationControllerDel } } -extension MacOSPasskeyAccountCreationAuthenticator: ASAuthorizationControllerPresentationContextProviding { +extension PlatformPasskeyAccountCreationAuthenticator: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - NSApplication.shared.keyWindow - ?? NSApplication.shared.mainWindow - ?? ASPresentationAnchor() + resolvePresentationAnchor() } } #endif diff --git a/Sources/RxAuthSwiftUI/RxSignInView.swift b/Sources/RxAuthSwiftUI/RxSignInView.swift index 9f63047..cca6fbe 100644 --- a/Sources/RxAuthSwiftUI/RxSignInView.swift +++ b/Sources/RxAuthSwiftUI/RxSignInView.swift @@ -413,22 +413,6 @@ public struct RxSignInView: View { mode == .signIn ? manager.signInSchema : manager.signUpSchema } - /// Methods to surface in the native form for the current platform. - /// - /// iOS passkey ceremonies are not yet implemented in `OAuthManager` (they - /// throw `.passkeyUnavailable`), so on iOS we only surface password-based - /// methods to avoid presenting buttons that always fail. If filtering would - /// leave nothing, we fall back to the full server-provided list. Remove this - /// filter once an iOS passkey authenticator lands. - private func displayMethods(_ schema: AuthUISchema) -> [AuthUISchema.SupportedMethod] { - #if os(iOS) - let filtered = schema.supportedMethods.filter { $0.id == .password } - return filtered.isEmpty ? schema.supportedMethods : filtered - #else - return schema.supportedMethods - #endif - } - private var nativeCredentialForm: some View { VStack(spacing: 18) { modeTogglePill @@ -445,7 +429,7 @@ public struct RxSignInView: View { GlassEffectContainer(spacing: 16) { VStack(spacing: 14) { - let methods = displayMethods(schema) + let methods = schema.supportedMethods let primaryMethods = methods.filter { $0.primary } let secondaryMethods = methods.filter { !$0.primary } let orderedMethods = primaryMethods + secondaryMethods