From 036c03d4862bd93f4d93c88f9a365dc292abb74f Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 14 Dec 2023 15:51:01 -0300 Subject: [PATCH] feat: prepare for v2 release (#187) * feat: use nil as default value for configuration params * feat: rename GoTrue to Auth * Deprecate access to default JSONEncoder and JSONDecoder * Refactor default initialization values * Bump version to 2.0.0 * Fix tests --- Package.swift | 15 ++- README.md | 2 +- .../AuthClient.swift} | 114 ++++++++---------- .../AuthError.swift} | 2 +- .../AuthLocalStorage.swift} | 4 +- .../GoTrueMFA.swift => Auth/AuthMFA.swift} | 6 +- Sources/Auth/Defaults.swift | 71 +++++++++++ Sources/Auth/Deprecated.swift | 47 ++++++++ .../{GoTrue => Auth}/Internal/APIClient.swift | 6 +- .../Internal/CodeVerifierStorage.swift | 2 +- .../Internal/Dependencies.swift | 2 +- .../Internal/EventEmitter.swift | 6 +- .../{GoTrue => Auth}/Internal/Helpers.swift | 6 +- Sources/{GoTrue => Auth}/Internal/PKCE.swift | 0 .../Internal/SessionManager.swift | 2 +- .../Internal/SessionStorage.swift | 9 +- Sources/{GoTrue => Auth}/Types.swift | 75 ++---------- Sources/PostgREST/Defaults.swift | 57 +++++++++ Sources/PostgREST/PostgrestClient.swift | 58 +-------- Sources/Supabase/SupabaseClient.swift | 10 +- Sources/Supabase/Types.swift | 34 ++++-- Sources/_Helpers/Version.swift | 2 +- Supabase.xctestplan | 14 +-- .../AuthResponseTests.swift | 2 +- .../GoTrueClientTests.swift | 20 +-- .../{GoTrueTests => AuthTests}/JWTTests.swift | 2 +- .../MockHelpers.swift | 2 +- .../Mocks/Mocks.swift | 24 +++- .../RequestsTests.swift | 17 +-- .../Resources/session.json | 0 .../Resources/signup-response.json | 0 .../Resources/user.json | 0 .../SessionManagerTests.swift | 6 +- .../RequestsTests/testRefreshSession.1.txt | 0 .../testResetPasswordForEmail.1.txt | 0 .../RequestsTests/testSessionFromURL.1.txt | 0 .../testSetSessionWithAExpiredToken.1.txt | 0 ...tSetSessionWithAFutureExpirationDate.1.txt | 0 .../testSignInWithEmailAndPassword.1.txt | 0 .../RequestsTests/testSignInWithIdToken.1.txt | 0 .../testSignInWithOTPUsingEmail.1.txt | 0 .../testSignInWithOTPUsingPhone.1.txt | 0 .../testSignInWithPhoneAndPassword.1.txt | 0 .../RequestsTests/testSignOut.1.txt | 0 .../testSignOutWithLocalScope.1.txt | 0 .../testSignOutWithOthersScope.1.txt | 0 .../testSignUpWithEmailAndPassword.1.txt | 0 .../testSignUpWithPhoneAndPassword.1.txt | 0 .../RequestsTests/testUpdateUser.1.txt | 0 .../testVerifyOTPUsingEmail.1.txt | 0 .../testVerifyOTPUsingPhone.1.txt | 0 Tests/SupabaseTests/SupabaseClientTests.swift | 6 +- .../xcshareddata/swiftpm/Package.resolved | 20 +-- 53 files changed, 379 insertions(+), 264 deletions(-) rename Sources/{GoTrue/GoTrueClient.swift => Auth/AuthClient.swift} (86%) rename Sources/{GoTrue/GoTrueError.swift => Auth/AuthError.swift} (95%) rename Sources/{GoTrue/GoTrueLocalStorage.swift => Auth/AuthLocalStorage.swift} (87%) rename Sources/{GoTrue/GoTrueMFA.swift => Auth/AuthMFA.swift} (97%) create mode 100644 Sources/Auth/Defaults.swift create mode 100644 Sources/Auth/Deprecated.swift rename Sources/{GoTrue => Auth}/Internal/APIClient.swift (88%) rename Sources/{GoTrue => Auth}/Internal/CodeVerifierStorage.swift (94%) rename Sources/{GoTrue => Auth}/Internal/Dependencies.swift (87%) rename Sources/{GoTrue => Auth}/Internal/EventEmitter.swift (88%) rename Sources/{GoTrue => Auth}/Internal/Helpers.swift (94%) rename Sources/{GoTrue => Auth}/Internal/PKCE.swift (100%) rename Sources/{GoTrue => Auth}/Internal/SessionManager.swift (97%) rename Sources/{GoTrue => Auth}/Internal/SessionStorage.swift (80%) rename Sources/{GoTrue => Auth}/Types.swift (87%) create mode 100644 Sources/PostgREST/Defaults.swift rename Tests/{GoTrueTests => AuthTests}/AuthResponseTests.swift (97%) rename Tests/{GoTrueTests => AuthTests}/GoTrueClientTests.swift (90%) rename Tests/{GoTrueTests => AuthTests}/JWTTests.swift (96%) rename Tests/{GoTrueTests => AuthTests}/MockHelpers.swift (92%) rename Tests/{GoTrueTests => AuthTests}/Mocks/Mocks.swift (85%) rename Tests/{GoTrueTests => AuthTests}/RequestsTests.swift (96%) rename Tests/{GoTrueTests => AuthTests}/Resources/session.json (100%) rename Tests/{GoTrueTests => AuthTests}/Resources/signup-response.json (100%) rename Tests/{GoTrueTests => AuthTests}/Resources/user.json (100%) rename Tests/{GoTrueTests => AuthTests}/SessionManagerTests.swift (95%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testRefreshSession.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSessionFromURL.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignOut.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testUpdateUser.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt (100%) rename Tests/{GoTrueTests => AuthTests}/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt (100%) diff --git a/Package.swift b/Package.swift index 45354ee5..3869c1b8 100644 --- a/Package.swift +++ b/Package.swift @@ -15,13 +15,13 @@ let package = Package( ], products: [ .library(name: "Functions", targets: ["Functions"]), - .library(name: "GoTrue", targets: ["GoTrue"]), + .library(name: "Auth", targets: ["Auth"]), .library(name: "PostgREST", targets: ["PostgREST"]), .library(name: "Realtime", targets: ["Realtime"]), .library(name: "Storage", targets: ["Storage"]), .library( name: "Supabase", - targets: ["Supabase", "Functions", "PostgREST", "GoTrue", "Realtime", "Storage"] + targets: ["Supabase", "Functions", "PostgREST", "Auth", "Realtime", "Storage"] ), ], dependencies: [ @@ -46,7 +46,7 @@ let package = Package( ] ), .target( - name: "GoTrue", + name: "Auth", dependencies: [ "_Helpers", .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), @@ -54,12 +54,15 @@ let package = Package( ] ), .testTarget( - name: "GoTrueTests", + name: "AuthTests", dependencies: [ - "GoTrue", + "Auth", .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ], + exclude: [ + "__Snapshots__", + ], resources: [.process("Resources")] ), .target( @@ -93,7 +96,7 @@ let package = Package( name: "Supabase", dependencies: [ .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - "GoTrue", + "Auth", "Storage", "Realtime", "PostgREST", diff --git a/README.md b/README.md index a6b60a27..ba7ba00e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ let package = Package( If you're using Xcode, [use this guide](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) to add `supabase-swift` to your project. Use `https://github.com/supabase-community/supabase-swift.git` for the url when Xcode asks. -If you don't want the full Supabase environment, you can also add individual packages, such as `Functions`, `GoTrue`, `Realtime`, `Storage`, or `PostgREST`. +If you don't want the full Supabase environment, you can also add individual packages, such as `Functions`, `Auth`, `Realtime`, `Storage`, or `PostgREST`. Then you're able to import the package and establish the connection with the database. diff --git a/Sources/GoTrue/GoTrueClient.swift b/Sources/Auth/AuthClient.swift similarity index 86% rename from Sources/GoTrue/GoTrueClient.swift rename to Sources/Auth/AuthClient.swift index eba0a4c1..79a51e85 100644 --- a/Sources/GoTrue/GoTrueClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -5,7 +5,7 @@ import Foundation import FoundationNetworking #endif -public actor GoTrueClient { +public actor AuthClient { /// FetchHandler is a type alias for asynchronous network request handling. public typealias FetchHandler = @Sendable (_ request: URLRequest) async throws -> (Data, URLResponse) @@ -15,45 +15,36 @@ public actor GoTrueClient { public let url: URL public var headers: [String: String] public let flowType: AuthFlowType - public let localStorage: GoTrueLocalStorage + public let localStorage: AuthLocalStorage public let encoder: JSONEncoder public let decoder: JSONDecoder public let fetch: FetchHandler - /// Initializes a GoTrueClient Configuration with optional parameters. + /// Initializes a AuthClient Configuration with optional parameters. /// /// - Parameters: - /// - url: The base URL of the GoTrue server. - /// - headers: (Optional) Custom headers to be included in requests. - /// - flowType: (Optional) The authentication flow type. Default is `.implicit`. - /// - localStorage: (Optional) The storage mechanism for local data. Default is a - /// KeychainLocalStorage. - /// - encoder: (Optional) The JSON encoder to use for encoding requests. - /// - decoder: (Optional) The JSON decoder to use for decoding responses. - /// - fetch: (Optional) The asynchronous fetch handler for network requests. + /// - url: The base URL of the Auth server. + /// - headers: Custom headers to be included in requests. + /// - flowType: The authentication flow type. + /// - localStorage: The storage mechanism for local data. + /// - encoder: The JSON encoder to use for encoding requests. + /// - decoder: The JSON decoder to use for decoding responses. + /// - fetch: The asynchronous fetch handler for network requests. public init( url: URL, headers: [String: String] = [:], - flowType: AuthFlowType = .implicit, - localStorage: GoTrueLocalStorage? = nil, - encoder: JSONEncoder = .goTrue, - decoder: JSONDecoder = .goTrue, + flowType: AuthFlowType = Configuration.defaultFlowType, + localStorage: AuthLocalStorage = Configuration.defaultLocalStorage, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } ) { - var headers = headers - if headers["X-Client-Info"] == nil { - headers["X-Client-Info"] = "gotrue-swift/\(version)" - } + let headers = headers.merging(Configuration.defaultHeaders) { l, _ in l } self.url = url self.headers = headers self.flowType = flowType - self.localStorage = - localStorage - ?? KeychainLocalStorage( - service: "supabase.gotrue.swift", - accessGroup: nil - ) + self.localStorage = localStorage self.encoder = encoder self.decoder = decoder self.fetch = fetch @@ -82,7 +73,7 @@ public actor GoTrueClient { /// Returns the session, refreshing it if necessary. /// - /// If no session can be found, a ``GoTrueError/sessionNotFound`` error is thrown. + /// If no session can be found, a ``AuthError/sessionNotFound`` error is thrown. public var session: Session { get async throws { try await sessionManager.session() @@ -90,26 +81,25 @@ public actor GoTrueClient { } /// Namespace for accessing multi-factor authentication API. - public let mfa: GoTrueMFA + public let mfa: AuthMFA - /// Initializes a GoTrueClient with optional parameters. + /// Initializes a AuthClient with optional parameters. /// /// - Parameters: - /// - url: The base URL of the GoTrue server. - /// - headers: (Optional) Custom headers to be included in requests. - /// - flowType: (Optional) The authentication flow type. Default is `.implicit`. - /// - localStorage: (Optional) The storage mechanism for local data. Default is a - /// KeychainLocalStorage. - /// - encoder: (Optional) The JSON encoder to use for encoding requests. - /// - decoder: (Optional) The JSON decoder to use for decoding responses. - /// - fetch: (Optional) The asynchronous fetch handler for network requests. + /// - url: The base URL of the Auth server. + /// - headers: Custom headers to be included in requests. + /// - flowType: The authentication flow type.. + /// - localStorage: The storage mechanism for local data.. + /// - encoder: The JSON encoder to use for encoding requests. + /// - decoder: The JSON decoder to use for decoding responses. + /// - fetch: The asynchronous fetch handler for network requests. public init( url: URL, headers: [String: String] = [:], - flowType: AuthFlowType = .implicit, - localStorage: GoTrueLocalStorage? = nil, - encoder: JSONEncoder = .goTrue, - decoder: JSONDecoder = .goTrue, + flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, + localStorage: AuthLocalStorage = AuthClient.Configuration.defaultLocalStorage, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } ) { self.init( @@ -125,7 +115,7 @@ public actor GoTrueClient { ) } - /// Initializes a GoTrueClient with a specific configuration. + /// Initializes a AuthClient with a specific configuration. /// /// - Parameters: /// - configuration: The client configuration. @@ -151,7 +141,7 @@ public actor GoTrueClient { eventEmitter: EventEmitter, sessionStorage: SessionStorage ) { - mfa = GoTrueMFA() + mfa = AuthMFA() Dependencies.current.setValue( Dependencies( @@ -213,7 +203,7 @@ public actor GoTrueClient { email: email, password: password, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)), + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), codeChallenge: codeChallenge, codeChallengeMethod: codeChallengeMethod ) @@ -243,7 +233,7 @@ public actor GoTrueClient { password: password, phone: phone, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -359,7 +349,7 @@ public actor GoTrueClient { email: email, createUser: shouldCreateUser, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)), + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), codeChallenge: codeChallenge, codeChallengeMethod: codeChallengeMethod ) @@ -391,7 +381,7 @@ public actor GoTrueClient { phone: phone, createUser: shouldCreateUser, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -401,7 +391,7 @@ public actor GoTrueClient { /// Log in an existing user by exchanging an Auth Code issued during the PKCE flow. public func exchangeCodeForSession(authCode: String) async throws -> Session { guard let codeVerifier = try codeVerifierStorage.getCodeVerifier() else { - throw GoTrueError.pkce(.codeVerifierNotFound) + throw AuthError.pkce(.codeVerifierNotFound) } do { let session: Session = try await api.execute( @@ -482,18 +472,18 @@ public actor GoTrueClient { @discardableResult public func session(from url: URL) async throws -> Session { if configuration.flowType == .implicit, !isImplicitGrantFlow(url: url) { - throw GoTrueError.invalidImplicitGrantFlowURL + throw AuthError.invalidImplicitGrantFlowURL } if configuration.flowType == .pkce, !isPKCEFlow(url: url) { - throw GoTrueError.pkce(.invalidPKCEFlowURL) + throw AuthError.pkce(.invalidPKCEFlowURL) } let params = extractParams(from: url) if isPKCEFlow(url: url) { guard let code = params.first(where: { $0.name == "code" })?.value else { - throw GoTrueError.pkce(.codeVerifierNotFound) + throw AuthError.pkce(.codeVerifierNotFound) } let session = try await exchangeCodeForSession(authCode: code) @@ -501,7 +491,7 @@ public actor GoTrueClient { } if let errorDescription = params.first(where: { $0.name == "error_description" })?.value { - throw GoTrueError.api(.init(errorDescription: errorDescription)) + throw AuthError.api(.init(errorDescription: errorDescription)) } guard @@ -565,7 +555,7 @@ public actor GoTrueClient { expiresAt = Date(timeIntervalSince1970: exp) hasExpired = expiresAt <= now } else { - throw GoTrueError.missingExpClaim + throw AuthError.missingExpClaim } if hasExpired { @@ -607,7 +597,7 @@ public actor GoTrueClient { // ignore 401s since an invalid or expired JWT should sign out the current session let ignoredCodes = Set([404, 401]) - if case let GoTrueError.api(apiError) = error, let code = apiError.code, + if case let AuthError.api(apiError) = error, let code = apiError.code, !ignoredCodes.contains(code) { throw error @@ -642,7 +632,7 @@ public actor GoTrueClient { email: email, token: token, type: type, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -669,7 +659,7 @@ public actor GoTrueClient { phone: phone, token: token, type: type, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)) ) ) ) @@ -755,7 +745,7 @@ public actor GoTrueClient { body: configuration.encoder.encode( RecoverParams( email: email, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)), + gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:)), codeChallenge: codeChallenge, codeChallengeMethod: codeChallengeMethod ) @@ -841,17 +831,17 @@ public actor GoTrueClient { } } -extension GoTrueClient { +extension AuthClient { /// Notification posted when an auth state event is triggered. public static let didChangeAuthStateNotification = Notification.Name( - "GoTrueClient.didChangeAuthStateNotification" + "AuthClient.didChangeAuthStateNotification" ) /// A user info key to retrieve the ``AuthChangeEvent`` value for a - /// ``GoTrueClient/didChangeAuthStateNotification`` notification. - public static let authChangeEventInfoKey = "GoTrueClient.authChangeEvent" + /// ``AuthClient/didChangeAuthStateNotification`` notification. + public static let authChangeEventInfoKey = "AuthClient.authChangeEvent" /// A user info key to retrieve the ``Session`` value for a - /// ``GoTrueClient/didChangeAuthStateNotification`` notification. - public static let authChangeSessionInfoKey = "GoTrueClient.authChangeSession" + /// ``AuthClient/didChangeAuthStateNotification`` notification. + public static let authChangeSessionInfoKey = "AuthClient.authChangeSession" } diff --git a/Sources/GoTrue/GoTrueError.swift b/Sources/Auth/AuthError.swift similarity index 95% rename from Sources/GoTrue/GoTrueError.swift rename to Sources/Auth/AuthError.swift index 7b8673f7..30140c78 100644 --- a/Sources/GoTrue/GoTrueError.swift +++ b/Sources/Auth/AuthError.swift @@ -1,6 +1,6 @@ import Foundation -public enum GoTrueError: LocalizedError, Sendable { +public enum AuthError: LocalizedError, Sendable { case missingExpClaim case malformedJWT case sessionNotFound diff --git a/Sources/GoTrue/GoTrueLocalStorage.swift b/Sources/Auth/AuthLocalStorage.swift similarity index 87% rename from Sources/GoTrue/GoTrueLocalStorage.swift rename to Sources/Auth/AuthLocalStorage.swift index cebd49b6..2b385c3d 100644 --- a/Sources/GoTrue/GoTrueLocalStorage.swift +++ b/Sources/Auth/AuthLocalStorage.swift @@ -1,13 +1,13 @@ import Foundation @preconcurrency import KeychainAccess -public protocol GoTrueLocalStorage: Sendable { +public protocol AuthLocalStorage: Sendable { func store(key: String, value: Data) throws func retrieve(key: String) throws -> Data? func remove(key: String) throws } -struct KeychainLocalStorage: GoTrueLocalStorage { +struct KeychainLocalStorage: AuthLocalStorage { private let keychain: Keychain init(service: String, accessGroup: String?) { diff --git a/Sources/GoTrue/GoTrueMFA.swift b/Sources/Auth/AuthMFA.swift similarity index 97% rename from Sources/GoTrue/GoTrueMFA.swift rename to Sources/Auth/AuthMFA.swift index d7d23654..f5aebef0 100644 --- a/Sources/GoTrue/GoTrueMFA.swift +++ b/Sources/Auth/AuthMFA.swift @@ -2,7 +2,7 @@ import Foundation @_spi(Internal) import _Helpers /// Contains the full multi-factor authentication API. -public actor GoTrueMFA { +public actor AuthMFA { private var api: APIClient { Dependencies.current.value!.api } @@ -11,7 +11,7 @@ public actor GoTrueMFA { Dependencies.current.value!.sessionManager } - private var configuration: GoTrueClient.Configuration { + private var configuration: AuthClient.Configuration { Dependencies.current.value!.configuration } @@ -148,7 +148,7 @@ public actor GoTrueMFA { nextLevel: nextLevel, currentAuthenticationMethods: currentAuthenticationMethods ) - } catch GoTrueError.sessionNotFound { + } catch AuthError.sessionNotFound { return AuthMFAGetAuthenticatorAssuranceLevelResponse( currentLevel: nil, nextLevel: nil, diff --git a/Sources/Auth/Defaults.swift b/Sources/Auth/Defaults.swift new file mode 100644 index 00000000..479f9924 --- /dev/null +++ b/Sources/Auth/Defaults.swift @@ -0,0 +1,71 @@ +// +// Defaults.swift +// +// +// Created by Guilherme Souza on 14/12/23. +// + +@_spi(Internal) import _Helpers +import Foundation + +extension AuthClient.Configuration { + private static let dateFormatterWithFractionalSeconds = { () -> ISO8601DateFormatter in + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + }() + + private static let dateFormatter = { () -> ISO8601DateFormatter in + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + return formatter + }() + + /// The default JSONEncoder instance used by the ``AuthClient``. + public static let jsonEncoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.dateEncodingStrategy = .custom { date, encoder in + var container = encoder.singleValueContainer() + let string = dateFormatter.string(from: date) + try container.encode(string) + } + return encoder + }() + + /// The default JSONDecoder instance used by the ``AuthClient``. + public static let jsonDecoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + + let supportedFormatters = [dateFormatterWithFractionalSeconds, dateFormatter] + + for formatter in supportedFormatters { + if let date = formatter.date(from: string) { + return date + } + } + + throw DecodingError.dataCorruptedError( + in: container, debugDescription: "Invalid date format: \(string)" + ) + } + return decoder + }() + + public static let defaultHeaders: [String: String] = [ + "X-Client-Info": "auth-swift/\(version)", + ] + + /// The default ``AuthFlowType`` used when initializing a ``AuthClient`` instance. + public static let defaultFlowType: AuthFlowType = .pkce + + /// The default ``AuthLocalStorage`` instance used by the ``AuthClient``. + public static let defaultLocalStorage: AuthLocalStorage = KeychainLocalStorage( + service: "supabase.gotrue.swift", + accessGroup: nil + ) +} diff --git a/Sources/Auth/Deprecated.swift b/Sources/Auth/Deprecated.swift new file mode 100644 index 00000000..3e1721a2 --- /dev/null +++ b/Sources/Auth/Deprecated.swift @@ -0,0 +1,47 @@ +// +// Deprecated.swift +// +// +// Created by Guilherme Souza on 14/12/23. +// + +import Foundation + +@available(*, deprecated, renamed: "AuthClient") +public typealias GoTrueClient = AuthClient + +@available(*, deprecated, renamed: "AuthMFA") +public typealias GoTrueMFA = AuthMFA + +@available(*, deprecated, renamed: "AuthLocalStorage") +public typealias GoTrueLocalStorage = AuthLocalStorage + +@available(*, deprecated, renamed: "AuthMetaSecurity") +public typealias GoTrueMetaSecurity = AuthMetaSecurity + +@available(*, deprecated, renamed: "AuthError") +public typealias GoTrueError = AuthError + +extension JSONEncoder { + @available( + *, + deprecated, + renamed: "AuthClient.Configuration.jsonEncoder", + message: "Access to the default JSONEncoder instance moved to AuthClient.Configuration.jsonEncoder" + ) + public static var goTrue: JSONEncoder { + AuthClient.Configuration.jsonEncoder + } +} + +extension JSONDecoder { + @available( + *, + deprecated, + renamed: "AuthClient.Configuration.jsonDecoder", + message: "Access to the default JSONDecoder instance moved to AuthClient.Configuration.jsonDecoder" + ) + public static var goTrue: JSONDecoder { + AuthClient.Configuration.jsonDecoder + } +} diff --git a/Sources/GoTrue/Internal/APIClient.swift b/Sources/Auth/Internal/APIClient.swift similarity index 88% rename from Sources/GoTrue/Internal/APIClient.swift rename to Sources/Auth/Internal/APIClient.swift index ee46deae..9be11c8d 100644 --- a/Sources/GoTrue/Internal/APIClient.swift +++ b/Sources/Auth/Internal/APIClient.swift @@ -7,7 +7,7 @@ struct APIClient: Sendable { extension APIClient { static func live(http: HTTPClient) -> Self { - var configuration: GoTrueClient.Configuration { + var configuration: AuthClient.Configuration { Dependencies.current.value!.configuration } @@ -20,10 +20,10 @@ extension APIClient { guard (200 ..< 300).contains(response.statusCode) else { let apiError = try configuration.decoder.decode( - GoTrueError.APIError.self, + AuthError.APIError.self, from: response.data ) - throw GoTrueError.api(apiError) + throw AuthError.api(apiError) } return response diff --git a/Sources/GoTrue/Internal/CodeVerifierStorage.swift b/Sources/Auth/Internal/CodeVerifierStorage.swift similarity index 94% rename from Sources/GoTrue/Internal/CodeVerifierStorage.swift rename to Sources/Auth/Internal/CodeVerifierStorage.swift index 2958e1ed..9d4a401f 100644 --- a/Sources/GoTrue/Internal/CodeVerifierStorage.swift +++ b/Sources/Auth/Internal/CodeVerifierStorage.swift @@ -9,7 +9,7 @@ struct CodeVerifierStorage: Sendable { extension CodeVerifierStorage { static var live: Self = { - var localStorage: GoTrueLocalStorage { + var localStorage: AuthLocalStorage { Dependencies.current.value!.configuration.localStorage } diff --git a/Sources/GoTrue/Internal/Dependencies.swift b/Sources/Auth/Internal/Dependencies.swift similarity index 87% rename from Sources/GoTrue/Internal/Dependencies.swift rename to Sources/Auth/Internal/Dependencies.swift index 8ee31963..77afe352 100644 --- a/Sources/GoTrue/Internal/Dependencies.swift +++ b/Sources/Auth/Internal/Dependencies.swift @@ -4,7 +4,7 @@ import Foundation struct Dependencies: Sendable { static let current = LockIsolated(Dependencies?.none) - var configuration: GoTrueClient.Configuration + var configuration: AuthClient.Configuration var sessionManager: SessionManager var api: APIClient var eventEmitter: EventEmitter diff --git a/Sources/GoTrue/Internal/EventEmitter.swift b/Sources/Auth/Internal/EventEmitter.swift similarity index 88% rename from Sources/GoTrue/Internal/EventEmitter.swift rename to Sources/Auth/Internal/EventEmitter.swift index 38cc610c..da391e5f 100644 --- a/Sources/GoTrue/Internal/EventEmitter.swift +++ b/Sources/Auth/Internal/EventEmitter.swift @@ -42,11 +42,11 @@ extension EventEmitter { }, emit: { event, session, id in NotificationCenter.default.post( - name: GoTrueClient.didChangeAuthStateNotification, + name: AuthClient.didChangeAuthStateNotification, object: nil, userInfo: [ - GoTrueClient.authChangeEventInfoKey: event, - GoTrueClient.authChangeSessionInfoKey: session as Any, + AuthClient.authChangeEventInfoKey: event, + AuthClient.authChangeSessionInfoKey: session as Any, ] ) if let id { diff --git a/Sources/GoTrue/Internal/Helpers.swift b/Sources/Auth/Internal/Helpers.swift similarity index 94% rename from Sources/GoTrue/Internal/Helpers.swift rename to Sources/Auth/Internal/Helpers.swift index feb9a846..feadea33 100644 --- a/Sources/GoTrue/Internal/Helpers.swift +++ b/Sources/Auth/Internal/Helpers.swift @@ -36,16 +36,16 @@ func extractParams(from fragment: String) -> [(name: String, value: String)] { func decode(jwt: String) throws -> [String: Any] { let parts = jwt.split(separator: ".") guard parts.count == 3 else { - throw GoTrueError.malformedJWT + throw AuthError.malformedJWT } let payload = String(parts[1]) guard let data = base64URLDecode(payload) else { - throw GoTrueError.malformedJWT + throw AuthError.malformedJWT } let json = try JSONSerialization.jsonObject(with: data, options: []) guard let decodedPayload = json as? [String: Any] else { - throw GoTrueError.malformedJWT + throw AuthError.malformedJWT } return decodedPayload } diff --git a/Sources/GoTrue/Internal/PKCE.swift b/Sources/Auth/Internal/PKCE.swift similarity index 100% rename from Sources/GoTrue/Internal/PKCE.swift rename to Sources/Auth/Internal/PKCE.swift diff --git a/Sources/GoTrue/Internal/SessionManager.swift b/Sources/Auth/Internal/SessionManager.swift similarity index 97% rename from Sources/GoTrue/Internal/SessionManager.swift rename to Sources/Auth/Internal/SessionManager.swift index 06b1b090..6e217fef 100644 --- a/Sources/GoTrue/Internal/SessionManager.swift +++ b/Sources/Auth/Internal/SessionManager.swift @@ -44,7 +44,7 @@ actor _LiveSessionManager { } guard let currentSession = try storage.getSession() else { - throw GoTrueError.sessionNotFound + throw AuthError.sessionNotFound } if currentSession.isValid || !shouldValidateExpiration { diff --git a/Sources/GoTrue/Internal/SessionStorage.swift b/Sources/Auth/Internal/SessionStorage.swift similarity index 80% rename from Sources/GoTrue/Internal/SessionStorage.swift rename to Sources/Auth/Internal/SessionStorage.swift index 6af2612c..a421dd82 100644 --- a/Sources/GoTrue/Internal/SessionStorage.swift +++ b/Sources/Auth/Internal/SessionStorage.swift @@ -31,18 +31,21 @@ struct SessionStorage: Sendable { extension SessionStorage { static var live: Self = { - var localStorage: GoTrueLocalStorage { + var localStorage: AuthLocalStorage { Dependencies.current.value!.configuration.localStorage } return Self( getSession: { try localStorage.retrieve(key: "supabase.session").flatMap { - try JSONDecoder.goTrue.decode(StoredSession.self, from: $0) + try AuthClient.Configuration.jsonDecoder.decode(StoredSession.self, from: $0) } }, storeSession: { - try localStorage.store(key: "supabase.session", value: JSONEncoder.goTrue.encode($0)) + try localStorage.store( + key: "supabase.session", + value: AuthClient.Configuration.jsonEncoder.encode($0) + ) }, deleteSession: { try localStorage.remove(key: "supabase.session") diff --git a/Sources/GoTrue/Types.swift b/Sources/Auth/Types.swift similarity index 87% rename from Sources/GoTrue/Types.swift rename to Sources/Auth/Types.swift index 87222ee9..b5a5095f 100644 --- a/Sources/GoTrue/Types.swift +++ b/Sources/Auth/Types.swift @@ -38,7 +38,7 @@ struct SignUpRequest: Codable, Hashable, Sendable { var password: String? var phone: String? var data: [String: AnyJSON]? - var gotrueMetaSecurity: GoTrueMetaSecurity? + var gotrueMetaSecurity: AuthMetaSecurity? var codeChallenge: String? var codeChallengeMethod: String? } @@ -234,14 +234,14 @@ public struct OpenIDConnectCredentials: Codable, Hashable, Sendable { public var nonce: String? /// Verification token received when the user completes the captcha on the site. - public var gotrueMetaSecurity: GoTrueMetaSecurity? + public var gotrueMetaSecurity: AuthMetaSecurity? public init( provider: Provider? = nil, idToken: String, accessToken: String? = nil, nonce: String? = nil, - gotrueMetaSecurity: GoTrueMetaSecurity? = nil + gotrueMetaSecurity: AuthMetaSecurity? = nil ) { self.provider = provider self.idToken = idToken @@ -255,7 +255,7 @@ public struct OpenIDConnectCredentials: Codable, Hashable, Sendable { } } -public struct GoTrueMetaSecurity: Codable, Hashable, Sendable { +public struct AuthMetaSecurity: Codable, Hashable, Sendable { public var captchaToken: String public init(captchaToken: String) { @@ -268,7 +268,7 @@ struct OTPParams: Codable, Hashable, Sendable { var phone: String? var createUser: Bool var data: [String: AnyJSON]? - var gotrueMetaSecurity: GoTrueMetaSecurity? + var gotrueMetaSecurity: AuthMetaSecurity? var codeChallenge: String? var codeChallengeMethod: String? } @@ -292,14 +292,14 @@ struct VerifyEmailOTPParams: Encodable, Hashable, Sendable { var email: String var token: String var type: EmailOTPType - var gotrueMetaSecurity: GoTrueMetaSecurity? + var gotrueMetaSecurity: AuthMetaSecurity? } struct VerifyMobileOTPParams: Encodable, Hashable { var phone: String var token: String var type: MobileOTPType - var gotrueMetaSecurity: GoTrueMetaSecurity? + var gotrueMetaSecurity: AuthMetaSecurity? } public enum MobileOTPType: String, Encodable, Sendable { @@ -389,7 +389,7 @@ public struct UserAttributes: Codable, Hashable, Sendable { struct RecoverParams: Codable, Hashable, Sendable { var email: String - var gotrueMetaSecurity: GoTrueMetaSecurity? + var gotrueMetaSecurity: AuthMetaSecurity? var codeChallenge: String? var codeChallengeMethod: String? } @@ -464,7 +464,7 @@ public struct AuthMFAEnrollResponse: Decodable, Hashable, Sendable { } public struct MFAChallengeParams: Encodable, Hashable { - /// ID of the factor to be challenged. Returned in ``GoTrueMFA/enroll(params:)``. + /// ID of the factor to be challenged. Returned in ``AuthMFA/enroll(params:)``. public let factorId: String public init(factorId: String) { @@ -473,7 +473,7 @@ public struct MFAChallengeParams: Encodable, Hashable { } public struct MFAVerifyParams: Encodable, Hashable { - /// ID of the factor being verified. Returned in ``GoTrueMFA/enroll(params:)``. + /// ID of the factor being verified. Returned in ``AuthMFA/enroll(params:)``. public let factorId: String /// ID of the challenge being verified. Returned in challenge(). @@ -490,7 +490,7 @@ public struct MFAVerifyParams: Encodable, Hashable { } public struct MFAUnenrollParams: Encodable, Hashable, Sendable { - /// ID of the factor to unenroll. Returned in ``GoTrueMFA/enroll(params:)``. + /// ID of the factor to unenroll. Returned in ``AuthMFA/enroll(params:)``. public let factorId: String public init(factorId: String) { @@ -499,7 +499,7 @@ public struct MFAUnenrollParams: Encodable, Hashable, Sendable { } public struct MFAChallengeAndVerifyParams: Encodable, Hashable, Sendable { - /// ID of the factor to be challenged. Returned in ``GoTrueMFA/enroll(params:)``. + /// ID of the factor to be challenged. Returned in ``AuthMFA/enroll(params:)``. public let factorId: String /// Verification code provided by the user. @@ -586,54 +586,3 @@ public enum SignOutScope: String, Sendable { /// session. case others } - -// MARK: - Encodable & Decodable - -private let dateFormatterWithFractionalSeconds = { () -> ISO8601DateFormatter in - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formatter -}() - -private let dateFormatter = { () -> ISO8601DateFormatter in - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - return formatter -}() - -extension JSONDecoder { - public static let goTrue = { () -> JSONDecoder in - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .custom { decoder in - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - - let supportedFormatters = [dateFormatterWithFractionalSeconds, dateFormatter] - - for formatter in supportedFormatters { - if let date = formatter.date(from: string) { - return date - } - } - - throw DecodingError.dataCorruptedError( - in: container, debugDescription: "Invalid date format: \(string)" - ) - } - return decoder - }() -} - -extension JSONEncoder { - public static let goTrue = { () -> JSONEncoder in - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - encoder.dateEncodingStrategy = .custom { date, encoder in - var container = encoder.singleValueContainer() - let string = dateFormatter.string(from: date) - try container.encode(string) - } - return encoder - }() -} diff --git a/Sources/PostgREST/Defaults.swift b/Sources/PostgREST/Defaults.swift new file mode 100644 index 00000000..e8af5208 --- /dev/null +++ b/Sources/PostgREST/Defaults.swift @@ -0,0 +1,57 @@ +// +// Defaults.swift +// +// +// Created by Guilherme Souza on 14/12/23. +// + +import Foundation +@_spi(Internal) import _Helpers + +let version = _Helpers.version + +extension PostgrestClient.Configuration { + private static let supportedDateFormatters: [ISO8601DateFormatter] = [ + { () -> ISO8601DateFormatter in + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + }(), + { () -> ISO8601DateFormatter in + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + return formatter + }(), + ] + + /// The default `JSONDecoder` instance for ``PostgrestClient`` responses. + public static let jsonDecoder = { () -> JSONDecoder in + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + + for formatter in supportedDateFormatters { + if let date = formatter.date(from: string) { + return date + } + } + + throw DecodingError.dataCorruptedError( + in: container, debugDescription: "Invalid date format: \(string)" + ) + } + return decoder + }() + + /// The default `JSONEncoder` instance for ``PostgrestClient`` requests. + public static let jsonEncoder = { () -> JSONEncoder in + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + return encoder + }() + + public static let defaultHeaders: [String: String] = [ + "X-Client-Info": "postgrest-swift/\(version)", + ] +} diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index 8217191f..173ed37b 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -1,8 +1,6 @@ import Foundation @_spi(Internal) import _Helpers -let version = _Helpers.version - /// PostgREST client. public actor PostgrestClient { public typealias FetchHandler = @Sendable (_ request: URLRequest) async throws -> ( @@ -31,8 +29,8 @@ public actor PostgrestClient { schema: String? = nil, headers: [String: String] = [:], fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, - encoder: JSONEncoder = .postgrest, - decoder: JSONDecoder = .postgrest + encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, + decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { self.url = url self.schema = schema @@ -49,9 +47,7 @@ public actor PostgrestClient { /// - Parameter configuration: The configuration for the client. public init(configuration: Configuration) { var configuration = configuration - if configuration.headers["X-Client-Info"] == nil { - configuration.headers["X-Client-Info"] = "postgrest-swift/\(version)" - } + configuration.headers.merge(Configuration.defaultHeaders) { l, _ in l } self.configuration = configuration } @@ -68,8 +64,8 @@ public actor PostgrestClient { schema: String? = nil, headers: [String: String] = [:], fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, - encoder: JSONEncoder = .postgrest, - decoder: JSONDecoder = .postgrest + encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, + decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { self.init( configuration: Configuration( @@ -139,47 +135,3 @@ public actor PostgrestClient { try rpc(fn, params: NoParams(), count: count) } } - -private let supportedDateFormatters: [ISO8601DateFormatter] = [ - { () -> ISO8601DateFormatter in - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formatter - }(), - { () -> ISO8601DateFormatter in - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - return formatter - }(), -] - -extension JSONDecoder { - /// The JSONDecoder instance for PostgREST responses. - public static let postgrest = { () -> JSONDecoder in - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .custom { decoder in - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - - for formatter in supportedDateFormatters { - if let date = formatter.date(from: string) { - return date - } - } - - throw DecodingError.dataCorruptedError( - in: container, debugDescription: "Invalid date format: \(string)" - ) - } - return decoder - }() -} - -extension JSONEncoder { - /// The JSONEncoder instance for PostgREST requests. - public static let postgrest = { () -> JSONEncoder in - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - return encoder - }() -} diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index b8eeef97..d994a5e7 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -1,8 +1,8 @@ import ConcurrencyExtras import Foundation @_spi(Internal) import _Helpers +@_exported import Auth @_exported import Functions -@_exported import GoTrue @_exported import PostgREST @_exported import Realtime @_exported import Storage @@ -20,7 +20,7 @@ public final class SupabaseClient: @unchecked Sendable { /// Supabase Auth allows you to create and manage user sessions for access to data that is secured /// by access policies. - public let auth: GoTrueClient + public let auth: AuthClient /// Database client for Supabase. public private(set) lazy var database = PostgrestClient( @@ -84,13 +84,15 @@ public final class SupabaseClient: @unchecked Sendable { "apikey": supabaseKey, ].merging(options.global.headers) { _, new in new } - auth = GoTrueClient( + auth = AuthClient( url: supabaseURL.appendingPathComponent("/auth/v1"), headers: defaultHeaders, flowType: options.auth.flowType, localStorage: options.auth.storage, + encoder: options.auth.encoder, + decoder: options.auth.decoder, fetch: { - // DON'T use `fetchWithAuth` method within the GoTrueClient as it may cause a deadlock. + // DON'T use `fetchWithAuth` method within the AuthClient as it may cause a deadlock. try await options.global.session.data(for: $0) } ) diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 44fffe51..e163b235 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -1,5 +1,6 @@ +import Auth import Foundation -import GoTrue +import PostgREST public struct SupabaseClientOptions: Sendable { public let db: DatabaseOptions @@ -8,15 +9,19 @@ public struct SupabaseClientOptions: Sendable { public struct DatabaseOptions: Sendable { /// The Postgres schema which your tables belong to. Must be on the list of exposed schemas in - /// Supabase. Defaults to `public`. - public let schema: String + /// Supabase. + public let schema: String? + + /// The JSONEncoder to use when encoding database request objects. public let encoder: JSONEncoder + + /// The JSONDecoder to use when decoding database response objects. public let decoder: JSONDecoder public init( - schema: String = "public", - encoder: JSONEncoder = .postgrest, - decoder: JSONDecoder = .postgrest + schema: String? = nil, + encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, + decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { self.schema = schema self.encoder = encoder @@ -26,15 +31,28 @@ public struct SupabaseClientOptions: Sendable { public struct AuthOptions: Sendable { /// A storage provider. Used to store the logged-in session. - public let storage: GoTrueLocalStorage? + public let storage: AuthLocalStorage /// OAuth flow to use - defaults to PKCE flow. PKCE is recommended for mobile and server-side /// applications. public let flowType: AuthFlowType - public init(storage: GoTrueLocalStorage? = nil, flowType: AuthFlowType = .pkce) { + /// The JSON encoder to use for encoding requests. + public let encoder: JSONEncoder + + /// The JSON decoder to use for decoding responses. + public let decoder: JSONDecoder + + public init( + storage: AuthLocalStorage = AuthClient.Configuration.defaultLocalStorage, + flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder + ) { self.storage = storage self.flowType = flowType + self.encoder = encoder + self.decoder = decoder } } diff --git a/Sources/_Helpers/Version.swift b/Sources/_Helpers/Version.swift index 54ae4c13..f64d1865 100644 --- a/Sources/_Helpers/Version.swift +++ b/Sources/_Helpers/Version.swift @@ -1 +1 @@ -@_spi(Internal) public let version = "1.1.0" +@_spi(Internal) public let version = "2.0.0" diff --git a/Supabase.xctestplan b/Supabase.xctestplan index ce93423d..8423f134 100644 --- a/Supabase.xctestplan +++ b/Supabase.xctestplan @@ -19,13 +19,6 @@ "name" : "FunctionsTests" } }, - { - "target" : { - "containerPath" : "container:", - "identifier" : "GoTrueTests", - "name" : "GoTrueTests" - } - }, { "target" : { "containerPath" : "container:", @@ -60,6 +53,13 @@ "identifier" : "SupabaseTests", "name" : "SupabaseTests" } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "AuthTests", + "name" : "AuthTests" + } } ], "version" : 1 diff --git a/Tests/GoTrueTests/AuthResponseTests.swift b/Tests/AuthTests/AuthResponseTests.swift similarity index 97% rename from Tests/GoTrueTests/AuthResponseTests.swift rename to Tests/AuthTests/AuthResponseTests.swift index 488821d6..d5c2eb06 100644 --- a/Tests/GoTrueTests/AuthResponseTests.swift +++ b/Tests/AuthTests/AuthResponseTests.swift @@ -1,4 +1,4 @@ -import GoTrue +import Auth import SnapshotTesting import XCTest diff --git a/Tests/GoTrueTests/GoTrueClientTests.swift b/Tests/AuthTests/GoTrueClientTests.swift similarity index 90% rename from Tests/GoTrueTests/GoTrueClientTests.swift rename to Tests/AuthTests/GoTrueClientTests.swift index 5d525faf..67830703 100644 --- a/Tests/GoTrueTests/GoTrueClientTests.swift +++ b/Tests/AuthTests/GoTrueClientTests.swift @@ -9,9 +9,9 @@ import XCTest @_spi(Internal) import _Helpers import ConcurrencyExtras -@testable import GoTrue +@testable import Auth -final class GoTrueClientTests: XCTestCase { +final class AuthClientTests: XCTestCase { func testAuthStateChanges() async throws { let session = Session.validSession let sut = makeSUT() @@ -65,7 +65,7 @@ final class GoTrueClientTests: XCTestCase { do { _ = try await sut.session - } catch GoTrueError.sessionNotFound { + } catch AuthError.sessionNotFound { } catch { XCTFail("Unexpected error.") } @@ -96,7 +96,7 @@ final class GoTrueClientTests: XCTestCase { let emitReceivedParams = LockIsolated((AuthChangeEvent, Session?)?.none) try await withDependencies { - $0.api.execute = { _ in throw GoTrueError.api(GoTrueError.APIError(code: 404)) } + $0.api.execute = { _ in throw AuthError.api(AuthError.APIError(code: 404)) } $0.sessionManager = .live $0.sessionStorage = .inMemory $0.eventEmitter.emit = { @Sendable event, session, _ in @@ -106,7 +106,7 @@ final class GoTrueClientTests: XCTestCase { } operation: { do { try await sut.signOut() - } catch GoTrueError.api { + } catch AuthError.api { } catch { XCTFail("Unexpected error: \(error)") } @@ -124,7 +124,7 @@ final class GoTrueClientTests: XCTestCase { let emitReceivedParams = LockIsolated((AuthChangeEvent, Session?)?.none) try await withDependencies { - $0.api.execute = { _ in throw GoTrueError.api(GoTrueError.APIError(code: 401)) } + $0.api.execute = { _ in throw AuthError.api(AuthError.APIError(code: 401)) } $0.sessionManager = .live $0.sessionStorage = .inMemory $0.eventEmitter.emit = { @Sendable event, session, _ in @@ -134,7 +134,7 @@ final class GoTrueClientTests: XCTestCase { } operation: { do { try await sut.signOut() - } catch GoTrueError.api { + } catch AuthError.api { } catch { XCTFail("Unexpected error: \(error)") } @@ -146,13 +146,13 @@ final class GoTrueClientTests: XCTestCase { } } - private func makeSUT() -> GoTrueClient { - let configuration = GoTrueClient.Configuration( + private func makeSUT() -> AuthClient { + let configuration = AuthClient.Configuration( url: clientURL, headers: ["apikey": "dummy.api.key"] ) - let sut = GoTrueClient( + let sut = AuthClient( configuration: configuration, sessionManager: .mock, codeVerifierStorage: .mock, diff --git a/Tests/GoTrueTests/JWTTests.swift b/Tests/AuthTests/JWTTests.swift similarity index 96% rename from Tests/GoTrueTests/JWTTests.swift rename to Tests/AuthTests/JWTTests.swift index 86a13239..e8157d54 100644 --- a/Tests/GoTrueTests/JWTTests.swift +++ b/Tests/AuthTests/JWTTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import GoTrue +@testable import Auth final class JWTTests: XCTestCase { func testDecodeJWT() throws { diff --git a/Tests/GoTrueTests/MockHelpers.swift b/Tests/AuthTests/MockHelpers.swift similarity index 92% rename from Tests/GoTrueTests/MockHelpers.swift rename to Tests/AuthTests/MockHelpers.swift index 922fc0ef..70c6d5fa 100644 --- a/Tests/GoTrueTests/MockHelpers.swift +++ b/Tests/AuthTests/MockHelpers.swift @@ -1,6 +1,6 @@ import Foundation -@testable import GoTrue +@testable import Auth func json(named name: String) -> Data { let url = Bundle.module.url(forResource: name, withExtension: "json") diff --git a/Tests/GoTrueTests/Mocks/Mocks.swift b/Tests/AuthTests/Mocks/Mocks.swift similarity index 85% rename from Tests/GoTrueTests/Mocks/Mocks.swift rename to Tests/AuthTests/Mocks/Mocks.swift index 8167c196..84ba62fe 100644 --- a/Tests/GoTrueTests/Mocks/Mocks.swift +++ b/Tests/AuthTests/Mocks/Mocks.swift @@ -10,7 +10,7 @@ import Foundation import XCTestDynamicOverlay @_spi(Internal) import _Helpers -@testable import GoTrue +@testable import Auth let clientURL = URL(string: "http://localhost:54321/auth/v1")! @@ -70,7 +70,7 @@ extension APIClient { extension Dependencies { static let mock = Dependencies( - configuration: GoTrueClient.Configuration(url: clientURL), + configuration: AuthClient.Configuration(url: clientURL), sessionManager: .mock, api: .mock, eventEmitter: .mock, @@ -109,3 +109,23 @@ extension Session { user: User(fromMockNamed: "user") ) } + +final class InMemoryLocalStorage: AuthLocalStorage, @unchecked Sendable { + let storage = LockIsolated([String: Data]()) + + func store(key: String, value: Data) throws { + storage.withValue { + $0[key] = value + } + } + + func retrieve(key: String) throws -> Data? { + storage.value[key] + } + + func remove(key: String) throws { + storage.withValue { + $0[key] = nil + } + } +} diff --git a/Tests/GoTrueTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift similarity index 96% rename from Tests/GoTrueTests/RequestsTests.swift rename to Tests/AuthTests/RequestsTests.swift index 826f5a47..7a08761d 100644 --- a/Tests/GoTrueTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -9,7 +9,7 @@ import SnapshotTesting import XCTest @_spi(Internal) import _Helpers -@testable import GoTrue +@testable import Auth struct UnimplementedError: Error {} @@ -92,7 +92,7 @@ final class RequestsTests: XCTestCase { idToken: "id-token", accessToken: "access-token", nonce: "nonce", - gotrueMetaSecurity: GoTrueMetaSecurity( + gotrueMetaSecurity: AuthMetaSecurity( captchaToken: "captcha-token" ) ) @@ -355,17 +355,20 @@ final class RequestsTests: XCTestCase { private func makeSUT( record: Bool = false, - fetch: GoTrueClient.FetchHandler? = nil, + flowType: AuthFlowType = .implicit, + fetch: AuthClient.FetchHandler? = nil, file: StaticString = #file, testName: String = #function, line: UInt = #line - ) -> GoTrueClient { - let encoder = JSONEncoder.goTrue + ) -> AuthClient { + let encoder = AuthClient.Configuration.jsonEncoder encoder.outputFormatting = .sortedKeys - let configuration = GoTrueClient.Configuration( + let configuration = AuthClient.Configuration( url: clientURL, headers: ["apikey": "dummy.api.key", "X-Client-Info": "gotrue-swift/x.y.z"], + flowType: flowType, + localStorage: InMemoryLocalStorage(), encoder: encoder, fetch: { request in DispatchQueue.main.sync { @@ -384,7 +387,7 @@ final class RequestsTests: XCTestCase { let api = APIClient.live(http: HTTPClient(fetchHandler: configuration.fetch)) - return GoTrueClient( + return AuthClient( configuration: configuration, sessionManager: .mock, codeVerifierStorage: .mock, diff --git a/Tests/GoTrueTests/Resources/session.json b/Tests/AuthTests/Resources/session.json similarity index 100% rename from Tests/GoTrueTests/Resources/session.json rename to Tests/AuthTests/Resources/session.json diff --git a/Tests/GoTrueTests/Resources/signup-response.json b/Tests/AuthTests/Resources/signup-response.json similarity index 100% rename from Tests/GoTrueTests/Resources/signup-response.json rename to Tests/AuthTests/Resources/signup-response.json diff --git a/Tests/GoTrueTests/Resources/user.json b/Tests/AuthTests/Resources/user.json similarity index 100% rename from Tests/GoTrueTests/Resources/user.json rename to Tests/AuthTests/Resources/user.json diff --git a/Tests/GoTrueTests/SessionManagerTests.swift b/Tests/AuthTests/SessionManagerTests.swift similarity index 95% rename from Tests/GoTrueTests/SessionManagerTests.swift rename to Tests/AuthTests/SessionManagerTests.swift index 87ecb5a3..78f328d8 100644 --- a/Tests/GoTrueTests/SessionManagerTests.swift +++ b/Tests/AuthTests/SessionManagerTests.swift @@ -10,7 +10,7 @@ import XCTestDynamicOverlay @_spi(Internal) import _Helpers import ConcurrencyExtras -@testable import GoTrue +@testable import Auth final class SessionManagerTests: XCTestCase { override func setUp() { @@ -27,8 +27,8 @@ final class SessionManagerTests: XCTestCase { do { _ = try await sut.session() - XCTFail("Expected a \(GoTrueError.sessionNotFound) failure") - } catch GoTrueError.sessionNotFound { + XCTFail("Expected a \(AuthError.sessionNotFound) failure") + } catch AuthError.sessionNotFound { } catch { XCTFail("Unexpected error \(error)") } diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignOut.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignOut.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt diff --git a/Tests/GoTrueTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt similarity index 100% rename from Tests/GoTrueTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt rename to Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt diff --git a/Tests/SupabaseTests/SupabaseClientTests.swift b/Tests/SupabaseTests/SupabaseClientTests.swift index 2d0e9cd7..597066ab 100644 --- a/Tests/SupabaseTests/SupabaseClientTests.swift +++ b/Tests/SupabaseTests/SupabaseClientTests.swift @@ -1,9 +1,9 @@ -import GoTrue +import Auth import XCTest @testable import Supabase -final class GoTrueLocalStorageMock: GoTrueLocalStorage { +final class AuthLocalStorageMock: AuthLocalStorage { func store(key _: String, value _: Data) throws {} func retrieve(key _: String) throws -> Data? { @@ -16,7 +16,7 @@ final class GoTrueLocalStorageMock: GoTrueLocalStorage { final class SupabaseClientTests: XCTestCase { func testClientInitialization() { let customSchema = "custom_schema" - let localStorage = GoTrueLocalStorageMock() + let localStorage = AuthLocalStorageMock() let customHeaders = ["header_field": "header_value"] let client = SupabaseClient( diff --git a/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved b/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved index 45a1905b..5c238176 100644 --- a/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version" : "1.0.0" + "revision" : "bba1111185863c9288c5f047770f421c3b7793a4", + "version" : "1.1.3" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "3efbfba0e4e56c7187cc19137ee16b7c95346b79", - "version" : "1.1.0" + "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", + "version" : "1.1.2" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4", - "version" : "1.14.2" + "revision" : "59b663f68e69f27a87b45de48cb63264b8194605", + "version" : "1.15.1" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", - "version" : "509.0.0" + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation.git", "state" : { - "revision" : "6eb293c49505d86e9e24232cb6af6be7fff93bd5", - "version" : "1.0.2" + "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", + "version" : "1.2.0" } }, {