diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 95c39f3c..c2524506 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -709,7 +709,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; - CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\""; DEVELOPMENT_TEAM = ELTTE7K8TT; ENABLE_HARDENED_RUNTIME = YES; @@ -719,7 +718,12 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.supabase.swift-examples"; PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -729,7 +733,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; - CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\""; DEVELOPMENT_TEAM = ELTTE7K8TT; ENABLE_HARDENED_RUNTIME = YES; @@ -739,7 +742,12 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.supabase.swift-examples"; PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; diff --git a/Examples/Examples/Auth/AuthView.swift b/Examples/Examples/Auth/AuthView.swift index e6dae5a8..88830900 100644 --- a/Examples/Examples/Auth/AuthView.swift +++ b/Examples/Examples/Auth/AuthView.swift @@ -14,7 +14,9 @@ struct AuthView: View { case signInWithPhone case signInWithApple case signInWithOAuth - case signInWithOAuthUsingUIKit + #if canImport(UIKit) + case signInWithOAuthUsingUIKit + #endif case googleSignInSDKFlow case signInAnonymously @@ -25,7 +27,9 @@ struct AuthView: View { case .signInWithPhone: "Sign in with Phone" case .signInWithApple: "Sign in with Apple" case .signInWithOAuth: "Sign in with OAuth flow" - case .signInWithOAuthUsingUIKit: "Sign in with OAuth flow (UIKit)" + #if canImport(UIKit) + case .signInWithOAuthUsingUIKit: "Sign in with OAuth flow (UIKit)" + #endif case .googleSignInSDKFlow: "Google Sign in (GIDSignIn SDK Flow)" case .signInAnonymously: "Sign in Anonymously" } @@ -43,7 +47,9 @@ struct AuthView: View { options .navigationTitle(options.title) } + #if !os(macOS) .navigationBarTitleDisplayMode(.inline) + #endif } } } @@ -56,8 +62,10 @@ extension AuthView.Option: View { case .signInWithPhone: SignInWithPhone() case .signInWithApple: SignInWithApple() case .signInWithOAuth: SignInWithOAuth() - case .signInWithOAuthUsingUIKit: UIViewControllerWrapper(SignInWithOAuthViewController()) - .edgesIgnoringSafeArea(.all) + #if canImport(UIKit) + case .signInWithOAuthUsingUIKit: UIViewControllerWrapper(SignInWithOAuthViewController()) + .edgesIgnoringSafeArea(.all) + #endif case .googleSignInSDKFlow: GoogleSignInSDKFlow() case .signInAnonymously: SignInAnonymously() } diff --git a/Examples/Examples/Auth/AuthWithEmailAndPassword.swift b/Examples/Examples/Auth/AuthWithEmailAndPassword.swift index 5a0fe82f..3db18342 100644 --- a/Examples/Examples/Auth/AuthWithEmailAndPassword.swift +++ b/Examples/Examples/Auth/AuthWithEmailAndPassword.swift @@ -27,15 +27,19 @@ struct AuthWithEmailAndPassword: View { Form { Section { TextField("Email", text: $email) - .keyboardType(.emailAddress) .textContentType(.emailAddress) .autocorrectionDisabled() + #if !os(macOS) + .keyboardType(.emailAddress) .textInputAutocapitalization(.never) + #endif SecureField("Password", text: $password) .textContentType(.password) .autocorrectionDisabled() + #if !os(macOS) .textInputAutocapitalization(.never) + #endif } Section { diff --git a/Examples/Examples/Auth/AuthWithMagicLink.swift b/Examples/Examples/Auth/AuthWithMagicLink.swift index 7cbb3707..1267e223 100644 --- a/Examples/Examples/Auth/AuthWithMagicLink.swift +++ b/Examples/Examples/Auth/AuthWithMagicLink.swift @@ -15,10 +15,12 @@ struct AuthWithMagicLink: View { Form { Section { TextField("Email", text: $email) - .keyboardType(.emailAddress) .textContentType(.emailAddress) .autocorrectionDisabled() + #if !os(macOS) + .keyboardType(.emailAddress) .textInputAutocapitalization(.never) + #endif } Section { diff --git a/Examples/Examples/Auth/GoogleSignInSDKFlow.swift b/Examples/Examples/Auth/GoogleSignInSDKFlow.swift index f7fb7fc7..09035f34 100644 --- a/Examples/Examples/Auth/GoogleSignInSDKFlow.swift +++ b/Examples/Examples/Auth/GoogleSignInSDKFlow.swift @@ -19,7 +19,7 @@ struct GoogleSignInSDKFlow: View { func handleSignIn() { Task { do { - let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) + let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: root) guard let idToken = result.user.idToken?.tokenString else { debug("No 'idToken' returned by GIDSignIn call.") @@ -38,9 +38,15 @@ struct GoogleSignInSDKFlow: View { } } - var rootViewController: UIViewController { - UIApplication.shared.firstKeyWindow?.rootViewController ?? UIViewController() - } + #if canImport(AppKit) + var root: NSWindow { + NSApplication.shared.windows.first ?? NSWindow() + } + #else + var root: UIViewController { + UIApplication.shared.firstKeyWindow?.rootViewController ?? UIViewController() + } + #endif } #Preview { diff --git a/Examples/Examples/Auth/SignInWithOAuth.swift b/Examples/Examples/Auth/SignInWithOAuth.swift index cc325a6f..295c73eb 100644 --- a/Examples/Examples/Auth/SignInWithOAuth.swift +++ b/Examples/Examples/Auth/SignInWithOAuth.swift @@ -45,78 +45,81 @@ struct SignInWithOAuth: View { } } -final class SignInWithOAuthViewController: UIViewController, UIPickerViewDataSource, - UIPickerViewDelegate -{ - let providers = Provider.allCases - var provider = Provider.allCases[0] +#if canImport(UIKit) + final class SignInWithOAuthViewController: UIViewController, UIPickerViewDataSource, + UIPickerViewDelegate + { + let providers = Provider.allCases + var provider = Provider.allCases[0] + + let providerPicker = UIPickerView() + let signInButton = UIButton(type: .system) + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + } - let providerPicker = UIPickerView() - let signInButton = UIButton(type: .system) + func setupViews() { + view.backgroundColor = .white + + providerPicker.dataSource = self + providerPicker.delegate = self + view.addSubview(providerPicker) + providerPicker.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + providerPicker.centerXAnchor.constraint(equalTo: view.centerXAnchor), + providerPicker.centerYAnchor.constraint(equalTo: view.centerYAnchor), + providerPicker.widthAnchor.constraint(equalToConstant: 200), + providerPicker.heightAnchor.constraint(equalToConstant: 100), + ]) + + signInButton.setTitle("Start Sign-in Flow", for: .normal) + signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside) + view.addSubview(signInButton) + signInButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + signInButton.topAnchor.constraint(equalTo: providerPicker.bottomAnchor, constant: 20), + ]) + } - override func viewDidLoad() { - super.viewDidLoad() - setupViews() - } + @objc func signInButtonTapped() { + Task { + do { + try await supabase.auth.signInWithOAuth( + provider: provider, + redirectTo: Constants.redirectToURL + ) + } catch { + debug("Failed to sign-in with OAuth flow: \(error)") + } + } + } - func setupViews() { - view.backgroundColor = .white - - providerPicker.dataSource = self - providerPicker.delegate = self - view.addSubview(providerPicker) - providerPicker.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - providerPicker.centerXAnchor.constraint(equalTo: view.centerXAnchor), - providerPicker.centerYAnchor.constraint(equalTo: view.centerYAnchor), - providerPicker.widthAnchor.constraint(equalToConstant: 200), - providerPicker.heightAnchor.constraint(equalToConstant: 100), - ]) - - signInButton.setTitle("Start Sign-in Flow", for: .normal) - signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside) - view.addSubview(signInButton) - signInButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - signInButton.topAnchor.constraint(equalTo: providerPicker.bottomAnchor, constant: 20), - ]) - } + func numberOfComponents(in _: UIPickerView) -> Int { + 1 + } - @objc func signInButtonTapped() { - Task { - do { - try await supabase.auth.signInWithOAuth( - provider: provider, - redirectTo: Constants.redirectToURL - ) - } catch { - debug("Failed to sign-in with OAuth flow: \(error)") - } + func pickerView(_: UIPickerView, numberOfRowsInComponent _: Int) -> Int { + providers.count } - } - func numberOfComponents(in _: UIPickerView) -> Int { - 1 - } + func pickerView(_: UIPickerView, titleForRow row: Int, forComponent _: Int) -> String? { + "\(providers[row])" + } - func pickerView(_: UIPickerView, numberOfRowsInComponent _: Int) -> Int { - providers.count + func pickerView(_: UIPickerView, didSelectRow row: Int, inComponent _: Int) { + provider = providers[row] + } } - func pickerView(_: UIPickerView, titleForRow row: Int, forComponent _: Int) -> String? { - "\(providers[row])" + #Preview("UIKit") { + SignInWithOAuthViewController() } - func pickerView(_: UIPickerView, didSelectRow row: Int, inComponent _: Int) { - provider = providers[row] - } -} +#endif #Preview("SwiftUI") { SignInWithOAuth() } - -#Preview("UIKit") { - SignInWithOAuthViewController() -} diff --git a/Examples/Examples/Examples.entitlements b/Examples/Examples/Examples.entitlements index 4210463c..625af03d 100644 --- a/Examples/Examples/Examples.entitlements +++ b/Examples/Examples/Examples.entitlements @@ -2,13 +2,11 @@ - com.apple.developer.applesignin - - Default - com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only + com.apple.security.network.client + diff --git a/Examples/Examples/ExamplesApp.swift b/Examples/Examples/ExamplesApp.swift index 2ee2b7b9..4ea4624b 100644 --- a/Examples/Examples/ExamplesApp.swift +++ b/Examples/Examples/ExamplesApp.swift @@ -9,40 +9,60 @@ import GoogleSignIn import Supabase import SwiftUI -class AppDelegate: UIResponder, UIApplicationDelegate { - func application( - _: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - if let url = launchOptions?[.url] as? URL { +#if canImport(AppKit) + class AppDelegate: NSResponder, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + print(notification) +// if let url = launchOptions?[.url] as? URL { +// supabase.handle(url) +// } + } + + func application(_: NSApplication, open urls: [URL]) { + guard let url = urls.first else { return } supabase.handle(url) } - return true } +#else + class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + if let url = launchOptions?[.url] as? URL { + supabase.handle(url) + } + return true + } - func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - supabase.handle(url) - return true - } + func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + supabase.handle(url) + return true + } - func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { - let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) - configuration.delegateClass = SceneDelegate.self - return configuration + func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + configuration.delegateClass = SceneDelegate.self + return configuration + } } -} -class SceneDelegate: UIResponder, UISceneDelegate { - func scene(_: UIScene, openURLContexts URLContexts: Set) { - guard let url = URLContexts.first?.url else { return } + class SceneDelegate: UIResponder, UISceneDelegate { + func scene(_: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } - supabase.handle(url) + supabase.handle(url) + } } -} +#endif @main struct ExamplesApp: App { - @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #if canImport(AppKit) + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #else + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #endif var body: some Scene { WindowGroup { diff --git a/Examples/Examples/Profile/UpdateProfileView.swift b/Examples/Examples/Profile/UpdateProfileView.swift index 49ecf14f..ce02bb7d 100644 --- a/Examples/Examples/Profile/UpdateProfileView.swift +++ b/Examples/Examples/Profile/UpdateProfileView.swift @@ -34,14 +34,18 @@ struct UpdateProfileView: View { Section { TextField("Email", text: $email) .textContentType(.emailAddress) - .keyboardType(.emailAddress) .autocorrectionDisabled() + #if !os(macOS) + .keyboardType(.emailAddress) .textInputAutocapitalization(.never) + #endif TextField("Phone", text: $phone) .textContentType(.telephoneNumber) - .keyboardType(.phonePad) .autocorrectionDisabled() + #if !os(macOS) + .keyboardType(.phonePad) .textInputAutocapitalization(.never) + #endif } Section { diff --git a/Examples/Examples/UIApplicationExtensions.swift b/Examples/Examples/UIApplicationExtensions.swift index e39ae820..e3a26aa5 100644 --- a/Examples/Examples/UIApplicationExtensions.swift +++ b/Examples/Examples/UIApplicationExtensions.swift @@ -5,14 +5,16 @@ // Created by Guilherme Souza on 05/03/24. // -import UIKit +#if canImport(UIKit) + import UIKit -extension UIApplication { - var firstKeyWindow: UIWindow? { - UIApplication.shared - .connectedScenes - .compactMap { $0 as? UIWindowScene } - .filter { $0.activationState == .foregroundActive } - .first?.keyWindow + extension UIApplication { + var firstKeyWindow: UIWindow? { + UIApplication.shared + .connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { $0.activationState == .foregroundActive } + .first?.keyWindow + } } -} +#endif diff --git a/Examples/Examples/UIViewControllerWrapper.swift b/Examples/Examples/UIViewControllerWrapper.swift index 7738a041..4e7ffda6 100644 --- a/Examples/Examples/UIViewControllerWrapper.swift +++ b/Examples/Examples/UIViewControllerWrapper.swift @@ -7,20 +7,23 @@ import SwiftUI -struct UIViewControllerWrapper: UIViewControllerRepresentable { - typealias UIViewControllerType = T +#if canImport(UIKit) - let viewController: T + struct UIViewControllerWrapper: UIViewControllerRepresentable { + typealias UIViewControllerType = T - init(_ viewController: T) { - self.viewController = viewController - } + let viewController: T - func makeUIViewController(context _: Context) -> T { - viewController - } + init(_ viewController: T) { + self.viewController = viewController + } + + func makeUIViewController(context _: Context) -> T { + viewController + } - func updateUIViewController(_: T, context _: Context) { - // Update the view controller if needed + func updateUIViewController(_: T, context _: Context) { + // Update the view controller if needed + } } -} +#endif diff --git a/Examples/SlackClone/SlackClone.entitlements b/Examples/SlackClone/SlackClone.entitlements index 625af03d..5776a3a2 100644 --- a/Examples/SlackClone/SlackClone.entitlements +++ b/Examples/SlackClone/SlackClone.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.client + keychain-access-groups + diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index d775eb55..15978ff4 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -12,17 +12,17 @@ import Helpers typealias AuthClientID = UUID -public final class AuthClient: Sendable { - let clientID = AuthClientID() +public actor AuthClient { + nonisolated let clientID = AuthClientID() + nonisolated var configuration: AuthClient.Configuration { Dependencies[clientID].configuration } + private nonisolated var codeVerifierStorage: CodeVerifierStorage { Dependencies[clientID].codeVerifierStorage } + private nonisolated var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage } private var api: APIClient { Dependencies[clientID].api } - var configuration: AuthClient.Configuration { Dependencies[clientID].configuration } - private var codeVerifierStorage: CodeVerifierStorage { Dependencies[clientID].codeVerifierStorage } private var date: @Sendable () -> Date { Dependencies[clientID].date } - private var sessionManager: SessionManager { Dependencies[clientID].sessionManager } private var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter } - private var logger: (any SupabaseLogger)? { Dependencies[clientID].configuration.logger } - private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage } + private nonisolated var logger: (any SupabaseLogger)? { Dependencies[clientID].configuration.logger } + private var sessionManager: SessionManager { Dependencies[clientID].sessionManager } /// Returns the session, refreshing it if necessary. /// @@ -36,26 +36,26 @@ public final class AuthClient: Sendable { /// Returns the current session, if any. /// /// The session returned by this property may be expired. Use ``session`` for a session that is guaranteed to be valid. - public var currentSession: Session? { + public nonisolated var currentSession: Session? { try? sessionStorage.get() } /// Returns the current user, if any. /// /// The user returned by this property may be outdated. Use ``user(jwt:)`` method to get an up-to-date user instance. - public var currentUser: User? { + public nonisolated var currentUser: User? { try? sessionStorage.get()?.user } /// Namespace for accessing multi-factor authentication API. - public var mfa: AuthMFA { + public nonisolated var mfa: AuthMFA { AuthMFA(clientID: clientID) } /// Namespace for the GoTrue admin methods. /// - Warning: This methods requires `service_role` key, be careful to never expose `service_role` /// key in the client. - public var admin: AuthAdmin { + public nonisolated var admin: AuthAdmin { AuthAdmin(clientID: clientID) } @@ -93,7 +93,7 @@ public final class AuthClient: Sendable { /// Listen for auth state changes. /// /// An `.initialSession` is always emitted when this method is called. - public var authStateChanges: AsyncStream<( + public nonisolated var authStateChanges: AsyncStream<( event: AuthChangeEvent, session: Session? )> { @@ -487,7 +487,7 @@ public final class AuthClient: Sendable { /// If that isn't the case, you should consider using /// ``signInWithOAuth(provider:redirectTo:scopes:queryParams:launchFlow:)`` or /// ``signInWithOAuth(provider:redirectTo:scopes:queryParams:configure:)``. - public func getOAuthSignInURL( + public nonisolated func getOAuthSignInURL( provider: Provider, scopes: String? = nil, redirectTo: URL? = nil, @@ -657,7 +657,7 @@ public final class AuthClient: Sendable { /// supabase.auth.handle(url) /// } /// ``` - public func handle(_ url: URL) { + public nonisolated func handle(_ url: URL) { Task { do { try await session(from: url) @@ -1174,7 +1174,7 @@ public final class AuthClient: Sendable { eventEmitter.emit(.initialSession, session: session, token: token) } - private func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) { + private nonisolated func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) { guard configuration.flowType == .pkce else { return (nil, nil) } @@ -1197,7 +1197,7 @@ public final class AuthClient: Sendable { return params["code"] != nil && currentCodeVerifier != nil } - private func getURLForProvider( + private nonisolated func getURLForProvider( url: URL, provider: Provider, scopes: String? = nil, diff --git a/Sources/Auth/AuthClientConfiguration.swift b/Sources/Auth/AuthClientConfiguration.swift index 4b724a0a..4fcc3bd3 100644 --- a/Sources/Auth/AuthClientConfiguration.swift +++ b/Sources/Auth/AuthClientConfiguration.swift @@ -93,7 +93,7 @@ extension AuthClient { /// - decoder: The JSON decoder to use for decoding responses. /// - fetch: The asynchronous fetch handler for network requests. /// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring. - public convenience init( + public init( url: URL, headers: [String: String] = [:], flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, diff --git a/Sources/Auth/Deprecated.swift b/Sources/Auth/Deprecated.swift index ac7c1fca..3f1eba1d 100644 --- a/Sources/Auth/Deprecated.swift +++ b/Sources/Auth/Deprecated.swift @@ -105,7 +105,7 @@ extension AuthClient { deprecated, message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" ) - public convenience init( + public init( url: URL, headers: [String: String] = [:], flowType: AuthFlowType = Configuration.defaultFlowType, diff --git a/Sources/Auth/Internal/EventEmitter.swift b/Sources/Auth/Internal/EventEmitter.swift index 0c2aedf2..7bc40dfb 100644 --- a/Sources/Auth/Internal/EventEmitter.swift +++ b/Sources/Auth/Internal/EventEmitter.swift @@ -3,7 +3,10 @@ import Foundation import Helpers struct AuthStateChangeEventEmitter { - var emitter = EventEmitter<(AuthChangeEvent, Session?)?>(initialEvent: nil, emitsLastEventWhenAttaching: false) + let emitter = EventEmitter<(AuthChangeEvent, Session?)?>( + initialEvent: nil, + emitsLastEventWhenAttaching: false + ) func attach(_ listener: @escaping AuthStateChangeListener) -> ObservationToken { emitter.attach { event in diff --git a/Sources/Auth/Internal/Keychain.swift b/Sources/Auth/Internal/Keychain.swift index 0d84378f..9be9f905 100644 --- a/Sources/Auth/Internal/Keychain.swift +++ b/Sources/Auth/Internal/Keychain.swift @@ -67,10 +67,12 @@ query[kSecAttrAccessGroup as String] = accessGroup } - // this is highly recommended for all keychain operations and makes the - // macOS keychain item behave like an iOS keychain item - // https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain - query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue + #if os(macOS) + // this is highly recommended for all keychain operations and makes the + // macOS keychain item behave like an iOS keychain item + // https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain + query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue + #endif return query } @@ -85,7 +87,14 @@ func setQuery(forKey key: String, data: Data) -> [String: Any] { var query = baseQuery(withKey: key, data: data) - query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock + #if os(macOS) + // See https://developer.apple.com/documentation/security/ksecattraccessible + if query[kSecUseDataProtectionKeychain as String] as? Bool == true { + query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock + } + #else + query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock + #endif return query } @@ -102,6 +111,7 @@ case itemNotFound case interactionNotAllowed case decodeFailed + case missingEntitlement case other(status: OSStatus) case unknown(message: String) @@ -116,6 +126,7 @@ case errSecItemNotFound: self = .itemNotFound case errSecInteractionNotAllowed: self = .interactionNotAllowed case errSecDecode: self = .decodeFailed + case errSecMissingEntitlement: self = .missingEntitlement default: self = .other(status: rawValue) } } @@ -131,6 +142,7 @@ case .itemNotFound: errSecItemNotFound case .interactionNotAllowed: errSecInteractionNotAllowed case .decodeFailed: errSecDecode + case .missingEntitlement: errSecMissingEntitlement case let .other(status): status case .unknown: errSecSuccess // This is not a Keychain error } @@ -171,6 +183,8 @@ "errSecInteractionNotAllowed: Interaction with the Security Server is not allowed." case .decodeFailed: "errSecDecode: Unable to decode the provided data." + case .missingEntitlement: + "errSecMissingEntitlement: A required entitlement is missing." case .other: "Unspecified Keychain error: \(status)." case let .unknown(message): @@ -216,6 +230,10 @@ /// See [errSecDecode](https://developer.apple.com/documentation/security/errsecdecode). static let decodeFailed: KeychainError = .init(code: .decodeFailed) + /// A required entitlement is missing. + /// See [errSecMissingEntitlement](https://developer.apple.com/documentation/security/errsecmissingentitlement) + static let missingEntitlement: KeychainError = .init(code: .missingEntitlement) + /// Other Keychain error. /// The `OSStatus` of the Keychain operation can be accessed via the ``status`` property. static let other: KeychainError = .init(code: .other(status: 0))