Files
honeyDueKMP/iosApp/iosApp/Login/LoginViewModel.swift
Trey t a2295672b9 Add comprehensive accessibility identifiers for UI testing
Added AccessibilityIdentifiers helper struct with identifiers for all major
UI elements across the app. Applied identifiers throughout authentication,
navigation, forms, and feature screens to enable reliable UI testing.

Changes:
- Added Helpers/AccessibilityIdentifiers.swift with centralized ID definitions
- LoginView: Added identifiers for username, password, login button fields
- RegisterView: Added identifiers for registration form fields
- MainTabView: Added identifiers for all tab bar items
- ProfileTabView: Added identifiers for logout and settings buttons
- ResidencesListView: Added identifier for add button
- Task views: Added identifiers for add, save, and form fields
- Document forms: Added identifiers for form fields and buttons

Identifiers follow naming pattern: [Feature].[Element]
Example: AccessibilityIdentifiers.Authentication.loginButton

This enables UI tests to reliably locate elements using:
app.buttons[AccessibilityIdentifiers.Authentication.loginButton]

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 23:06:31 -06:00

225 lines
8.0 KiB
Swift

import Foundation
import ComposeApp
import Combine
@MainActor
class LoginViewModel: ObservableObject {
// MARK: - Published Properties
@Published var username: String = ""
@Published var password: String = ""
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var isVerified: Bool = false
@Published var currentUser: User?
// MARK: - Private Properties
private let sharedViewModel: ComposeApp.AuthViewModel
private let tokenStorage: TokenStorage
private var cancellables = Set<AnyCancellable>()
// Callback for successful login
var onLoginSuccess: ((Bool) -> Void)?
// MARK: - Initialization
init() {
self.sharedViewModel = ComposeApp.AuthViewModel()
self.tokenStorage = TokenStorage.shared
}
// MARK: - Public Methods
func login() {
guard !username.isEmpty else {
errorMessage = "Username is required"
return
}
guard !password.isEmpty else {
errorMessage = "Password is required"
return
}
isLoading = true
errorMessage = nil
sharedViewModel.login(username: username, password: password)
Task {
for await state in sharedViewModel.loginState {
if state is ApiResultLoading {
await MainActor.run {
self.isLoading = true
}
} else if let success = state as? ApiResultSuccess<AuthResponse> {
await MainActor.run {
if let token = success.data?.token,
let user = success.data?.user {
self.tokenStorage.saveToken(token: token)
// Store user data and verification status
self.currentUser = user
self.isVerified = user.verified
self.isLoading = false
print("Login successful! Token: token")
print("User: \(user.username), Verified: \(user.verified)")
print("isVerified set to: \(self.isVerified)")
// Initialize lookups via APILayer
Task {
_ = try? await APILayer.shared.initializeLookups()
}
// Prefetch all data for caching
Task {
do {
print("Starting data prefetch...")
let prefetchManager = DataPrefetchManager.Companion().getInstance()
_ = try await prefetchManager.prefetchAllData()
print("Data prefetch completed successfully")
} catch {
print("Data prefetch failed: \(error.localizedDescription)")
// Don't block login on prefetch failure
}
}
// Call login success callback
self.onLoginSuccess?(user.verified)
}
}
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.isLoading = false
// Check for specific error codes and provide user-friendly messages
if let code = error.code?.intValue {
switch code {
case 400, 401:
self.errorMessage = "Invalid username or password"
case 403:
self.errorMessage = "Access denied. Please check your credentials."
case 404:
self.errorMessage = "Service not found. Please try again later."
case 500...599:
self.errorMessage = "Server error. Please try again later."
default:
self.errorMessage = ErrorMessageParser.parse(error.message)
}
} else {
self.errorMessage = ErrorMessageParser.parse(error.message)
}
print("API Error: \(error.message)")
}
break
}
}
}
}
// Helper function to clean up error messages
private func cleanErrorMessage(_ message: String) -> String {
// Remove common API error prefixes and technical details
var cleaned = message
// Remove JSON-like error structures
if let range = cleaned.range(of: #"[{\[]"#, options: .regularExpression) {
cleaned = String(cleaned[..<range.lowerBound])
}
// Remove "Error:" prefix if present
cleaned = cleaned.replacingOccurrences(of: "Error:", with: "")
// Trim whitespace
cleaned = cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
// If message is too technical or empty, provide a generic message
if cleaned.isEmpty || cleaned.count > 100 || cleaned.contains("Exception") {
return "Unable to sign in. Please check your credentials and try again."
}
// Capitalize first letter
if let first = cleaned.first {
cleaned = first.uppercased() + cleaned.dropFirst()
}
// Ensure it ends with a period
if !cleaned.hasSuffix(".") && !cleaned.hasSuffix("!") && !cleaned.hasSuffix("?") {
cleaned += "."
}
return cleaned
}
func logout() {
// Call shared ViewModel logout
sharedViewModel.logout()
// Clear token from storage
tokenStorage.clearToken()
// Clear lookups data on logout via DataCache
DataCache.shared.clearLookups()
// Clear all cached data
DataCache.shared.clearAll()
// Reset state
isVerified = false
currentUser = nil
username = ""
password = ""
errorMessage = nil
print("Logged out - all state reset")
}
func clearError() {
errorMessage = nil
}
// MARK: - Private Methods
private func checkAuthenticationStatus() {
guard tokenStorage.getToken() != nil else {
isVerified = false
return
}
// Fetch current user to check verification status
sharedViewModel.getCurrentUser(forceRefresh: false)
Task {
for await state in sharedViewModel.currentUserState {
if let success = state as? ApiResultSuccess<User> {
await MainActor.run {
if let user = success.data {
self.currentUser = user
self.isVerified = user.verified
// Initialize lookups if verified
if user.verified {
Task {
_ = try? await APILayer.shared.initializeLookups()
}
}
print("Auth check - User: \(user.username), Verified: \(user.verified)")
}
}
sharedViewModel.resetCurrentUserState()
break
} else if state is ApiResultError {
await MainActor.run {
// Token invalid or expired, clear it
self.tokenStorage.clearToken()
self.isVerified = false
}
sharedViewModel.resetCurrentUserState()
break
}
}
}
}
}