Implement unified network layer with APILayer and migrate iOS ViewModels

Major architectural improvements:
- Created APILayer as single entry point for all network operations
- Integrated cache-first reads with automatic cache updates on mutations
- Migrated all shared Kotlin ViewModels to use APILayer instead of direct API calls
- Migrated iOS ViewModels to wrap shared Kotlin ViewModels with StateFlow observation
- Replaced LookupsManager with DataCache for centralized lookup data management
- Added password reset methods to AuthViewModel
- Added task completion and update methods to APILayer
- Added residence user management methods to APILayer

iOS specific changes:
- Updated LoginViewModel, RegisterViewModel, ProfileViewModel to use shared AuthViewModel
- Updated ContractorViewModel, DocumentViewModel to use shared ViewModels
- Updated ResidenceViewModel to use shared ViewModel and APILayer
- Updated TaskViewModel to wrap shared ViewModel with callback-based interface
- Migrated PasswordResetViewModel and VerifyEmailViewModel to shared AuthViewModel
- Migrated AllTasksView, CompleteTaskView, EditTaskView to use APILayer
- Migrated ManageUsersView, ResidenceDetailView to use APILayer
- Migrated JoinResidenceView to use async/await pattern with APILayer
- Removed LookupsManager.swift in favor of DataCache
- Fixed PushNotificationManager @MainActor issue
- Converted all direct API calls to use async/await with proper error handling

Benefits:
- Reduced code duplication between iOS and Android
- Consistent error handling across platforms
- Automatic cache management for better performance
- Centralized network layer for easier testing and maintenance
- Net reduction of ~700 lines of code through shared logic

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-12 20:29:42 -06:00
parent eeb8a96f20
commit a61cada072
38 changed files with 2458 additions and 2395 deletions

View File

