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:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user