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>
225 lines
8.0 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|