// // UserManager.swift // HabitPactV0 // // The pane of glass rest of app uses to figure out the current user // Rest of app should only get user id from here and use that to make queries // import Clerk import Combine import ConvexMobile import SwiftUI @MainActor class UserManager: ObservableObject { // MARK: Data Shared with Me @Published var clerkUser: User? { didSet { if clerkUser == nil { error = nil currentUser = nil isCreatingProfile = false profileCreationError = nil subscriptionsInitialized = false cancellables.removeAll() } else { // _ = await ConvexClientManager.shared.client.login() // Monitor auth state and setup subscriptions only after authentication var bag = Set() ConvexClientManager.shared.client.authState .sink { authState in switch authState { case .authenticated(_): print("Convex authenticated, setting up subscriptions") case .unauthenticated: print("Convex unauthenticated") case .loading: print("Convex authentication loading") } } .store(in: &bag) print("UserManager didSet: clerkUser is not nil") print("isLoadingUser: \(isLoadingUser)") print("error: \(error)") print("currentUser: \(String(describing: currentUser?.id))") print("isCreatingProfile: \(isCreatingProfile)") print("profileCreationError: \(profileCreationError)") print("subscriptionsInitialized: \(subscriptionsInitialized)") print("cancellables: \(cancellables)") } } } private let convex = ConvexClientManager.shared.client //MARK: - Data Owned by Me @Published var isLoadingUser = false @Published var error: String? @Published var currentUser: Account? @Published var isCreatingProfile = false @Published var profileCreationError: String? private var subscriptionsInitialized = false private var cancellables = Set() init() { print("UserManager initialized") } deinit { print("UserManager deinitialized") cancellables.removeAll() } func setupUserSubscription() { guard let clerkUser = clerkUser else { print("No clerk user available") currentUser = nil isLoadingUser = false return } guard !subscriptionsInitialized else { print("User subscription already initialized, skipping") return } // subscriptionsInitialized = true // isLoadingUser = true //TODO: race condition with subsequent subscriptions with the same clerkId // curb by holding on to last clerk id and last subscription and only cancel // if the clerkId is different print("UserManager setupUserSubscription: subscribing to user: \(clerkUser.id)") convex.subscribe( to: "users:getUserByClerkId", with: ["clerkId": clerkUser.id] ) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { [weak self] completion in print("UserManager setupUserSubscription: receiveCompletion: \(completion)") if case .failure(let error) = completion { print("Error subscribing to user: \(error)") self?.error = error.localizedDescription self?.currentUser = nil self?.isLoadingUser = false } }, receiveValue: { [weak self] (user: Account?) in print("UserManager setupUserSubscription: received user: \(String(describing: user?.id))") if var user = user { // Mark as current user for UI indicators user.isCurrentUser = true self?.currentUser = user } else { self?.currentUser = nil } self?.isLoadingUser = false } ) .store(in: &cancellables) } // Single source of truth for authentication status var isAuth: Bool { currentUser != nil } func refreshUser() { // Reset and reinitialize subscription subscriptionsInitialized = false cancellables.removeAll() isLoadingUser = true setupUserSubscription() } func createOrUpdateProfile(name: String, profileDescription: String) async -> Bool { guard let clerkUser = clerkUser else { profileCreationError = "No authenticated user found" return false } isCreatingProfile = true profileCreationError = nil do { let _ = try await convex.mutation( "users:createOrUpdateProfile", with: [ "clerkId": clerkUser.id, "name": name.trimmingCharacters(in: .whitespacesAndNewlines), "profileDescription": profileDescription.trimmingCharacters( in: .whitespacesAndNewlines), ] ) isCreatingProfile = false refreshUser() return true } catch { profileCreationError = error.localizedDescription isCreatingProfile = false return false } } func logout() async { // Clear local state isLoadingUser = true clerkUser = nil subscriptionsInitialized = false currentUser = nil error = nil profileCreationError = nil // Cancel subscriptions cancellables.removeAll() // Logout from Convex await convex.logout() var bag = Set() ConvexClientManager.shared.client.authState .sink { authState in switch authState { case .authenticated(_): print("Convex authenticated, setting up subscriptions") case .unauthenticated: print("Convex unauthenticated") self.isLoadingUser = false case .loading: print("Convex authentication loading") } } .store(in: &bag) } }