Skip to content
Merged
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
24 changes: 12 additions & 12 deletions Sources/RxAuthSwift/OAuthManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1348,7 +1348,7 @@ private struct PasskeyAccountCreationVerifyRequest: Encodable {
}
}

#if os(macOS)
#if os(macOS) || os(iOS)
private extension PasskeyAccountCreationVerifyRequest {
init(
clientID: String,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PasskeyAssertion, Error>?
private var retainedSelf: MacOSPasskeyAuthenticator?
private var retainedSelf: PlatformPasskeyAuthenticator?

func authenticate(
relyingPartyIdentifier: String,
Expand Down Expand Up @@ -46,7 +66,7 @@ final class MacOSPasskeyAuthenticator: NSObject {
}
}

extension MacOSPasskeyAuthenticator: ASAuthorizationControllerDelegate {
extension PlatformPasskeyAuthenticator: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
Expand Down Expand Up @@ -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()
}
}

Expand All @@ -96,9 +114,9 @@ struct PasskeyAssertion {
}

@MainActor
final class MacOSPasskeyRegistrationAuthenticator: NSObject {
final class PlatformPasskeyRegistrationAuthenticator: NSObject {
private var continuation: CheckedContinuation<PasskeyRegistration, Error>?
private var retainedSelf: MacOSPasskeyRegistrationAuthenticator?
private var retainedSelf: PlatformPasskeyRegistrationAuthenticator?

func register(
relyingPartyIdentifier: String,
Expand Down Expand Up @@ -140,7 +158,7 @@ final class MacOSPasskeyRegistrationAuthenticator: NSObject {
}
}

extension MacOSPasskeyRegistrationAuthenticator: ASAuthorizationControllerDelegate {
extension PlatformPasskeyRegistrationAuthenticator: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
Expand Down Expand Up @@ -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()
}
}

Expand All @@ -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<PasskeyRegistration?, Never>?
private var retainedSelf: MacOSPasskeyConditionalUpgradeAuthenticator?
private var retainedSelf: PlatformPasskeyConditionalUpgradeAuthenticator?

func upgrade(
relyingPartyIdentifier: String,
Expand Down Expand Up @@ -231,7 +247,7 @@ final class MacOSPasskeyConditionalUpgradeAuthenticator: NSObject {
}
}

extension MacOSPasskeyConditionalUpgradeAuthenticator: ASAuthorizationControllerDelegate {
extension PlatformPasskeyConditionalUpgradeAuthenticator: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
Expand All @@ -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()
}
}

Expand Down Expand Up @@ -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<PasskeyAccountCreation, Error>?
private var retainedSelf: MacOSPasskeyAccountCreationAuthenticator?
private var retainedSelf: PlatformPasskeyAccountCreationAuthenticator?

func createAccount(
relyingPartyIdentifier: String,
Expand Down Expand Up @@ -339,7 +353,7 @@ final class MacOSPasskeyAccountCreationAuthenticator: NSObject {
}
}

extension MacOSPasskeyAccountCreationAuthenticator: ASAuthorizationControllerDelegate {
extension PlatformPasskeyAccountCreationAuthenticator: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
Expand Down Expand Up @@ -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
18 changes: 1 addition & 17 deletions Sources/RxAuthSwiftUI/RxSignInView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -413,22 +413,6 @@ public struct RxSignInView<Header: View>: 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
Expand All @@ -445,7 +429,7 @@ public struct RxSignInView<Header: View>: 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
Expand Down
Loading