Skip to content

Commit bdf1961

Browse files
authored
fix(auth): make AuthClient an Actor (#664)
1 parent ec05075 commit bdf1961

File tree

3 files changed

+85
-67
lines changed

3 files changed

+85
-67
lines changed

Sources/Auth/AuthClient.swift

Lines changed: 83 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import Helpers
1414
import WatchKit
1515
#endif
1616

17+
#if canImport(ObjectiveC) && canImport(Combine)
18+
import Combine
19+
#endif
20+
1721
typealias AuthClientID = Int
1822

1923
struct AuthClientLoggerDecorator: SupabaseLogger {
@@ -27,21 +31,28 @@ struct AuthClientLoggerDecorator: SupabaseLogger {
2731
}
2832
}
2933

30-
public final class AuthClient: Sendable {
31-
static let globalClientID = LockIsolated(0)
32-
let clientID: AuthClientID
34+
public actor AuthClient {
35+
static var globalClientID = 0
36+
nonisolated let clientID: AuthClientID
3337

34-
private var api: APIClient { Dependencies[clientID].api }
35-
var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
36-
private var codeVerifierStorage: CodeVerifierStorage {
38+
nonisolated private var api: APIClient { Dependencies[clientID].api }
39+
40+
nonisolated var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
41+
42+
nonisolated private var codeVerifierStorage: CodeVerifierStorage {
3743
Dependencies[clientID].codeVerifierStorage
3844
}
39-
private var date: @Sendable () -> Date { Dependencies[clientID].date }
40-
private var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
41-
private var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }
42-
private var logger: (any SupabaseLogger)? { Dependencies[clientID].configuration.logger }
43-
private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage }
44-
private var pkce: PKCE { Dependencies[clientID].pkce }
45+
46+
nonisolated private var date: @Sendable () -> Date { Dependencies[clientID].date }
47+
nonisolated private var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
48+
nonisolated private var eventEmitter: AuthStateChangeEventEmitter {
49+
Dependencies[clientID].eventEmitter
50+
}
51+
nonisolated private var logger: (any SupabaseLogger)? {
52+
Dependencies[clientID].configuration.logger
53+
}
54+
nonisolated private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage }
55+
nonisolated private var pkce: PKCE { Dependencies[clientID].pkce }
4556

4657
/// Returns the session, refreshing it if necessary.
4758
///
@@ -55,26 +66,26 @@ public final class AuthClient: Sendable {
5566
/// Returns the current session, if any.
5667
///
5768
/// The session returned by this property may be expired. Use ``session`` for a session that is guaranteed to be valid.
58-
public var currentSession: Session? {
69+
nonisolated public var currentSession: Session? {
5970
sessionStorage.get()
6071
}
6172

6273
/// Returns the current user, if any.
6374
///
6475
/// The user returned by this property may be outdated. Use ``user(jwt:)`` method to get an up-to-date user instance.
65-
public var currentUser: User? {
76+
nonisolated public var currentUser: User? {
6677
currentSession?.user
6778
}
6879

6980
/// Namespace for accessing multi-factor authentication API.
70-
public var mfa: AuthMFA {
81+
nonisolated public var mfa: AuthMFA {
7182
AuthMFA(clientID: clientID)
7283
}
7384

7485
/// Namespace for the GoTrue admin methods.
7586
/// - Warning: This methods requires `service_role` key, be careful to never expose `service_role`
7687
/// key in the client.
77-
public var admin: AuthAdmin {
88+
nonisolated public var admin: AuthAdmin {
7889
AuthAdmin(clientID: clientID)
7990
}
8091

@@ -83,10 +94,8 @@ public final class AuthClient: Sendable {
8394
/// - Parameters:
8495
/// - configuration: The client configuration.
8596
public init(configuration: Configuration) {
86-
clientID = AuthClient.globalClientID.withValue {
87-
$0 += 1
88-
return $0
89-
}
97+
AuthClient.globalClientID += 1
98+
clientID = AuthClient.globalClientID
9099

91100
Dependencies[clientID] = Dependencies(
92101
configuration: configuration,
@@ -103,63 +112,69 @@ public final class AuthClient: Sendable {
103112
Task { @MainActor in observeAppLifecycleChanges() }
104113
}
105114

106-
#if canImport(ObjectiveC)
115+
#if canImport(ObjectiveC) && canImport(Combine)
107116
@MainActor
108117
private func observeAppLifecycleChanges() {
118+
var didBecomeActiveNotification: NSNotification.Name?
119+
var willResignActiveNotification: NSNotification.Name?
120+
109121
#if canImport(UIKit)
110122
#if canImport(WatchKit)
111123
if #available(watchOS 7.0, *) {
112-
NotificationCenter.default.addObserver(
113-
self,
114-
selector: #selector(handleDidBecomeActive),
115-
name: WKExtension.applicationDidBecomeActiveNotification,
116-
object: nil
117-
)
118-
NotificationCenter.default.addObserver(
119-
self,
120-
selector: #selector(handleWillResignActive),
121-
name: WKExtension.applicationWillResignActiveNotification,
122-
object: nil
123-
)
124+
didBecomeActiveNotification = WKExtension.applicationDidBecomeActiveNotification
125+
willResignActiveNotification = WKExtension.applicationWillResignActiveNotification
124126
}
125127
#else
126-
NotificationCenter.default.addObserver(
127-
self,
128-
selector: #selector(handleDidBecomeActive),
129-
name: UIApplication.didBecomeActiveNotification,
130-
object: nil
131-
)
132-
NotificationCenter.default.addObserver(
133-
self,
134-
selector: #selector(handleWillResignActive),
135-
name: UIApplication.willResignActiveNotification,
136-
object: nil
137-
)
128+
didBecomeActiveNotification = UIApplication.didBecomeActiveNotification
129+
willResignActiveNotification = UIApplication.willResignActiveNotification
138130
#endif
139131
#elseif canImport(AppKit)
140-
NotificationCenter.default.addObserver(
141-
self,
142-
selector: #selector(handleDidBecomeActive),
143-
name: NSApplication.didBecomeActiveNotification,
144-
object: nil
145-
)
146-
NotificationCenter.default.addObserver(
147-
self,
148-
selector: #selector(handleWillResignActive),
149-
name: NSApplication.willResignActiveNotification,
150-
object: nil
151-
)
132+
didBecomeActiveNotification = NSApplication.didBecomeActiveNotification
133+
willResignActiveNotification = NSApplication.willResignActiveNotification
152134
#endif
135+
136+
if let didBecomeActiveNotification, let willResignActiveNotification {
137+
var cancellables = Set<AnyCancellable>()
138+
139+
NotificationCenter.default
140+
.publisher(for: didBecomeActiveNotification)
141+
.sink(
142+
receiveCompletion: { _ in
143+
// hold ref to cancellable until it completes
144+
_ = cancellables
145+
},
146+
receiveValue: { [weak self] _ in
147+
Task {
148+
await self?.handleDidBecomeActive()
149+
}
150+
}
151+
)
152+
.store(in: &cancellables)
153+
154+
NotificationCenter.default
155+
.publisher(for: willResignActiveNotification)
156+
.sink(
157+
receiveCompletion: { _ in
158+
// hold ref to cancellable until it completes
159+
_ = cancellables
160+
},
161+
receiveValue: { [weak self] _ in
162+
Task {
163+
await self?.handleWillResignActive()
164+
}
165+
}
166+
)
167+
.store(in: &cancellables)
168+
}
169+
153170
}
154171

155-
@objc
156172
private func handleDidBecomeActive() {
157173
if configuration.autoRefreshToken {
158174
startAutoRefresh()
159175
}
160176
}
161177

162-
@objc
163178
private func handleWillResignActive() {
164179
if configuration.autoRefreshToken {
165180
stopAutoRefresh()
@@ -170,6 +185,7 @@ public final class AuthClient: Sendable {
170185
// no-op
171186
}
172187
#endif
188+
173189
/// Listen for auth state changes.
174190
/// - Parameter listener: Block that executes when a new event is emitted.
175191
/// - Returns: A handle that can be used to manually unsubscribe.
@@ -189,7 +205,7 @@ public final class AuthClient: Sendable {
189205
/// Listen for auth state changes.
190206
///
191207
/// An `.initialSession` is always emitted when this method is called.
192-
public var authStateChanges:
208+
nonisolated public var authStateChanges:
193209
AsyncStream<
194210
(
195211
event: AuthChangeEvent,
@@ -597,7 +613,7 @@ public final class AuthClient: Sendable {
597613
/// If that isn't the case, you should consider using
598614
/// ``signInWithOAuth(provider:redirectTo:scopes:queryParams:launchFlow:)`` or
599615
/// ``signInWithOAuth(provider:redirectTo:scopes:queryParams:configure:)``.
600-
public func getOAuthSignInURL(
616+
nonisolated public func getOAuthSignInURL(
601617
provider: Provider,
602618
scopes: String? = nil,
603619
redirectTo: URL? = nil,
@@ -672,7 +688,7 @@ public final class AuthClient: Sendable {
672688
scopes: scopes,
673689
queryParams: queryParams
674690
) { @MainActor url in
675-
try await withCheckedThrowingContinuation { continuation in
691+
try await withCheckedThrowingContinuation { [configuration] continuation in
676692
guard let callbackScheme = (configuration.redirectToURL ?? redirectTo)?.scheme else {
677693
preconditionFailure(
678694
"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 {
767783
/// supabase.auth.handle(url)
768784
/// }
769785
/// ```
770-
public func handle(_ url: URL) {
786+
nonisolated public func handle(_ url: URL) {
771787
Task {
772788
do {
773789
try await session(from: url)
@@ -1326,7 +1342,9 @@ public final class AuthClient: Sendable {
13261342
eventEmitter.emit(.initialSession, session: session, token: token)
13271343
}
13281344

1329-
private func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) {
1345+
nonisolated private func prepareForPKCE() -> (
1346+
codeChallenge: String?, codeChallengeMethod: String?
1347+
) {
13301348
guard configuration.flowType == .pkce else {
13311349
return (nil, nil)
13321350
}
@@ -1350,7 +1368,7 @@ public final class AuthClient: Sendable {
13501368
|| params["error_code"] != nil && currentCodeVerifier != nil
13511369
}
13521370

1353-
private func getURLForProvider(
1371+
nonisolated private func getURLForProvider(
13541372
url: URL,
13551373
provider: Provider,
13561374
scopes: String? = nil,

Sources/Auth/AuthClientConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ extension AuthClient {
104104
/// - decoder: The JSON decoder to use for decoding responses.
105105
/// - fetch: The asynchronous fetch handler for network requests.
106106
/// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring.
107-
public convenience init(
107+
public init(
108108
url: URL? = nil,
109109
headers: [String: String] = [:],
110110
flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType,

Sources/Auth/Deprecated.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ extension AuthClient {
105105
deprecated,
106106
message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)"
107107
)
108-
public convenience init(
108+
public init(
109109
url: URL,
110110
headers: [String: String] = [:],
111111
flowType: AuthFlowType = Configuration.defaultFlowType,

0 commit comments

Comments
 (0)