fix(conversation-assistant): add missing API and view files

- Add checkVerification method to APIClient
- Update MenuBarView with registrationCode binding

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-29 00:12:25 -08:00
parent a67a2cc110
commit 190802c6f1
2 changed files with 111 additions and 61 deletions

View file

@ -20,6 +20,8 @@ class APIClient {
// Load saved auth
authToken = KeychainHelper.load(key: "authToken")
deviceId = UserDefaults.standard.string(forKey: "deviceId")
NSLog("APIClient: init - baseURL: \(baseURL), deviceId: \(deviceId ?? "nil"), authToken: \(authToken != nil ? "present" : "nil")")
}
func registerDevice() async throws -> (deviceId: String, code: String) {
@ -55,42 +57,39 @@ class APIClient {
return (id, code)
}
func verifyDevice(code: String) async throws {
let currentDeviceId: String
if let existingId = deviceId {
currentDeviceId = existingId
} else {
// Register first if no device ID
let (newId, _) = try await registerDevice()
self.deviceId = newId
currentDeviceId = newId
func checkVerification() async throws -> Bool {
guard let currentDeviceId = deviceId else {
NSLog("APIClient: checkVerification - no deviceId")
return false
}
let params: [String: String] = [
"deviceId": currentDeviceId,
"code": code
]
let url = "\(baseURL)/api/devices/\(currentDeviceId)/status"
NSLog("APIClient: checkVerification - calling \(url)")
let response = try await AF.request(
"\(baseURL)/api/devices/verify",
method: .post,
parameters: params,
encoding: JSONEncoding.default
url,
method: .get
).serializingData().value
let json = try JSON(data: response)
NSLog("APIClient: checkVerification - response: \(json)")
guard json["success"].boolValue else {
throw APIError.requestFailed(json["error"]["message"].stringValue)
if json["success"].boolValue && json["data"]["isActive"].boolValue {
// Device is verified - get auth token
if let token = json["data"]["token"].string {
NSLog("APIClient: checkVerification - got token, saving to keychain")
authToken = token
KeychainHelper.save(key: "authToken", value: token)
}
return true
}
let token = json["data"]["token"].stringValue
authToken = token
KeychainHelper.save(key: "authToken", value: token)
NSLog("APIClient: checkVerification - not active")
return false
}
func syncMessages(_ data: SyncMessagesPayload) async throws -> Int {
guard let token = authToken else {
NSLog("APIClient: syncMessages - no auth token!")
throw APIError.notAuthenticated
}
@ -98,21 +97,34 @@ class APIClient {
"Authorization": "Bearer \(token)"
]
let response = try await AF.request(
"\(baseURL)/api/sync/messages",
method: .post,
parameters: data.dictionary,
encoding: JSONEncoding.default,
headers: headers
).serializingData().value
let url = "\(baseURL)/api/sync/messages"
NSLog("APIClient: syncMessages - calling \(url) with \(data.messages.count) messages")
let json = try JSON(data: response)
do {
let response = try await AF.request(
url,
method: .post,
parameters: data.dictionary,
encoding: JSONEncoding.default,
headers: headers
).serializingData().value
guard json["success"].boolValue else {
throw APIError.requestFailed(json["error"]["message"].stringValue)
let json = try JSON(data: response)
NSLog("APIClient: syncMessages - response: \(json)")
guard json["success"].boolValue else {
let errorMsg = json["error"]["message"].stringValue
let errorCode = json["statusCode"].intValue
NSLog("APIClient: syncMessages - API error: \(errorMsg) (code: \(errorCode))")
throw APIError.requestFailed(errorMsg.isEmpty ? "Unknown API error" : errorMsg)
}
return json["data"]["synced"].intValue
} catch {
NSLog("APIClient: syncMessages - Error: \(error)")
NSLog("APIClient: syncMessages - Error type: \(type(of: error))")
throw error
}
return json["data"]["synced"].intValue
}
private func getHardwareId() -> String {
@ -198,27 +210,51 @@ enum APIError: LocalizedError {
}
class KeychainHelper {
private static let service = "com.lilith.conversation-assistant"
static func save(key: String, value: String) {
let data = value.data(using: .utf8)!
let query: [String: Any] = [
// First try to delete any existing item
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
NSLog("KeychainHelper: delete status for \(key): \(deleteStatus)")
// Now add the new item
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
NSLog("KeychainHelper: add status for \(key): \(addStatus)")
if addStatus != errSecSuccess {
NSLog("KeychainHelper: Failed to save \(key) - error: \(addStatus)")
}
}
static func load(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
guard let data = result as? Data else { return nil }
let status = SecItemCopyMatching(query as CFDictionary, &result)
NSLog("KeychainHelper: load status for \(key): \(status)")
guard status == errSecSuccess, let data = result as? Data else {
return nil
}
return String(data: data, encoding: .utf8)
}
}

View file

@ -88,33 +88,47 @@ struct MenuBarView: View {
private var authenticationView: some View {
VStack(spacing: 16) {
Image(systemName: "lock.shield")
Image(systemName: "link.badge.plus")
.font(.system(size: 48))
.foregroundColor(.secondary)
.foregroundColor(.accentColor)
Text("Not Connected")
Text("Connect This Device")
.font(.headline)
Text("Enter the code shown in your admin panel to connect this device.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
if viewModel.isRegistering {
ProgressView("Registering...")
} else if !viewModel.registrationCode.isEmpty {
Text("Enter this code in your admin panel:")
.font(.caption)
.foregroundColor(.secondary)
TextField("Device Code", text: $viewModel.authCode)
.textFieldStyle(.roundedBorder)
Button(action: viewModel.authenticate) {
HStack {
if viewModel.isAuthenticating {
ProgressView()
.scaleEffect(0.7)
Text(viewModel.registrationCode)
.font(.system(size: 32, weight: .bold, design: .monospaced))
.foregroundColor(.accentColor)
.padding(.vertical, 8)
.onTapGesture {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(viewModel.registrationCode, forType: .string)
}
Text("Connect")
Text("Tap to copy • Waiting for verification...")
.font(.caption2)
.foregroundColor(.secondary)
Button("Get New Code") {
viewModel.refreshCode()
}
.frame(maxWidth: .infinity)
.buttonStyle(.link)
} else {
Text("Unable to connect to server")
.font(.caption)
.foregroundColor(.secondary)
Button("Retry") {
viewModel.registerDevice()
}
.buttonStyle(.borderedProminent)
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.authCode.isEmpty || viewModel.isAuthenticating)
if let error = viewModel.authError {
Text(error)