Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions Sources/ConvexMobile/ConvexMobile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -169,11 +169,12 @@ public enum AuthState<T> {
/// 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<T> {
public protocol AuthProvider<T, LoginParams> {
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()``.
Expand All @@ -187,9 +188,9 @@ public protocol AuthProvider<T> {
///
/// The generic parameter `T` matches the type of data returned by the ``AuthProvider`` upon successful
/// authentication.
public class ConvexClientWithAuth<T>: ConvexClient {
public class ConvexClientWithAuth<T, LoginParams>: ConvexClient {
private let authPublisher = CurrentValueSubject<AuthState<T>, Never>(AuthState.unauthenticated)
private let authProvider: any AuthProvider<T>
private let authProvider: any AuthProvider<T, LoginParams>

/// A publisher that updates with the current ``AuthState`` of this client instance.
public let authState: AnyPublisher<AuthState<T>, Never>
Expand All @@ -199,13 +200,13 @@ public class ConvexClientWithAuth<T>: 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<T>) {
public init(deploymentUrl: String, authProvider: any AuthProvider<T, LoginParams>) {
self.authProvider = authProvider
self.authState = authPublisher.eraseToAnyPublisher()
super.init(deploymentUrl: deploymentUrl)
}

init(ffiClient: MobileConvexClientProtocol, authProvider: any AuthProvider<T>) {
init(ffiClient: MobileConvexClientProtocol, authProvider: any AuthProvider<T, LoginParams>) {
self.authProvider = authProvider
self.authState = authPublisher.eraseToAnyPublisher()
super.init(ffiClient: ffiClient)
Expand All @@ -216,8 +217,10 @@ public class ConvexClientWithAuth<T>: 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<T, Error> {
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
Expand All @@ -230,8 +233,8 @@ public class ConvexClientWithAuth<T>: 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<T, Error> {
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``.
Expand All @@ -247,17 +250,17 @@ public class ConvexClientWithAuth<T>: ConvexClient {
}
}

private func login(strategy: LoginStrategy) async -> Result<T, Error> {
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
}
}

Expand Down Expand Up @@ -285,7 +288,15 @@ private class SubscriptionAdapter<T: Decodable>: 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))
)
}
}

}
30 changes: 20 additions & 10 deletions Sources/ConvexMobile/Encoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: ","))]"
}
}