From bdf19614b35b76281f2a50e785b6d21fa9578e40 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 14 Feb 2025 07:24:48 -0300 Subject: [PATCH 1/2] fix(auth): make AuthClient an Actor (#664) --- Sources/Auth/AuthClient.swift | 148 ++++++++++++--------- Sources/Auth/AuthClientConfiguration.swift | 2 +- Sources/Auth/Deprecated.swift | 2 +- 3 files changed, 85 insertions(+), 67 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index ee114978..1e8fe0f7 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -14,6 +14,10 @@ import Helpers import WatchKit #endif +#if canImport(ObjectiveC) && canImport(Combine) + import Combine +#endif + typealias AuthClientID = Int struct AuthClientLoggerDecorator: SupabaseLogger { @@ -27,21 +31,28 @@ struct AuthClientLoggerDecorator: SupabaseLogger { } } -public final class AuthClient: Sendable { - static let globalClientID = LockIsolated(0) - let clientID: AuthClientID +public actor AuthClient { + static var globalClientID = 0 + nonisolated let clientID: AuthClientID - private var api: APIClient { Dependencies[clientID].api } - var configuration: AuthClient.Configuration { Dependencies[clientID].configuration } - private var codeVerifierStorage: CodeVerifierStorage { + nonisolated private var api: APIClient { Dependencies[clientID].api } + + nonisolated var configuration: AuthClient.Configuration { Dependencies[clientID].configuration } + + nonisolated 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 var pkce: PKCE { Dependencies[clientID].pkce } + + nonisolated private var date: @Sendable () -> Date { Dependencies[clientID].date } + nonisolated private var sessionManager: SessionManager { Dependencies[clientID].sessionManager } + nonisolated private var eventEmitter: AuthStateChangeEventEmitter { + Dependencies[clientID].eventEmitter + } + nonisolated private var logger: (any SupabaseLogger)? { + Dependencies[clientID].configuration.logger + } + nonisolated private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage } + nonisolated private var pkce: PKCE { Dependencies[clientID].pkce } /// Returns the session, refreshing it if necessary. /// @@ -55,26 +66,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? { + nonisolated public var currentSession: Session? { 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? { + nonisolated public var currentUser: User? { currentSession?.user } /// Namespace for accessing multi-factor authentication API. - public var mfa: AuthMFA { + nonisolated public 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 { + nonisolated public var admin: AuthAdmin { AuthAdmin(clientID: clientID) } @@ -83,10 +94,8 @@ public final class AuthClient: Sendable { /// - Parameters: /// - configuration: The client configuration. public init(configuration: Configuration) { - clientID = AuthClient.globalClientID.withValue { - $0 += 1 - return $0 - } + AuthClient.globalClientID += 1 + clientID = AuthClient.globalClientID Dependencies[clientID] = Dependencies( configuration: configuration, @@ -103,63 +112,69 @@ public final class AuthClient: Sendable { Task { @MainActor in observeAppLifecycleChanges() } } - #if canImport(ObjectiveC) + #if canImport(ObjectiveC) && canImport(Combine) @MainActor private func observeAppLifecycleChanges() { + var didBecomeActiveNotification: NSNotification.Name? + var willResignActiveNotification: NSNotification.Name? + #if canImport(UIKit) #if canImport(WatchKit) if #available(watchOS 7.0, *) { - NotificationCenter.default.addObserver( - self, - selector: #selector(handleDidBecomeActive), - name: WKExtension.applicationDidBecomeActiveNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleWillResignActive), - name: WKExtension.applicationWillResignActiveNotification, - object: nil - ) + didBecomeActiveNotification = WKExtension.applicationDidBecomeActiveNotification + willResignActiveNotification = WKExtension.applicationWillResignActiveNotification } #else - NotificationCenter.default.addObserver( - self, - selector: #selector(handleDidBecomeActive), - name: UIApplication.didBecomeActiveNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleWillResignActive), - name: UIApplication.willResignActiveNotification, - object: nil - ) + didBecomeActiveNotification = UIApplication.didBecomeActiveNotification + willResignActiveNotification = UIApplication.willResignActiveNotification #endif #elseif canImport(AppKit) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleDidBecomeActive), - name: NSApplication.didBecomeActiveNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleWillResignActive), - name: NSApplication.willResignActiveNotification, - object: nil - ) + didBecomeActiveNotification = NSApplication.didBecomeActiveNotification + willResignActiveNotification = NSApplication.willResignActiveNotification #endif + + if let didBecomeActiveNotification, let willResignActiveNotification { + var cancellables = Set() + + NotificationCenter.default + .publisher(for: didBecomeActiveNotification) + .sink( + receiveCompletion: { _ in + // hold ref to cancellable until it completes + _ = cancellables + }, + receiveValue: { [weak self] _ in + Task { + await self?.handleDidBecomeActive() + } + } + ) + .store(in: &cancellables) + + NotificationCenter.default + .publisher(for: willResignActiveNotification) + .sink( + receiveCompletion: { _ in + // hold ref to cancellable until it completes + _ = cancellables + }, + receiveValue: { [weak self] _ in + Task { + await self?.handleWillResignActive() + } + } + ) + .store(in: &cancellables) + } + } - @objc private func handleDidBecomeActive() { if configuration.autoRefreshToken { startAutoRefresh() } } - @objc private func handleWillResignActive() { if configuration.autoRefreshToken { stopAutoRefresh() @@ -170,6 +185,7 @@ public final class AuthClient: Sendable { // no-op } #endif + /// Listen for auth state changes. /// - Parameter listener: Block that executes when a new event is emitted. /// - Returns: A handle that can be used to manually unsubscribe. @@ -189,7 +205,7 @@ public final class AuthClient: Sendable { /// Listen for auth state changes. /// /// An `.initialSession` is always emitted when this method is called. - public var authStateChanges: + nonisolated public var authStateChanges: AsyncStream< ( event: AuthChangeEvent, @@ -597,7 +613,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( + nonisolated public func getOAuthSignInURL( provider: Provider, scopes: String? = nil, redirectTo: URL? = nil, @@ -672,7 +688,7 @@ public final class AuthClient: Sendable { scopes: scopes, queryParams: queryParams ) { @MainActor url in - try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { [configuration] continuation in guard let callbackScheme = (configuration.redirectToURL ?? redirectTo)?.scheme else { preconditionFailure( "Please, provide a valid redirect URL, either thorugh `redirectTo` param, or globally thorugh `AuthClient.Configuration.redirectToURL`." @@ -767,7 +783,7 @@ public final class AuthClient: Sendable { /// supabase.auth.handle(url) /// } /// ``` - public func handle(_ url: URL) { + nonisolated public func handle(_ url: URL) { Task { do { try await session(from: url) @@ -1326,7 +1342,9 @@ public final class AuthClient: Sendable { eventEmitter.emit(.initialSession, session: session, token: token) } - private func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) { + nonisolated private func prepareForPKCE() -> ( + codeChallenge: String?, codeChallengeMethod: String? + ) { guard configuration.flowType == .pkce else { return (nil, nil) } @@ -1350,7 +1368,7 @@ public final class AuthClient: Sendable { || params["error_code"] != nil && currentCodeVerifier != nil } - private func getURLForProvider( + nonisolated private func getURLForProvider( url: URL, provider: Provider, scopes: String? = nil, diff --git a/Sources/Auth/AuthClientConfiguration.swift b/Sources/Auth/AuthClientConfiguration.swift index 49b8577f..e5944aee 100644 --- a/Sources/Auth/AuthClientConfiguration.swift +++ b/Sources/Auth/AuthClientConfiguration.swift @@ -104,7 +104,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? = nil, 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, From 660f709d2043cf93cc7af2cb2eae4cc4e4c00b45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:12:27 -0300 Subject: [PATCH 2/2] chore(main): release 2.24.6 (#665) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ Sources/Helpers/Version.swift | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8f0f0366..f3f842cf 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.24.5" + ".": "2.24.6" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a2254a..67bd31a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.24.6](https://github.com/supabase/supabase-swift/compare/v2.24.5...v2.24.6) (2025-02-14) + + +### Bug Fixes + +* **auth:** make AuthClient an Actor ([#664](https://github.com/supabase/supabase-swift/issues/664)) ([bdf1961](https://github.com/supabase/supabase-swift/commit/bdf19614b35b76281f2a50e785b6d21fa9578e40)) + ## [2.24.5](https://github.com/supabase/supabase-swift/compare/v2.24.4...v2.24.5) (2025-02-10) diff --git a/Sources/Helpers/Version.swift b/Sources/Helpers/Version.swift index 398ce92e..82221d86 100644 --- a/Sources/Helpers/Version.swift +++ b/Sources/Helpers/Version.swift @@ -1,6 +1,6 @@ import XCTestDynamicOverlay -private let _version = "2.24.5" // {x-release-please-version} +private let _version = "2.24.6" // {x-release-please-version} #if DEBUG package let version = isTesting ? "0.0.0" : _version