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>
259 lines
8.2 KiB
Swift
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"
|
|
}
|
|
}
|
|
}
|