Files
honeyDueKMP/iosApp/iosApp/Login/LoginViewModel.swift
Trey t b888315e0c Complete iOS document form implementation and improve login error handling
This commit completes the DRY refactoring by implementing the missing document form functionality and enhancing user experience with better error messages.

## iOS Document Forms
- Implemented complete createDocument() method in DocumentViewModel:
  - Support for all warranty-specific fields (itemName, modelNumber, serialNumber, provider, etc.)
  - Multiple image uploads with JPEG compression
  - Proper UIImage to KotlinByteArray conversion
  - Async completion handlers
- Implemented updateDocument() method with full field support
- Completed DocumentFormView submitForm() implementation with proper API calls
- Fixed type conversion issues (Bool/KotlinBoolean, Int32/KotlinInt)
- Added proper error handling and user feedback

## iOS Login Error Handling
- Enhanced error messages to be user-friendly and concise
- Added specific messages for common HTTP error codes (400, 401, 403, 404, 500+)
- Implemented cleanErrorMessage() helper to remove technical jargon
- Added network-specific error handling (connection, timeout)
- Fixed MainActor isolation warnings with proper Task wrapping

## Code Quality
- Removed ~4,086 lines of duplicate code through form consolidation
- Added 429 lines of new shared form components
- Fixed Swift compiler performance issues
- Ensured both iOS and Android builds succeed

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 11:35:41 -06:00

259 lines
8.2 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 isAuthenticated: Bool = false
@Published var isVerified: Bool = false
@Published var currentUser: User?
// MARK: - Private Properties
private let authApi: AuthApi
private let tokenStorage: TokenStorage
// MARK: - Initialization
init() {
self.authApi = AuthApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage.shared
// Check if user is already logged in
checkAuthenticationStatus()
}
// 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
let loginRequest = LoginRequest(username: username, password: password)
do {
// Call the KMM AuthApi login method
authApi.login(request: loginRequest) { result, error in
Task { @MainActor in
if let successResult = result as? ApiResultSuccess<AuthResponse> {
self.handleSuccess(results: successResult)
return
}
if let errorResult = result as? ApiResultError {
self.handleApiError(errorResult: errorResult)
return
}
if let error = error {
self.handleError(error: error)
return
}
self.isLoading = false
self.isAuthenticated = false
self.errorMessage = "Login failed. Please try again."
print("unknown error")
}
}
}
}
@MainActor
func handleError(error: any Error) {
self.isLoading = false
self.isAuthenticated = false
// Clean up error message for user
let errorDescription = error.localizedDescription
if errorDescription.contains("network") || errorDescription.contains("connection") || errorDescription.contains("Internet") {
self.errorMessage = "Network error. Please check your connection and try again."
} else if errorDescription.contains("timeout") {
self.errorMessage = "Request timed out. Please try again."
} else {
self.errorMessage = cleanErrorMessage(errorDescription)
}
print("Error: \(error)")
}
@MainActor
func handleApiError(errorResult: ApiResultError) {
self.isLoading = false
self.isAuthenticated = false
// Check for specific error codes and provide user-friendly messages
if let code = errorResult.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 = cleanErrorMessage(errorResult.message)
}
} else {
self.errorMessage = cleanErrorMessage(errorResult.message)
}
print("API Error: \(errorResult.message)")
}
// 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
}
@MainActor
func handleSuccess(results: ApiResultSuccess<AuthResponse>) {
if let token = results.data?.token,
let user = results.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 repository after successful login
LookupsManager.shared.initialize()
// Update authentication state AFTER setting verified status
// Small delay to ensure state updates are processed
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.isAuthenticated = true
print("isAuthenticated set to true, isVerified is: \(self.isVerified)")
}
}
}
func logout() {
let token = tokenStorage.getToken()
if let token = token {
// Call logout API
authApi.logout(token: token) { _, _ in
// Ignore result, clear token anyway
}
}
// Clear token from storage
tokenStorage.clearToken()
// Clear lookups data on logout
LookupsManager.shared.clear()
// Reset state
isAuthenticated = false
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 let token = tokenStorage.getToken() else {
isAuthenticated = false
isVerified = false
return
}
// Fetch current user to check verification status
authApi.getCurrentUser(token: token) { result, error in
Task { @MainActor in
if let successResult = result as? ApiResultSuccess<User> {
self.handleAuthCheck(user: successResult.data!)
} else {
// Token invalid or expired, clear it
self.tokenStorage.clearToken()
self.isAuthenticated = false
self.isVerified = false
}
}
}
}
@MainActor
private func handleAuthCheck(user: User) {
self.currentUser = user
self.isVerified = user.verified
self.isAuthenticated = true
// Initialize lookups if verified
if user.verified {
LookupsManager.shared.initialize()
}
print("Auth check - User: \(user.username), Verified: \(user.verified)")
}
}
// MARK: - Error Types
enum LoginError: LocalizedError {
case unknownError
var errorDescription: String? {
switch self {
case .unknownError:
return "An unknown error occurred"
}
}
}