@@ -3,21 +3,26 @@ package com.mycrib.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.mycrib.shared.models.AuthResponse
import com.mycrib.shared.models.ForgotPasswordRequest
import com.mycrib.shared.models.ForgotPasswordResponse
import com.mycrib.shared.models.LoginRequest
import com.mycrib.shared.models.RegisterRequest
import com.mycrib.shared.models.ResetPasswordRequest
import com.mycrib.shared.models.ResetPasswordResponse
import com.mycrib.shared.models.Residence
import com.mycrib.shared.models.User
import com.mycrib.shared.models.VerifyEmailRequest
import com.mycrib.shared.models.VerifyEmailResponse
import com.mycrib.shared.models.VerifyResetCodeRequest
import com.mycrib.shared.models.VerifyResetCodeResponse
import com.mycrib.shared.network.ApiResult
import com.mycrib.shared.network.AuthApi
import com.mycrib.network.APILayer
import com.mycrib.storage.TokenStorage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class AuthViewModel : ViewModel() {
private val authApi = AuthApi()
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
@@ -31,10 +36,22 @@ class AuthViewModel : ViewModel() {
private val _updateProfileState = MutableStateFlow<ApiResult<User>>(ApiResult.Idle)
val updateProfileState: StateFlow<ApiResult<User>> = _updateProfileState
private val _currentUserState = MutableStateFlow<ApiResult<User>>(ApiResult.Idle)
val currentUserState: StateFlow<ApiResult<User>> = _currentUserState
private val _forgotPasswordState = MutableStateFlow<ApiResult<ForgotPasswordResponse>>(ApiResult.Idle)
val forgotPasswordState: StateFlow<ApiResult<ForgotPasswordResponse>> = _forgotPasswordState
private val _verifyResetCodeState = MutableStateFlow<ApiResult<VerifyResetCodeResponse>>(ApiResult.Idle)
val verifyResetCodeState: StateFlow<ApiResult<VerifyResetCodeResponse>> = _verifyResetCodeState
private val _resetPasswordState = MutableStateFlow<ApiResult<ResetPasswordResponse>>(ApiResult.Idle)
val resetPasswordState: StateFlow<ApiResult<ResetPasswordResponse>> = _resetPasswordState
fun login(username: String, password: String) {
viewModelScope.launch {
_loginState.value = ApiResult.Loading
val result = authApi.login(LoginRequest(username, password))
val result = APILayer.login(LoginRequest(username, password))
_loginState.value = when (result) {
is ApiResult.Success -> {
// Store token for future API calls
@@ -50,7 +67,7 @@ class AuthViewModel : ViewModel() {
fun register(username: String, email: String, password: String) {
viewModelScope.launch {
_registerState.value = ApiResult.Loading
val result = authApi.register(
val result = APILayer.register(
RegisterRequest(
username = username,
email = email,
@@ -76,19 +93,15 @@ class AuthViewModel : ViewModel() {
fun verifyEmail(code: String) {
viewModelScope.launch {
_verifyEmailState.value = ApiResult.Loading
val token = TokenStorage.getToken()
if (token != null) {
val result = authApi.verifyEmail(
token = token,
request = VerifyEmailRequest(code = code)
)
_verifyEmailState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
} else {
val token = TokenStorage.getToken() ?: run {
_verifyEmailState.value = ApiResult.Error("Not authenticated")
return@launch
}
val result = APILayer.verifyEmail(token, VerifyEmailRequest(code = code))
_verifyEmailState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
@@ -100,23 +113,22 @@ class AuthViewModel : ViewModel() {
fun updateProfile(firstName: String?, lastName: String?, email: String?) {
viewModelScope.launch {
_updateProfileState.value = ApiResult.Loading
val token = TokenStorage.getToken()
if (token != null) {
val result = authApi.updateProfile(
token = token,
request = com.mycrib.shared.models.UpdateProfileRequest(
firstName = firstName,
lastName = lastName,
email = email
)
)
_updateProfileState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
} else {
val token = TokenStorage.getToken() ?: run {
_updateProfileState.value = ApiResult.Error("Not authenticated")
return@launch
}
val result = APILayer.updateProfile(
token,
com.mycrib.shared.models.UpdateProfileRequest(
firstName = firstName,
lastName = lastName,
email = email
)
)
_updateProfileState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
@@ -125,12 +137,79 @@ class AuthViewModel : ViewModel() {
_updateProfileState.value = ApiResult.Idle
}
fun getCurrentUser(forceRefresh: Boolean = false) {
viewModelScope.launch {
_currentUserState.value = ApiResult.Loading
val result = APILayer.getCurrentUser(forceRefresh = forceRefresh)
_currentUserState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
fun resetCurrentUserState() {
_currentUserState.value = ApiResult.Idle
}
fun forgotPassword(email: String) {
viewModelScope.launch {
_forgotPasswordState.value = ApiResult.Loading
val result = APILayer.forgotPassword(ForgotPasswordRequest(email = email))
_forgotPasswordState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
fun resetForgotPasswordState() {
_forgotPasswordState.value = ApiResult.Idle
}
fun verifyResetCode(email: String, code: String) {
viewModelScope.launch {
_verifyResetCodeState.value = ApiResult.Loading
val result = APILayer.verifyResetCode(VerifyResetCodeRequest(email = email, code = code))
_verifyResetCodeState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
fun resetVerifyResetCodeState() {
_verifyResetCodeState.value = ApiResult.Idle
}
fun resetPassword(resetToken: String, newPassword: String, confirmPassword: String) {
viewModelScope.launch {
_resetPasswordState.value = ApiResult.Loading
val result = APILayer.resetPassword(
ResetPasswordRequest(
resetToken = resetToken,
newPassword = newPassword,
confirmPassword = confirmPassword
)
)
_resetPasswordState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
fun resetResetPasswordState() {
_resetPasswordState.value = ApiResult.Idle
}
fun logout() {
viewModelScope.launch {
val token = TokenStorage.getToken()
if (token != null) {
authApi.logout(token)
}
APILayer.logout()
TokenStorage.clearToken()
}
}