Skip to content
Open
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
75 changes: 75 additions & 0 deletions Sources/XMTPiOS/Auth.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Foundation

// MARK: - Public Types

public struct Credential {
public let name: Optional<String>
public let value: String
public let expiresAtSeconds: Int64

public init(name: String?, value: String, expiresAtSeconds: Int64) {
self.name = name
self.value = value
self.expiresAtSeconds = expiresAtSeconds
}

public var expiresAt: Date {
Date(timeIntervalSince1970: TimeInterval(expiresAtSeconds))
}
}

extension Credential {
var ffi: FfiCredential {
FfiCredential(
name: name,
value: value,
expiresAtSeconds: expiresAtSeconds
)
}

init(ffi: FfiCredential) {
self.init(
name: ffi.name,
value: ffi.value,
expiresAtSeconds: ffi.expiresAtSeconds
)
}
}

public typealias AuthCallback = @Sendable () async throws -> Credential

public class AuthHandle {
private let ffiHandle: FfiAuthHandle

public init() {
ffiHandle = FfiAuthHandle()
}

public func set(_ credential: Credential) async throws {
try await ffiHandle.set(credential: credential.ffi)
}

var ffi: FfiAuthHandle {
ffiHandle
}
}

// MARK: - Internal Wrappers

private final class InternalAuthCallback: FfiAuthCallback, @unchecked Sendable {
let callback: AuthCallback

init(_ callback: @escaping AuthCallback) {
self.callback = callback
}

func onAuthRequired() async throws -> FfiCredential {
let credential = try await callback()
return credential.ffi
}
}

func makeInternalAuthCallback(_ callback: AuthCallback?) -> FfiAuthCallback? {
guard let callback = callback else { return nil }
return InternalAuthCallback(callback)
}
21 changes: 15 additions & 6 deletions Sources/XMTPiOS/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,24 @@ public struct ClientOptions {
// Future proofing - gateway URL support.
public var gatewayHost: String?

public var authCallback: AuthCallback?

public var authHandle: AuthHandle?

public init(
env: XMTPEnvironment = .dev, isSecure: Bool = true,
env: XMTPEnvironment = .dev,
isSecure: Bool = true,
appVersion: String? = nil,
gatewayHost: String? = nil
gatewayHost: String? = nil,
authCallback: AuthCallback? = nil,
authHandle: AuthHandle? = nil
) {
self.env = env
self.isSecure = isSecure
self.appVersion = appVersion
self.gatewayHost = gatewayHost
self.authCallback = authCallback
self.authHandle = authHandle
}
}

Expand Down Expand Up @@ -462,8 +471,8 @@ public final class Client {
isSecure: api.isSecure,
clientMode: FfiClientMode.default,
appVersion: api.appVersion,
authCallback: nil,
authHandle: nil
authCallback: makeInternalAuthCallback(api.authCallback),
authHandle: api.authHandle?.ffi
)
await apiCache.setClient(newClient, forKey: cacheKey)
return newClient
Expand All @@ -489,8 +498,8 @@ public final class Client {
isSecure: api.isSecure,
clientMode: FfiClientMode.default,
appVersion: api.appVersion,
authCallback: nil,
authHandle: nil
authCallback: makeInternalAuthCallback(api.authCallback),
authHandle: api.authHandle?.ffi
)
await apiCache.setSyncClient(newClient, forKey: cacheKey)
return newClient
Expand Down
76 changes: 76 additions & 0 deletions Tests/XMTPTests/AuthTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import XCTest

@testable import XMTPiOS

final class AuthTests: XCTestCase {
func testCredentialConversion() async throws {
// Test forward conversion
let publicCredential = Credential(
name: "test-name",
value: "test-value",
expiresAtSeconds: 1_234_567_890
)
let ffiCredential = publicCredential.ffi
XCTAssertEqual(ffiCredential.name, "test-name")
XCTAssertEqual(ffiCredential.value, "test-value")
XCTAssertEqual(ffiCredential.expiresAtSeconds, 1_234_567_890)

// Test backward conversion
let backConverted = Credential(ffi: ffiCredential)
XCTAssertEqual(backConverted.name, "test-name")
XCTAssertEqual(backConverted.value, "test-value")
XCTAssertEqual(backConverted.expiresAtSeconds, 1_234_567_890)
XCTAssertEqual(backConverted.expiresAt.timeIntervalSince1970, 1_234_567_890.0, accuracy: 0.1)
}

func testAuthCallback() async throws {
let expectation = XCTestExpectation(description: "AuthCallback invoked")

let testCallback: AuthCallback = {
expectation.fulfill()
return Credential(
name: "dummy-name",
value: "dummy-value",
expiresAtSeconds: Int64(Date().timeIntervalSince1970 + 3600)
)
}

let internalCallback = makeInternalAuthCallback(testCallback)!
let ffiCredential = try await internalCallback.onAuthRequired()

XCTAssertEqual(ffiCredential.name, "dummy-name")
XCTAssertEqual(ffiCredential.value, "dummy-value")

wait(for: [expectation], timeout: 1.0)

// Verify acceptance in connectToApiBackend
let api = ClientOptions.Api(
env: .local,
gatewayHost: "https://gateway.example.com",
authCallback: testCallback
)
let client = try await Client.connectToApiBackend(api: api)
XCTAssertNotNil(client)
}

func testAuthHandleSet() async throws {
let handle = AuthHandle()
let credential = Credential(
name: "handle-name",
value: "handle-value",
expiresAtSeconds: 1_234_567_890
)

// Verify acceptance in connectToApiBackend
let api = ClientOptions.Api(
env: .local,
gatewayHost: "https://gateway.example.com",
authHandle: handle
)
let client = try await Client.connectToApiBackend(api: api)
XCTAssertNotNil(client)

// This should not throw if the wrapper works
try await handle.set(credential)
}
}
Loading