Skip to content

Commit a629bde

Browse files
authored
Add and improve documentation for GoTrue (#138)
* Add and improve documentation for GoTrue * Fix tests
1 parent cb0c6a6 commit a629bde

File tree

11 files changed

+128
-77
lines changed

11 files changed

+128
-77
lines changed

Examples/ProductSample/Data/AuthenticationRepository.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ struct AuthenticationRepositoryImpl: AuthenticationRepository {
2828
}
2929

3030
func authStateListener() async -> AsyncStream<AuthenticationState> {
31-
await client.onAuthStateChange().compactMap { event in
31+
await client.onAuthStateChange().compactMap { event, session in
3232
switch event {
33+
case .initialSession: session != nil ? AuthenticationState.signedIn : .signedOut
3334
case .signedIn: AuthenticationState.signedIn
3435
case .signedOut: AuthenticationState.signedOut
3536
case .passwordRecovery, .tokenRefreshed, .userUpdated, .userDeleted, .mfaChallengeVerified:

Sources/GoTrue/GoTrueClient.swift

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ public typealias AnyJSON = _Helpers.AnyJSON
88
#endif
99

1010
public actor GoTrueClient {
11+
/// FetchHandler is a type alias for asynchronous network request handling.
1112
public typealias FetchHandler =
1213
@Sendable (_ request: URLRequest) async throws -> (Data, URLResponse)
1314

15+
/// Configuration struct represents the client configuration.
1416
public struct Configuration: Sendable {
1517
public let url: URL
1618
public var headers: [String: String]
@@ -20,6 +22,17 @@ public actor GoTrueClient {
2022
public let decoder: JSONDecoder
2123
public let fetch: FetchHandler
2224

25+
/// Initializes a GoTrueClient Configuration with optional parameters.
26+
///
27+
/// - Parameters:
28+
/// - url: The base URL of the GoTrue server.
29+
/// - headers: (Optional) Custom headers to be included in requests.
30+
/// - flowType: (Optional) The authentication flow type. Default is `.implicit`.
31+
/// - localStorage: (Optional) The storage mechanism for local data. Default is a
32+
/// KeychainLocalStorage.
33+
/// - encoder: (Optional) The JSON encoder to use for encoding requests.
34+
/// - decoder: (Optional) The JSON decoder to use for decoding responses.
35+
/// - fetch: (Optional) The asynchronous fetch handler for network requests.
2336
public init(
2437
url: URL,
2538
headers: [String: String] = [:],
@@ -81,6 +94,17 @@ public actor GoTrueClient {
8194
/// Namespace for accessing multi-factor authentication API.
8295
public let mfa: GoTrueMFA
8396

97+
/// Initializes a GoTrueClient with optional parameters.
98+
///
99+
/// - Parameters:
100+
/// - url: The base URL of the GoTrue server.
101+
/// - headers: (Optional) Custom headers to be included in requests.
102+
/// - flowType: (Optional) The authentication flow type. Default is `.implicit`.
103+
/// - localStorage: (Optional) The storage mechanism for local data. Default is a
104+
/// KeychainLocalStorage.
105+
/// - encoder: (Optional) The JSON encoder to use for encoding requests.
106+
/// - decoder: (Optional) The JSON decoder to use for decoding responses.
107+
/// - fetch: (Optional) The asynchronous fetch handler for network requests.
84108
public init(
85109
url: URL,
86110
headers: [String: String] = [:],
@@ -103,6 +127,10 @@ public actor GoTrueClient {
103127
)
104128
}
105129

130+
/// Initializes a GoTrueClient with a specific configuration.
131+
///
132+
/// - Parameters:
133+
/// - configuration: The client configuration.
106134
public init(configuration: Configuration) {
107135
let api = APIClient()
108136

@@ -144,12 +172,13 @@ public actor GoTrueClient {
144172
)
145173
}
146174

147-
public func onAuthStateChange() async -> AsyncStream<AuthChangeEvent> {
175+
/// Listen for auth state changes.
176+
///
177+
/// An `.initialSession` is always emitted when this method is called.
178+
public func onAuthStateChange() async -> AsyncStream<(event: AuthChangeEvent, session: Session?)> {
148179
let (id, stream) = await eventEmitter.attachListener()
149180

150181
Task { [id] in
151-
_debug("emitInitialSessionTask start")
152-
defer { _debug("emitInitialSessionTask end") }
153182
await emitInitialSession(forStreamWithID: id)
154183
}
155184

@@ -192,25 +221,6 @@ public actor GoTrueClient {
192221
)
193222
}
194223

195-
private func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) {
196-
if configuration.flowType == .pkce {
197-
let codeVerifier = PKCE.generateCodeVerifier()
198-
199-
do {
200-
try codeVerifierStorage.storeCodeVerifier(codeVerifier)
201-
} catch {
202-
_debug("Error storing code verifier: \(error)")
203-
}
204-
205-
let codeChallenge = PKCE.generateCodeChallenge(from: codeVerifier)
206-
let codeChallengeMethod = codeVerifier == codeChallenge ? "plain" : "s256"
207-
208-
return (codeChallenge, codeChallengeMethod)
209-
}
210-
211-
return (nil, nil)
212-
}
213-
214224
/// Creates a new user.
215225
/// - Parameters:
216226
/// - phone: User's phone number with international prefix.
@@ -248,7 +258,7 @@ public actor GoTrueClient {
248258

249259
if let session = response.session {
250260
try await sessionManager.update(session)
251-
await eventEmitter.emit(.signedIn)
261+
await eventEmitter.emit(.signedIn, session: session)
252262
}
253263

254264
return response
@@ -308,7 +318,7 @@ public actor GoTrueClient {
308318

309319
if session.user.emailConfirmedAt != nil || session.user.confirmedAt != nil {
310320
try await sessionManager.update(session)
311-
await eventEmitter.emit(.signedIn)
321+
await eventEmitter.emit(.signedIn, session: session)
312322
}
313323

314324
return session
@@ -411,7 +421,7 @@ public actor GoTrueClient {
411421
try codeVerifierStorage.deleteCodeVerifier()
412422

413423
try await sessionManager.update(session)
414-
await eventEmitter.emit(.signedIn)
424+
await eventEmitter.emit(.signedIn, session: session)
415425

416426
return session
417427
} catch {
@@ -524,10 +534,10 @@ public actor GoTrueClient {
524534
)
525535

526536
try await sessionManager.update(session)
527-
await eventEmitter.emit(.signedIn)
537+
await eventEmitter.emit(.signedIn, session: session)
528538

529539
if let type = params.first(where: { $0.name == "type" })?.value, type == "recovery" {
530-
await eventEmitter.emit(.passwordRecovery)
540+
await eventEmitter.emit(.passwordRecovery, session: session)
531541
}
532542

533543
return session
@@ -571,7 +581,7 @@ public actor GoTrueClient {
571581
}
572582

573583
try await sessionManager.update(session)
574-
await eventEmitter.emit(.signedIn)
584+
await eventEmitter.emit(.signedIn, session: session)
575585
return session
576586
}
577587

@@ -586,9 +596,9 @@ public actor GoTrueClient {
586596
)
587597
)
588598
await sessionManager.remove()
589-
await eventEmitter.emit(.signedOut)
599+
await eventEmitter.emit(.signedOut, session: nil)
590600
} catch {
591-
await eventEmitter.emit(.signedOut)
601+
await eventEmitter.emit(.signedOut, session: nil)
592602
throw error
593603
}
594604
}
@@ -662,7 +672,7 @@ public actor GoTrueClient {
662672

663673
if let session = response.session {
664674
try await sessionManager.update(session)
665-
await eventEmitter.emit(.signedIn)
675+
await eventEmitter.emit(.signedIn, session: session)
666676
}
667677

668678
return response
@@ -702,7 +712,7 @@ public actor GoTrueClient {
702712
).decoded(as: User.self, decoder: configuration.decoder)
703713
session.user = updatedUser
704714
try await sessionManager.update(session)
705-
await eventEmitter.emit(.userUpdated)
715+
await eventEmitter.emit(.userUpdated, session: session)
706716
return updatedUser
707717
}
708718

@@ -733,14 +743,24 @@ public actor GoTrueClient {
733743
)
734744
}
735745

746+
/// Refresh and return a new session, regardless of expiry status.
747+
/// - Parameter refreshToken: The optional refresh token to use for refreshing the session. If
748+
/// none is provided then this method tries to load the refresh token from the current session.
749+
/// - Returns: A new session.
736750
@discardableResult
737-
public func refreshSession(refreshToken: String) async throws -> Session {
751+
public func refreshSession(refreshToken: String? = nil) async throws -> Session {
752+
var credentials = UserCredentials(refreshToken: refreshToken)
753+
if credentials.refreshToken == nil {
754+
credentials.refreshToken = try await sessionManager.session(shouldValidateExpiration: false)
755+
.refreshToken
756+
}
757+
738758
let session = try await api.execute(
739759
.init(
740760
path: "/token",
741761
method: "POST",
742762
query: [URLQueryItem(name: "grant_type", value: "refresh_token")],
743-
body: configuration.encoder.encode(UserCredentials(refreshToken: refreshToken))
763+
body: configuration.encoder.encode(credentials)
744764
)
745765
).decoded(as: Session.self, decoder: configuration.decoder)
746766

@@ -749,33 +769,41 @@ public actor GoTrueClient {
749769
.user.confirmedAt != nil
750770
{
751771
try await sessionManager.update(session)
752-
await eventEmitter.emit(.tokenRefreshed)
772+
await eventEmitter.emit(.tokenRefreshed, session: session)
753773
}
754774

755775
return session
756776
}
757777

758-
/// Refresh and return a new session, regardless of expiry status.
759-
@discardableResult
760-
public func refreshSession() async throws -> Session {
761-
let refreshToken = try await session.refreshToken
762-
return try await refreshSession(refreshToken: refreshToken)
763-
}
764-
765778
private func emitInitialSession(forStreamWithID id: UUID) async {
766-
_debug("start")
767-
defer { _debug("end") }
768-
769779
let session = try? await session
770-
await eventEmitter.emit(session != nil ? .signedIn : .signedOut, id)
780+
await eventEmitter.emit(.initialSession, session, id)
771781
}
772782

773-
private func _debug(
774-
_ message: String,
775-
function: StaticString = #function,
776-
line: UInt = #line
777-
) {
778-
debugPrint("[GoTrueClient] \(function):\(line) \(message)")
783+
private func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) {
784+
if configuration.flowType == .pkce {
785+
let codeVerifier = PKCE.generateCodeVerifier()
786+
787+
do {
788+
try codeVerifierStorage.storeCodeVerifier(codeVerifier)
789+
} catch {
790+
assertionFailure(
791+
"""
792+
An error occurred while storing the code verifier,
793+
PKCE flow may not work as expected.
794+
795+
Error: \(error.localizedDescription)
796+
"""
797+
)
798+
}
799+
800+
let codeChallenge = PKCE.generateCodeChallenge(from: codeVerifier)
801+
let codeChallengeMethod = codeVerifier == codeChallenge ? "plain" : "s256"
802+
803+
return (codeChallenge, codeChallengeMethod)
804+
}
805+
806+
return (nil, nil)
779807
}
780808

781809
private func isImplicitGrantFlow(url: URL) -> Bool {
@@ -793,6 +821,7 @@ public actor GoTrueClient {
793821
}
794822

795823
extension GoTrueClient {
824+
/// Notification posted when an auth state event is triggered.
796825
public static let didChangeAuthStateNotification = Notification.Name(
797826
"DID_CHANGE_AUTH_STATE_NOTIFICATION"
798827
)

Sources/GoTrue/GoTrueMFA.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public actor GoTrueMFA {
6666

6767
try await sessionManager.update(response)
6868

69-
await eventEmitter.emit(.mfaChallengeVerified)
69+
await eventEmitter.emit(.mfaChallengeVerified, session: response)
7070

7171
return response
7272
}

Sources/GoTrue/Internal/EventEmitter.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@ import Foundation
22
@_spi(Internal) import _Helpers
33

44
struct EventEmitter: Sendable {
5-
var attachListener: @Sendable () async -> (id: UUID, stream: AsyncStream<AuthChangeEvent>)
6-
var emit: @Sendable (_ event: AuthChangeEvent, _ id: UUID?) async -> Void
5+
var attachListener: @Sendable () async -> (id: UUID, stream: AsyncStream<(event: AuthChangeEvent, session: Session?)>)
6+
var emit: @Sendable (_ event: AuthChangeEvent, _ session: Session?, _ id: UUID?) async -> Void
77
}
88

99
extension EventEmitter {
10-
func emit(_ event: AuthChangeEvent) async {
11-
await emit(event, nil)
10+
func emit(_ event: AuthChangeEvent, session: Session?) async {
11+
await emit(event, session, nil)
1212
}
1313
}
1414

1515
extension EventEmitter {
1616
static var live: Self = {
17-
let continuations = ActorIsolated([UUID: AsyncStream<AuthChangeEvent>.Continuation]())
17+
let continuations = ActorIsolated([UUID: AsyncStream<(event: AuthChangeEvent, session: Session?)>.Continuation]())
1818

1919
return Self(
2020
attachListener: {
2121
let id = UUID()
2222

23-
let (stream, continuation) = AsyncStream<AuthChangeEvent>.makeStream()
23+
let (stream, continuation) = AsyncStream<(event: AuthChangeEvent, session: Session?)>.makeStream()
2424

2525
continuation.onTermination = { [id] _ in
2626
continuations.withValue {
@@ -34,12 +34,16 @@ extension EventEmitter {
3434

3535
return (id, stream)
3636
},
37-
emit: { event, id in
37+
emit: { event, session, id in
38+
NotificationCenter.default.post(
39+
name: GoTrueClient.didChangeAuthStateNotification,
40+
object: nil
41+
)
3842
if let id {
39-
continuations.value[id]?.yield(event)
43+
continuations.value[id]?.yield((event, session))
4044
} else {
4145
for continuation in continuations.value.values {
42-
continuation.yield(event)
46+
continuation.yield((event, session))
4347
}
4448
}
4549
}

Sources/GoTrue/Internal/SessionManager.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@ struct SessionRefresher: Sendable {
77
}
88

99
struct SessionManager: Sendable {
10-
var session: @Sendable () async throws -> Session
10+
var session: @Sendable (_ shouldValidateExpiration: Bool) async throws -> Session
1111
var update: @Sendable (_ session: Session) async throws -> Void
1212
var remove: @Sendable () async -> Void
13+
14+
func session(shouldValidateExpiration: Bool = true) async throws -> Session {
15+
try await session(shouldValidateExpiration)
16+
}
1317
}
1418

1519
extension SessionManager {
1620
static var live: Self = {
1721
let manager = _LiveSessionManager()
1822
return Self(
19-
session: { try await manager.session() },
23+
session: { try await manager.session(shouldValidateExpiration: $0) },
2024
update: { try await manager.update($0) },
2125
remove: { await manager.remove() }
2226
)
@@ -34,7 +38,7 @@ actor _LiveSessionManager {
3438
Dependencies.current.value!.sessionRefresher
3539
}
3640

37-
func session() async throws -> Session {
41+
func session(shouldValidateExpiration: Bool) async throws -> Session {
3842
if let task {
3943
return try await task.value
4044
}
@@ -43,7 +47,7 @@ actor _LiveSessionManager {
4347
throw GoTrueError.sessionNotFound
4448
}
4549

46-
if currentSession.isValid {
50+
if currentSession.isValid || !shouldValidateExpiration {
4751
return currentSession.session
4852
}
4953

Sources/GoTrue/Types.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22

33
public enum AuthChangeEvent: String, Sendable {
4+
case initialSession = "INITIAL_SESSION"
45
case passwordRecovery = "PASSWORD_RECOVERY"
56
case signedIn = "SIGNED_IN"
67
case signedOut = "SIGNED_OUT"

0 commit comments

Comments
 (0)