diff --git a/Sources/ConvexMobile/ConvexMobile.swift b/Sources/ConvexMobile/ConvexMobile.swift index 4a30dad..7da665e 100644 --- a/Sources/ConvexMobile/ConvexMobile.swift +++ b/Sources/ConvexMobile/ConvexMobile.swift @@ -147,7 +147,7 @@ public class ConvexClient { args?.mapValues({ v in try v?.convexEncode() ?? "null" }) ?? [:]) - return try! JSONDecoder().decode(T.self, from: Data(rawResult.utf8)) + return try JSONDecoder().decode(T.self, from: Data(rawResult.utf8)) } typealias RemoteCall = (String, [String: String]) async throws -> String @@ -169,11 +169,12 @@ public enum AuthState { /// An authentication provider, used with ``ConvexClientWithAuth``. /// /// The generic type `T` is the data returned by the provider upon a successful authentication attempt. -public protocol AuthProvider { +public protocol AuthProvider { associatedtype T + associatedtype LoginParams /// Trigger a login flow, which might launch a new UI/screen. - func login() async throws -> T + func login(with loginParams: LoginParams) async throws -> T /// Trigger a logout flow, which might launch a new UI/screen. func logout() async throws /// Trigger a cached, UI-less re-authentication ussing stored credentials from a previous ``login()``. @@ -187,9 +188,9 @@ public protocol AuthProvider { /// /// The generic parameter `T` matches the type of data returned by the ``AuthProvider`` upon successful /// authentication. -public class ConvexClientWithAuth: ConvexClient { +public class ConvexClientWithAuth: ConvexClient { private let authPublisher = CurrentValueSubject, Never>(AuthState.unauthenticated) - private let authProvider: any AuthProvider + private let authProvider: any AuthProvider /// A publisher that updates with the current ``AuthState`` of this client instance. public let authState: AnyPublisher, Never> @@ -199,13 +200,13 @@ public class ConvexClientWithAuth: ConvexClient { /// - Parameters: /// - deploymentUrl: The Convex backend URL to connect to; find it in the [dashboard](https://dashboard.convex.dev) Settings for your project /// - authProvider: An instance that will handle the actual authentication duties. - public init(deploymentUrl: String, authProvider: any AuthProvider) { + public init(deploymentUrl: String, authProvider: any AuthProvider) { self.authProvider = authProvider self.authState = authPublisher.eraseToAnyPublisher() super.init(deploymentUrl: deploymentUrl) } - init(ffiClient: MobileConvexClientProtocol, authProvider: any AuthProvider) { + init(ffiClient: MobileConvexClientProtocol, authProvider: any AuthProvider) { self.authProvider = authProvider self.authState = authPublisher.eraseToAnyPublisher() super.init(ffiClient: ffiClient) @@ -216,8 +217,10 @@ public class ConvexClientWithAuth: ConvexClient { /// The ``authState`` is set to ``AuthState.loading`` immediately upon calling this method and /// will change to either ``AuthState.authenticated`` or ``AuthState.unauthenticated`` /// depending on the result. - public func login() async -> Result { - await login(strategy: authProvider.login) + public func login(with loginParams: LoginParams) async throws -> T { + try await login { + try await authProvider.login(with: loginParams) + } } /// Triggers a cached, UI-less re-authentication flow using previously stored credentials and updates the @@ -230,8 +233,8 @@ public class ConvexClientWithAuth: ConvexClient { /// The ``authState`` is set to ``AuthState.loading`` immediately upon calling this method and /// will change to either ``AuthState.authenticated`` or ``AuthState.unauthenticated`` /// depending on the result. - public func loginFromCache() async -> Result { - await login(strategy: authProvider.loginFromCache) + public func loginFromCache() async throws -> T { + try await login(strategy: authProvider.loginFromCache) } /// Triggers a logout flow and updates the ``authState``. @@ -247,17 +250,17 @@ public class ConvexClientWithAuth: ConvexClient { } } - private func login(strategy: LoginStrategy) async -> Result { + private func login(strategy: LoginStrategy) async throws -> T { authPublisher.send(AuthState.loading) do { let authData = try await strategy() try await ffiClient.setAuth(token: authProvider.extractIdToken(from: authData)) authPublisher.send(AuthState.authenticated(authData)) - return Result.success(authData) + return authData } catch { dump(error) authPublisher.send(AuthState.unauthenticated) - return Result.failure(error) + throw error } } @@ -285,7 +288,15 @@ private class SubscriptionAdapter: QuerySubscriber { } func onUpdate(value: String) { - publisher.send(try! JSONDecoder().decode(Publisher.Output.self, from: Data(value.utf8))) - } + do { + let output = try JSONDecoder().decode(Publisher.Output.self, from: Data(value.utf8)) + publisher.send(output) + } catch { + publisher.send( + completion: .failure(ClientError.DecodingError(msg: error.localizedDescription)) + ) + } + } + } diff --git a/Sources/ConvexMobile/Encoding.swift b/Sources/ConvexMobile/Encoding.swift index 08f630d..27bba0d 100644 --- a/Sources/ConvexMobile/Encoding.swift +++ b/Sources/ConvexMobile/Encoding.swift @@ -48,23 +48,33 @@ extension Float: ConvexEncodable {} extension Double: ConvexEncodable {} extension Bool: ConvexEncodable {} extension String: ConvexEncodable {} -extension [String: ConvexEncodable?]: ConvexEncodable { +extension Optional: ConvexEncodable where Wrapped: ConvexEncodable { + public func convexEncode() throws -> String { + switch self { + case .none: + "null" + case .some(let wrapped): + try wrapped.convexEncode() + } + } +} +extension Dictionary: ConvexEncodable where Key == String, Value == ConvexEncodable { public func convexEncode() throws -> String { var kvPairs: [String] = [] - for key in self.keys.sorted() { + for key in self.keys { let value = self[key] - let encodedValue = try value??.convexEncode() ?? "null" + let encodedValue = try value?.convexEncode() kvPairs.append("\"\(key)\":\(encodedValue)") } return "{\(kvPairs.joined(separator: ","))}" } } -extension [ConvexEncodable?]: ConvexEncodable { - public func convexEncode() throws -> String { - var encodedValues: [String] = [] - for value in self { - encodedValues.append(try value?.convexEncode() ?? "null") +extension Array: ConvexEncodable where Element: ConvexEncodable { + public func convexEncode() throws -> String { + var encodedValues: [String] = [] + for value in self { + encodedValues.append(try value.convexEncode()) + } + return "[\(encodedValues.joined(separator: ","))]" } - return "[\(encodedValues.joined(separator: ","))]" - } }