Jun 24 - Implement device registry

This commit is contained in:
Nguyen Ngo
2026-06-25 10:26:22 -04:00
parent e6de1f01b6
commit c05f0029fb
4 changed files with 85 additions and 0 deletions
+68
View File
@@ -2,6 +2,7 @@
import Foundation
import Combine
import UIKit
enum APIError: Error, LocalizedError, Sendable {
case invalidURL
@@ -371,6 +372,73 @@ actor APIClient {
return try await request("/api/v1/stats/dashboard")
}
// Device Registration
// Called on every app foreground (active scenePhase) when authenticated.
// Upserts a device_registrations row on the server so the admin can see
// all installed devices and their versions.
// Errors are suppressed device registration is best-effort and must
// never block the normal app launch flow.
/// Returns or creates a stable device UUID, persisted in Keychain so it
/// survives app restarts but is unique per physical device.
nonisolated static func stableDeviceId() -> String {
if let existing = KeychainHelper.get(Constants.Keychain.deviceId) {
return existing
}
let new = UUID().uuidString
KeychainHelper.set(new, forKey: Constants.Keychain.deviceId)
return new
}
func registerDevice() async {
guard KeychainHelper.get(Constants.Keychain.accessToken) != nil else { return }
let deviceId = Self.stableDeviceId()
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
// UIDevice.current is @MainActor read on MainActor then pass as plain Strings
let (deviceName, iosVersion): (String, String) = await MainActor.run {
(UIDevice.current.name, UIDevice.current.systemVersion)
}
let body: [String: Any] = [
"device_id": deviceId,
"device_name": deviceName,
"app_version": appVersion,
"ios_version": iosVersion,
]
do {
// nonisolated init required SWIFT_DEFAULT_ACTOR_ISOLATION=MainActor
// taints synthesised Decodable inits (CLAUDE.md rule 29).
struct R: Decodable, Sendable {
let registered: Bool
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
registered = try c.decode(Bool.self, forKey: .registered)
}
private enum CodingKeys: String, CodingKey { case registered }
}
let _: R = try await request("/api/v1/devices/register", method: "POST", body: body)
print("[JQC] registerDevice succeeded")
} catch {
// Log raw response to diagnose server-side failures
if let url = URL(string: ServerConfig.current + "/api/v1/devices/register"),
let token = KeychainHelper.get(Constants.Keychain.accessToken) {
var req = URLRequest(url: url)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
req.httpBody = try? JSONSerialization.data(withJSONObject: body)
if let (data, resp) = try? await URLSession.shared.data(for: req) {
let status = (resp as? HTTPURLResponse)?.statusCode ?? 0
let raw = String(data: data, encoding: .utf8) ?? "<binary>"
print("[JQC] registerDevice HTTP \(status): \(raw)")
}
}
print("[JQC] registerDevice error: \(error)")
}
}
// Issue Comments (Phase D)
/// Fetch all comments for an issue, oldest-first.