diff --git a/app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt b/app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt index 9ca6795..f8d78f3 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt @@ -13,7 +13,10 @@ interface RepoDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(repo: RepoEntity) + @Query("DELETE FROM repositories WHERE id = :id") + suspend fun delete(id: Int) + @Query("SELECT * FROM repositories") fun getAll(): Flow> -} \ No newline at end of file +} diff --git a/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt index 841baab..794806d 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt @@ -8,4 +8,6 @@ interface RepoLocalDataSource { suspend fun save(repo: RepoEntity) fun getAll(): Flow> + + suspend fun delete(id: Int) } diff --git a/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt index 9d8e8e4..e73fc63 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt @@ -14,4 +14,8 @@ class RepoLocalDataSourceImpl( override fun getAll(): Flow> = dao.getAll() + + override suspend fun delete(id: Int) { + dao.delete(id) + } } diff --git a/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt b/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt index 101e968..f0ded28 100644 --- a/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt +++ b/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt @@ -2,10 +2,12 @@ package com.delecrode.devhub.data.mapper import com.delecrode.devhub.data.model.dto.UserForFirebaseDto import com.delecrode.devhub.data.model.dto.UserForGitDto +import com.delecrode.devhub.domain.model.UserAuth import com.delecrode.devhub.domain.model.UserForFirebase import com.delecrode.devhub.domain.model.UserForGit +import com.google.firebase.auth.FirebaseUser -fun UserForGitDto.toUserDomain(): UserForGit { +fun UserForGitDto.toUserGitDomain(): UserForGit { return UserForGit( login = login, avatar_url = avatar_url, @@ -17,10 +19,18 @@ fun UserForGitDto.toUserDomain(): UserForGit { } -fun UserForFirebaseDto.toUserDomain(): UserForFirebase{ +fun UserForFirebaseDto.toUserFirebaseDomain(): UserForFirebase{ return UserForFirebase( fullName = fullName, username = username, email = email ) } + +fun FirebaseUser.toUserAuthDomain(): UserAuth = + UserAuth( + uid = uid, + email = email + ) + + diff --git a/app/src/main/java/com/delecrode/devhub/data/model/dto/UserDto.kt b/app/src/main/java/com/delecrode/devhub/data/model/dto/UserDto.kt index 16327c6..5f07bdd 100644 --- a/app/src/main/java/com/delecrode/devhub/data/model/dto/UserDto.kt +++ b/app/src/main/java/com/delecrode/devhub/data/model/dto/UserDto.kt @@ -40,4 +40,4 @@ data class UserForFirebaseDto( val fullName: String = "", val username: String = "", val email: String = "" -) +) \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/remote/firebase/FirebaseAuth.kt b/app/src/main/java/com/delecrode/devhub/data/remote/firebase/FirebaseAuth.kt index e634003..56c26f5 100644 --- a/app/src/main/java/com/delecrode/devhub/data/remote/firebase/FirebaseAuth.kt +++ b/app/src/main/java/com/delecrode/devhub/data/remote/firebase/FirebaseAuth.kt @@ -1,5 +1,6 @@ package com.delecrode.devhub.data.remote.firebase +import android.util.Log import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import kotlin.coroutines.resume @@ -43,4 +44,21 @@ class FirebaseAuth( fun signOut() { auth.signOut() } + + suspend fun forgotPassword(email: String) { + Log.i("ForgotPasswordScreen", "forgotPassword: $email") + suspendCoroutine { cont -> + auth.sendPasswordResetEmail(email) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + cont.resume(Unit) + } else { + cont.resumeWithException( + task.exception ?: Exception("Erro ao enviar e-mail de recuperação") + ) + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/delecrode/devhub/data/repository/AuthRepositoryImpl.kt index 4fe8709..4c1e6c6 100644 --- a/app/src/main/java/com/delecrode/devhub/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/repository/AuthRepositoryImpl.kt @@ -1,15 +1,15 @@ package com.delecrode.devhub.data.repository import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSource +import com.delecrode.devhub.data.mapper.toUserAuthDomain import com.delecrode.devhub.data.remote.firebase.FirebaseAuth import com.delecrode.devhub.data.remote.firebase.UserExtraData import com.delecrode.devhub.domain.model.RegisterUser +import com.delecrode.devhub.domain.model.UserAuth import com.delecrode.devhub.domain.repository.AuthRepository -import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException -import com.google.firebase.auth.FirebaseAuthInvalidUserException -import com.google.firebase.auth.FirebaseAuthUserCollisionException -import com.google.firebase.auth.FirebaseAuthWeakPasswordException -import com.google.firebase.auth.FirebaseUser +import com.delecrode.devhub.utils.Result +import com.delecrode.devhub.utils.mapAuthError +import com.delecrode.devhub.utils.mapSignUpError class AuthRepositoryImpl( private val authDataSource: FirebaseAuth, @@ -17,31 +17,32 @@ class AuthRepositoryImpl( private val authLocalDataSource: AuthLocalDataSource ) : AuthRepository { - override suspend fun signIn(email: String, password: String): FirebaseUser { - try { - val response = authDataSource.signIn(email, password) - authLocalDataSource.saveUID(response?.uid ?: "") - return response ?: throw Exception("Erro ao recuperar usuário após login") + override suspend fun signIn( + email: String, + password: String + ): Result { + return try { + val firebaseUser = authDataSource.signIn(email, password) + ?: return Result.Error("Erro ao autenticar usuário") + authLocalDataSource.saveUID(firebaseUser.uid) + + Result.Success(firebaseUser.toUserAuthDomain()) + } catch (e: Exception) { - val errorMessage = when (e) { - is FirebaseAuthInvalidUserException -> "Usuário não encontrado." - is FirebaseAuthInvalidCredentialsException -> "Senha incorreta ou e-mail inválido." - is FirebaseAuthUserCollisionException -> "Este e-mail já está em uso." - is FirebaseAuthWeakPasswordException -> "A senha deve ter pelo menos 6 caracteres." - else -> e.message ?: "Erro desconhecido ao fazer login" - } - throw Exception(errorMessage) + Result.Error(mapAuthError(e)) } } + override suspend fun signUp( name: String, username: String, email: String, password: String - ): Boolean { - try { - val uid = authDataSource.signUp(email, password) ?: return false + ): Result { + return try { + val uid = authDataSource.signUp(email, password) + ?: return Result.Error("Erro ao criar conta") val userData = RegisterUser( fullName = name, @@ -51,18 +52,24 @@ class AuthRepositoryImpl( userExtraDataSource.saveUserData(uid, userData) - return true + Result.Success(Unit) + } catch (e: Exception) { - val errorMessage = when (e) { - is FirebaseAuthUserCollisionException -> "Este e-mail já está em uso." - is FirebaseAuthWeakPasswordException -> "A senha deve ter pelo menos 6 caracteres." - is FirebaseAuthInvalidCredentialsException -> "E-mail inválido." - else -> e.message ?: "Erro desconhecido ao cadastrar" - } - throw Exception(errorMessage) + Result.Error(mapSignUpError(e)) } } + override suspend fun forgotPassword(email: String): Result { + return try { + authDataSource.forgotPassword(email) + Result.Success(Unit) + + } catch (e: Exception) { + Result.Error(mapAuthError(e)) + } + } + + override suspend fun signOut() { authDataSource.signOut() authLocalDataSource.clearUID() diff --git a/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt b/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt index 95c5b37..acc2b4b 100644 --- a/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt @@ -11,15 +11,26 @@ import com.delecrode.devhub.domain.model.Languages import com.delecrode.devhub.domain.model.RepoDetail import com.delecrode.devhub.domain.model.RepoFav import com.delecrode.devhub.domain.repository.RepoRepository +import com.delecrode.devhub.utils.Result +import com.delecrode.devhub.utils.mapHttpError import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.io.IOException -class RepoRepositoryImpl(val repoApi: RepoApiService, private val localDataSource: RepoLocalDataSource, private val authLocalDataSource: AuthLocalDataSource) : RepoRepository { +class RepoRepositoryImpl( + val repoApi: RepoApiService, + private val localDataSource: RepoLocalDataSource, + private val authLocalDataSource: AuthLocalDataSource +) : RepoRepository { override suspend fun save(repo: RepoFav) { localDataSource.save(repo.toEntity()) } + override suspend fun delete(id: Int){ + localDataSource.delete(id) + } + override fun getAll(): Flow> = localDataSource.getAll() .map { list -> list.map { it.toDomain() } } @@ -27,35 +38,40 @@ class RepoRepositoryImpl(val repoApi: RepoApiService, private val localDataSour override fun getUserName(): Flow = authLocalDataSource.getUserName() - override suspend fun getRepoDetail(owner: String, repo: String): RepoDetail { - try { + override suspend fun getRepoDetail(owner: String, repo: String): Result { + return try { val response = repoApi.getRepoDetail(owner, repo) if (response.isSuccessful) { val body = response.body() - if (body != null) { - return body.toRepoDetailDomain() - } else { - throw Exception("Resposta vazia do servidor") - } + ?: return Result.Error("Dados do repositório indisponíveis") + Result.Success(body.toRepoDetailDomain()) } else { - throw Exception("Erro na requisição ${response.code()}") + Result.Error(mapHttpError(response.code())) } - } catch (e: Exception) { - throw e + } catch (e: IOException) { + Result.Error("Sem conexão com a internet") } } - override suspend fun getLanguagesRepo(owner: String, repo: String) : Languages{ + override suspend fun getLanguagesRepo( + owner: String, + repo: String + ): Result { return try { val response = repoApi.getRepoLanguages(owner, repo) + if (response.isSuccessful) { val body = response.body() - body?.toLanguagesDomain() ?: Languages(emptyList()) + ?: return Result.Error("Linguagens indisponíveis") + + Result.Success(body.toLanguagesDomain()) } else { - Languages(emptyList()) + Result.Error(mapHttpError(response.code())) } - } catch (e: Exception) { - Languages(emptyList()) + + } catch (e: IOException) { + Result.Error("Sem conexão com a internet") } } + } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt index aeb7139..244a0b5 100644 --- a/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt @@ -1,9 +1,9 @@ package com.delecrode.devhub.data.repository -import android.util.Log import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSource import com.delecrode.devhub.data.mapper.toReposDomain -import com.delecrode.devhub.data.mapper.toUserDomain +import com.delecrode.devhub.data.mapper.toUserFirebaseDomain +import com.delecrode.devhub.data.mapper.toUserGitDomain import com.delecrode.devhub.data.model.dto.UserForFirebaseDto import com.delecrode.devhub.data.remote.firebase.UserExtraData import com.delecrode.devhub.data.remote.webApi.service.UserApiService @@ -11,7 +11,10 @@ import com.delecrode.devhub.domain.model.Repos import com.delecrode.devhub.domain.model.UserForFirebase import com.delecrode.devhub.domain.model.UserForGit import com.delecrode.devhub.domain.repository.UserRepository +import com.delecrode.devhub.utils.Result +import com.delecrode.devhub.utils.mapHttpError import kotlinx.coroutines.flow.first +import java.io.IOException class UserRepositoryImpl( private val userApi: UserApiService, @@ -19,68 +22,62 @@ class UserRepositoryImpl( private val authLocalDataSource: AuthLocalDataSource ) : UserRepository { - override suspend fun getUserForGitHub(userName: String): UserForGit { - try { + override suspend fun getUserForGitHub(userName: String): Result { + return try { val response = userApi.getUser(userName) if (response.isSuccessful) { val body = response.body() - if (body != null) { - Log.i("GitRepositoryImpl", "getUser: $body") - return body.toUserDomain() - } else { - throw Exception("Resposta vazia do servidor") - } + ?: return Result.Error("Dados do usuário indisponíveis") + Result.Success(body.toUserGitDomain()) } else { - throw Exception("Erro na requisição ${response.code()}") + Result.Error(mapHttpError(response.code())) } - } catch (e: Exception) { - throw e + } catch (e: IOException) { + Result.Error("Sem conexão com a internet") } } - override suspend fun getUserForFirebase(): UserForFirebase { - try { + override suspend fun getUserForFirebase(): Result { + return try { val uid = authLocalDataSource.getUID().first() - Log.i("UserRepositoryImpl", "getUserForFirebase (UID Real): $uid") - if (uid != null) { - val response = userExtraData.getUser(uid) - if (response.exists()) { - val body = response.toObject(UserForFirebaseDto::class.java)?.toUserDomain() - val userName = body?.username - if (userName != null) { - authLocalDataSource.saveUserName(userName) - } - if (body != null) { - return body - } else { - throw Exception("Resposta vazia do servidor") - } - } else { - throw Exception("Usuário não encontrado") - } - } else { - return UserForFirebase() + + if (uid.isNullOrBlank()) { + return Result.Error("Usuário não autenticado") + } + + val snapshot = userExtraData.getUser(uid) + + if (!snapshot.exists()) { + return Result.Error("Usuário não encontrado") } + + val user = snapshot + .toObject(UserForFirebaseDto::class.java) + ?.toUserFirebaseDomain() + ?: return Result.Error("Dados do usuário inválidos") + + authLocalDataSource.saveUserName(user.username) + + Result.Success(user) + } catch (e: Exception) { - throw e + Result.Error("Erro ao buscar dados do usuário") } } - override suspend fun getRepos(userName: String): List { - try { + + override suspend fun getRepos(userName: String): Result> { + return try { val response = userApi.getReposForUser(userName) if (response.isSuccessful) { val body = response.body() - if (body != null) { - return body.map { it.toReposDomain() } - } else { - throw Exception("Resposta vazia do servidor") - } + ?: return Result.Error("Dados dos repositórios indisponíveis") + Result.Success(body.map { it.toReposDomain() }) } else { - throw Exception("Erro na requisição ${response.code()}") + Result.Error(mapHttpError(response.code())) } - } catch (e: Exception) { - throw e + } catch (e: IOException) { + Result.Error("Sem conexão com a internet") } } } diff --git a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt index cfd29d1..b8052cd 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -16,6 +16,7 @@ import com.delecrode.devhub.domain.repository.RepoRepository import com.delecrode.devhub.domain.repository.UserRepository import com.delecrode.devhub.domain.session.SessionViewModel import com.delecrode.devhub.ui.favoritos.RepoFavViewModel +import com.delecrode.devhub.ui.forgot.ForgotPasswordViewModel import com.delecrode.devhub.ui.home.HomeViewModel import com.delecrode.devhub.ui.login.AuthViewModel import com.delecrode.devhub.ui.profile.ProfileViewModel @@ -55,7 +56,7 @@ val appModule = module { single { UserExtraData(get()) } single { UserRepositoryImpl(get(), get(), get()) } - single { RepoRepositoryImpl(get(),get(),get()) } + single { RepoRepositoryImpl(get(), get(), get()) } single { AuthRepositoryImpl(get(), get(), get()) } viewModel { HomeViewModel(get(), get()) } @@ -63,7 +64,7 @@ val appModule = module { viewModel { AuthViewModel(get()) } viewModel { SessionViewModel(get()) } viewModel { RegisterViewModel(get()) } - viewModel { ProfileViewModel(get(),get()) } + viewModel { ProfileViewModel(get(), get()) } viewModel { RepoFavViewModel(get()) } - + viewModel { ForgotPasswordViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt b/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt index fe6441f..9cddbe8 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt @@ -1,21 +1,21 @@ package com.delecrode.devhub.domain.model data class RepoDetail( - val id: Int, - val name: String, - val html_url: String, + val id: Int?, + val name: String?, + val html_url: String?, val description: String?, - val branches_url: String, - val tags_url: String, - val created_at: String, - val updated_at: String, - val pushed_at: String, - val clone_url: String, - val size: Int, - val language: String, - val forks_count: Int, - val default_branch: String, - val subscribers_count: Int + val branches_url: String?, + val tags_url: String?, + val created_at: String?, + val updated_at: String?, + val pushed_at: String?, + val clone_url: String?, + val size: Int?, + val language: String?, + val forks_count: Int?, + val default_branch: String?, + val subscribers_count: Int? ) data class Languages( diff --git a/app/src/main/java/com/delecrode/devhub/domain/model/User.kt b/app/src/main/java/com/delecrode/devhub/domain/model/User.kt index 9aac089..cb51733 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/model/User.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/model/User.kt @@ -25,3 +25,9 @@ data class UserForFirebase( val email: String = "" ) +data class UserAuth( + val uid: String, + val email: String? +) + + diff --git a/app/src/main/java/com/delecrode/devhub/domain/repository/AuthRepository.kt b/app/src/main/java/com/delecrode/devhub/domain/repository/AuthRepository.kt index 5113038..56a2bb0 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/repository/AuthRepository.kt @@ -1,18 +1,18 @@ package com.delecrode.devhub.domain.repository -import com.google.firebase.auth.FirebaseUser +import com.delecrode.devhub.domain.model.UserAuth +import com.delecrode.devhub.utils.Result interface AuthRepository { - suspend fun signIn(email: String, password: String): FirebaseUser + suspend fun signIn(email: String, password: String): Result suspend fun signUp( name: String, username: String, email: String, password: String - ): Boolean + ): Result suspend fun signOut() - - + suspend fun forgotPassword(email: String): Result } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt b/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt index c2d510c..af662ae 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt @@ -3,16 +3,16 @@ package com.delecrode.devhub.domain.repository import com.delecrode.devhub.domain.model.Languages import com.delecrode.devhub.domain.model.RepoDetail import com.delecrode.devhub.domain.model.RepoFav +import com.delecrode.devhub.utils.Result import kotlinx.coroutines.flow.Flow interface RepoRepository { - suspend fun getRepoDetail(owner: String, repo: String): RepoDetail - - suspend fun getLanguagesRepo(owner: String, repo: String): Languages - + suspend fun getRepoDetail(owner: String, repo: String): Result + suspend fun getLanguagesRepo(owner: String, repo: String): Result suspend fun save(repo: RepoFav) fun getAll(): Flow> + suspend fun delete(id: Int) fun getUserName(): Flow } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/domain/repository/UserRepository.kt b/app/src/main/java/com/delecrode/devhub/domain/repository/UserRepository.kt index a773d55..8bba190 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/repository/UserRepository.kt @@ -3,12 +3,13 @@ package com.delecrode.devhub.domain.repository import com.delecrode.devhub.domain.model.Repos import com.delecrode.devhub.domain.model.UserForFirebase import com.delecrode.devhub.domain.model.UserForGit +import com.delecrode.devhub.utils.Result interface UserRepository { - suspend fun getUserForGitHub(userName: String): UserForGit - suspend fun getRepos(userName: String): List + suspend fun getUserForGitHub(userName: String): Result + suspend fun getRepos(userName: String): Result> - suspend fun getUserForFirebase(): UserForFirebase + suspend fun getUserForFirebase(): Result } diff --git a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt index f81f9ee..5e9cb30 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt @@ -11,6 +11,7 @@ import com.delecrode.devhub.domain.session.SessionViewModel import com.delecrode.devhub.ui.favoritos.RepoFavViewModel import com.delecrode.devhub.ui.favoritos.ReposFavScreen import com.delecrode.devhub.ui.forgot.ForgotPasswordScreen +import com.delecrode.devhub.ui.forgot.ForgotPasswordViewModel import com.delecrode.devhub.ui.home.HomeScreen import com.delecrode.devhub.ui.home.HomeViewModel import com.delecrode.devhub.ui.login.AuthViewModel @@ -34,6 +35,8 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { val registerViewModel: RegisterViewModel = koinViewModel() val profileViewModel: ProfileViewModel = koinViewModel() val repoFavViewModel: RepoFavViewModel = koinViewModel() + val forgotPasswordViewModel: ForgotPasswordViewModel = koinViewModel() + val logged = sessionViewModel.isLoggedIn.collectAsState() @@ -74,7 +77,7 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { //Forgot Password Flow composable(AppDestinations.ForgotPassword.route) { - ForgotPasswordScreen(navController) + ForgotPasswordScreen(navController, forgotPasswordViewModel) } composable(AppDestinations.Profile.route) { diff --git a/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt index f1261ec..3b8fae4 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt @@ -5,16 +5,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Favorite import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar @@ -32,9 +31,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.navigation.NavController import com.delecrode.devhub.navigation.AppDestinations @@ -68,15 +65,6 @@ fun ReposFavScreen(navController: NavController, viewModel: RepoFavViewModel) { ) { padding -> Column(modifier = Modifier.padding(padding)) { - - Text( - "Repositórios Favoritos", fontWeight = FontWeight.Bold, - fontSize = 28.sp, - color = MaterialTheme.colorScheme.onBackground - ) - - Spacer(modifier = Modifier.height(16.dp)) - LazyColumn(modifier = Modifier.fillMaxWidth()) { items(state.repoFav.size) { index -> val repo = repos[index] @@ -103,7 +91,12 @@ fun ReposFavScreen(navController: NavController, viewModel: RepoFavViewModel) { .padding(10.dp), verticalAlignment = Alignment.CenterVertically ) { - Text(text = repo.name) + Text(text = repo.name, modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = "Favorito", + tint = Color.Red + ) } if (repo.description != null) { diff --git a/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordScreen.kt index 5d790a7..572e5c1 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordScreen.kt @@ -1,5 +1,6 @@ package com.delecrode.devhub.ui.forgot +import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -16,11 +17,14 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign @@ -31,13 +35,35 @@ import androidx.navigation.compose.rememberNavController import com.delecrode.devhub.navigation.AppDestinations import com.delecrode.devhub.ui.components.EmailTextField import com.delecrode.devhub.ui.components.PrimaryButton +import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ForgotPasswordScreen(navController: NavController) { +fun ForgotPasswordScreen(navController: NavController, viewModel: ForgotPasswordViewModel) { + + val state by viewModel.state.collectAsState() + val context = LocalContext.current var email by remember { mutableStateOf("") } + LaunchedEffect(state.success) { + if (state.success) { + navController.navigate(AppDestinations.Login.route){ + popUpTo(AppDestinations.Login.route){ + inclusive = true + } + } + Toast.makeText(context, "E-mail enviado com sucesso!", Toast.LENGTH_SHORT).show() + viewModel.clearState() + } + } + + LaunchedEffect(email) { + if (state.emailError != null) { + viewModel.clearEmailError() + } + } + Scaffold( topBar = { @@ -83,14 +109,16 @@ fun ForgotPasswordScreen(navController: NavController) { value = email, onValueChange = { email = it }, label = "Email", - imeAction = ImeAction.Done + imeAction = ImeAction.Done, + isError = state.emailError != null, + errorMessage = state.emailError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) PrimaryButton( text = "Enviar e-mail", - onClick = { navController.navigate(AppDestinations.Login.route) }, + onClick = { viewModel.forgotPassword(email) }, enabled = email.isNotBlank() ) } @@ -101,5 +129,5 @@ fun ForgotPasswordScreen(navController: NavController) { @Preview @Composable fun ForgotPasswordScreenPreview() { - ForgotPasswordScreen(rememberNavController()) + ForgotPasswordScreen(rememberNavController(), koinViewModel()) } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordState.kt b/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordState.kt new file mode 100644 index 0000000..9e34c09 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordState.kt @@ -0,0 +1,8 @@ +package com.delecrode.devhub.ui.forgot + +data class ForgotPasswordState( + val isLoading: Boolean = false, + val error: String? = null, + val success: Boolean = false, + val emailError: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordViewModel.kt new file mode 100644 index 0000000..f1a6818 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordViewModel.kt @@ -0,0 +1,69 @@ +package com.delecrode.devhub.ui.forgot + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.delecrode.devhub.domain.repository.AuthRepository +import com.delecrode.devhub.utils.Result +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class ForgotPasswordViewModel(private val authRepository: AuthRepository) : ViewModel() { + + private val _state = MutableStateFlow(ForgotPasswordState()) + val state: StateFlow = _state + + fun forgotPassword(email: String) { + viewModelScope.launch { + val emailValidation = validateEmail(email) + if (emailValidation != null) { + _state.value = _state.value.copy( + emailError = emailValidation, + error = null, + isLoading = false + ) + return@launch + } + _state.value = _state.value.copy( + isLoading = true, + error = null + ) + when (val result = authRepository.forgotPassword(email)) { + is Result.Success -> { + _state.value = _state.value.copy( + isLoading = false, + success = true + ) + } + + is Result.Error -> { + _state.value = _state.value.copy( + isLoading = false, + error = result.message + ) + } + } + } + } + + fun clearState() { + _state.value = ForgotPasswordState() + } + + fun clearEmailError() { + _state.value = _state.value.copy(emailError = null) + } + + private fun validateEmail(email: String): String? { + return when { + email.isBlank() -> "Email é obrigatório" + !isValidEmailFormat(email) -> "Email inválido" + else -> null + } + } + + private fun isValidEmailFormat(email: String): Boolean { + return Regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$").matches(email) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/home/HomeScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/home/HomeScreen.kt index 14792c4..38da238 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/home/HomeScreen.kt @@ -72,20 +72,10 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel) { val repos = uiState.value.repos - var searchText by remember { mutableStateOf("") } - var search by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) } val context = LocalContext.current - LaunchedEffect(search) { - if (search && searchText.isNotBlank()) { - homeViewModel.getRepos(searchText) - homeViewModel.getUserForSearchGit(searchText) - search = false - } - } - LaunchedEffect(uiState.value.error) { if (uiState.value.error != null) { Toast.makeText(context, uiState.value.error, Toast.LENGTH_SHORT).show() @@ -155,6 +145,7 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel) { onClick = { expanded = false navController.navigate(AppDestinations.Profile.route) + homeViewModel.clearStates() } ) DropdownMenuItem( @@ -165,6 +156,7 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel) { navController.navigate(AppDestinations.Profile.route) { popUpTo(0) } + homeViewModel.clearStates() } ) } @@ -189,8 +181,10 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel) { .padding(8.dp) ) { OutlinedTextField( - value = searchText, - onValueChange = { searchText = it }, + value = uiState.value.searchText, + onValueChange = { value -> + homeViewModel.onSearchTextChange(value) + }, modifier = Modifier .fillMaxWidth() .padding(end = 16.dp), @@ -203,7 +197,7 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel) { unfocusedBorderColor = Color.Gray ), trailingIcon = { - IconButton(onClick = { search = true }) { + IconButton(onClick = { homeViewModel.onSearchClick() }) { Icon( painter = painterResource(R.drawable.ic_search_24), contentDescription = "Search", diff --git a/app/src/main/java/com/delecrode/devhub/ui/home/HomeState.kt b/app/src/main/java/com/delecrode/devhub/ui/home/HomeState.kt index e262c63..4a8b152 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/home/HomeState.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/home/HomeState.kt @@ -5,10 +5,11 @@ import com.delecrode.devhub.domain.model.UserForFirebase import com.delecrode.devhub.domain.model.UserForGit data class HomeState( - val isLoading: Boolean = false, val userForSearchGit: UserForGit? = null, val userForGit: UserForGit? = null, val userForFirebase: UserForFirebase? = null, val repos: List = emptyList(), + val searchText: String = "", + val isLoading: Boolean = false, val error: String? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/home/HomeViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/home/HomeViewModel.kt index 382010a..8c23ce1 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/home/HomeViewModel.kt @@ -3,11 +3,12 @@ package com.delecrode.devhub.ui.home import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.delecrode.devhub.domain.model.Repos import com.delecrode.devhub.domain.repository.AuthRepository import com.delecrode.devhub.domain.repository.UserRepository +import com.delecrode.devhub.utils.Result import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class HomeViewModel( @@ -19,24 +20,42 @@ class HomeViewModel( private val _uiState = MutableStateFlow(HomeState()) val uiState: StateFlow = _uiState + fun onSearchTextChange(value: String) { + _uiState.update { + it.copy(searchText = value) + } + } + + fun onSearchClick() { + val search = uiState.value.searchText + if (search.isBlank()) return + + getRepos(search) + getUserForSearchGit(search) + } + + fun getUserForSearchGit(userName: String) { viewModelScope.launch { _uiState.value = _uiState.value.copy( isLoading = true, error = null ) - try { - val user = userRepository.getUserForGitHub(userName) - _uiState.value = _uiState.value.copy( - userForSearchGit = user, - isLoading = false - ) - } catch (e: Exception) { - Log.e("HomeViewModel", "Erro ao buscar usuário", e) - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false - ) + when (val result = userRepository.getUserForGitHub(userName)) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + userForSearchGit = result.data, + isLoading = false, + error = null + ) + } + + is Result.Error -> { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = result.message + ) + } } } } @@ -47,18 +66,21 @@ class HomeViewModel( isLoading = true, error = null ) - try { - val user = userRepository.getUserForGitHub(userName) - _uiState.value = _uiState.value.copy( - userForGit = user, - isLoading = false - ) - } catch (e: Exception) { - Log.e("HomeViewModel", "Erro ao buscar usuário", e) - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false - ) + when (val result = userRepository.getUserForGitHub(userName)) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + userForGit = result.data, + isLoading = false, + error = null + ) + } + + is Result.Error -> { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = result.message + ) + } } } } @@ -69,16 +91,23 @@ class HomeViewModel( isLoading = true, error = null ) - try { - val user = userRepository.getUserForFirebase() - _uiState.value = _uiState.value.copy( - userForFirebase = user, - isLoading = false - ) - Log.i("HomeViewModel", "getUserForFirebase: $user") - } catch (e: Exception) { - throw e + when (val result = userRepository.getUserForFirebase()) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + userForFirebase = result.data, + isLoading = false, + error = null + ) + } + + is Result.Error -> { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = result.message + ) + } } + } } @@ -88,40 +117,44 @@ class HomeViewModel( isLoading = true, error = null ) - try { - val repos: List = userRepository.getRepos(userName) - _uiState.value = _uiState.value.copy( - repos = repos, - isLoading = false - ) - } catch (e: Exception) { - Log.e("HomeViewModel", "Erro ao buscar repositórios", e) - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false - ) + when (val result = userRepository.getRepos(userName)) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + repos = result.data, + isLoading = false, + error = null + ) + } + + is Result.Error -> { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = result.message + ) + } } } } - fun signOut() { - viewModelScope.launch { - try { - authRepository.signOut() - } catch (e: Exception) { - Log.e("HomeViewModel", "Erro ao fazer logout", e) - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false - ) + fun signOut() { + viewModelScope.launch { + try { + authRepository.signOut() + } catch (e: Exception) { + Log.e("HomeViewModel", "Erro ao fazer logout", e) + _uiState.value = _uiState.value.copy( + error = e.message, + isLoading = false + ) + } } } - } - fun clearStates() { - _uiState.value = _uiState.value.copy( - isLoading = false, - error = null - ) + fun clearStates() { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = null + ) + } } -} + diff --git a/app/src/main/java/com/delecrode/devhub/ui/login/AuthState.kt b/app/src/main/java/com/delecrode/devhub/ui/login/AuthState.kt index 8c67f4d..0f690ca 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/login/AuthState.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/login/AuthState.kt @@ -4,5 +4,13 @@ data class AuthState( val isLoading: Boolean = false, val isSuccess: Boolean = false, val error: String? = null, - val userUid: String? = null -) + val userUid: String? = null, + val emailError: String? = null, + val passwordError: String? = null, +) { + val hasValidationErrors: Boolean + get() = emailError != null || passwordError != null + + val canLogin: Boolean + get() = !isLoading && !hasValidationErrors && error == null +} diff --git a/app/src/main/java/com/delecrode/devhub/ui/login/AuthViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/login/AuthViewModel.kt index 694d0c2..07da1bf 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/login/AuthViewModel.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/login/AuthViewModel.kt @@ -1,9 +1,9 @@ package com.delecrode.devhub.ui.login -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.delecrode.devhub.domain.repository.AuthRepository +import com.delecrode.devhub.utils.Result import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -16,29 +16,79 @@ class AuthViewModel( val state = _state.asStateFlow() fun signIn(email: String, password: String) { - _state.value = AuthState(isLoading = true) + viewModelScope.launch { _state.value = AuthState(isLoading = true) - - try { - val user = repository.signIn(email, password) - _state.value = AuthState( - isSuccess = true, - userUid = user.uid, + val emailValidation = validateEmail(email) + if (emailValidation != null) { + _state.value = _state.value.copy( + emailError = emailValidation, + passwordError = null, + error = null, isLoading = false ) - Log.i("AuthViewModel", "signIn: Usuario Logado ${user.uid}") - } catch (e: Exception) { - _state.value = AuthState( - error = e.message, + return@launch + } + + val passwordValidation = validatePassword(password) + if (passwordValidation != null) { + _state.value = _state.value.copy( + emailError = null, + passwordError = passwordValidation, + error = null, isLoading = false ) + return@launch + } + + when (val result = repository.signIn(email, password)) { + is Result.Success -> { + _state.value = AuthState(isSuccess = true) + } + + is Result.Error -> { + _state.value = AuthState(error = result.message, isLoading = false) + + } } } } - fun clearState(){ + fun clearState() { _state.value = AuthState() } + + fun clearEmailError() { + _state.value = _state.value.copy(emailError = null) + } + + fun clearPasswordError() { + _state.value = _state.value.copy(passwordError = null) + } + + private fun validateEmail(email: String): String? { + return when { + email.isBlank() -> "Email é obrigatório" + !isValidEmailFormat(email) -> "Email inválido" + else -> null + } + } + + private fun validatePassword(password: String): String? { + return when { + password.isBlank() -> "Senha é obrigatória" + password.length < 6 -> "Senha deve ter pelo menos 6 caracteres" + !countPassword(password) -> "Senha deve conter pelo menos 1 letra e 1 número" + else -> null + } + } + + private fun isValidEmailFormat(email: String): Boolean { + return Regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$").matches(email) + } + + private fun countPassword(password: String): Boolean { + return password.length >= 6 + } } diff --git a/app/src/main/java/com/delecrode/devhub/ui/login/LoginScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/login/LoginScreen.kt index 9f86180..b97dfc1 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/login/LoginScreen.kt @@ -71,6 +71,18 @@ fun LoginScreen(navController: NavController, viewModel: AuthViewModel) { } } + LaunchedEffect(email) { + if (state.emailError != null) { + viewModel.clearEmailError() + } + } + + LaunchedEffect(password) { + if (state.passwordError != null) { + viewModel.clearPasswordError() + } + } + Scaffold( topBar = { @@ -111,8 +123,8 @@ fun LoginScreen(navController: NavController, viewModel: AuthViewModel) { value = email, onValueChange = { email = it }, imeAction = ImeAction.Next, - //isError = uiState.emailError != null, - //errorMessage = uiState.emailError ?: "" + isError = state.emailError != null, + errorMessage = state.emailError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) @@ -132,8 +144,8 @@ fun LoginScreen(navController: NavController, viewModel: AuthViewModel) { imeAction = ImeAction.Done, isPasswordVisible = passwordVisible, onVisibilityChange = { passwordVisible = !passwordVisible }, - //isError = uiState.passwordError != null, - //errorMessage = uiState.passwordError ?: "" + isError = state.passwordError != null, + errorMessage = state.passwordError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) @@ -156,7 +168,7 @@ fun LoginScreen(navController: NavController, viewModel: AuthViewModel) { onClick = { viewModel.signIn(email, password) }, - enabled = email.isNotBlank() && password.isNotBlank() + enabled = state.canLogin && email.isNotBlank() && password.isNotBlank() ) } } diff --git a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt index 256cda3..767d740 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt @@ -64,7 +64,6 @@ fun ProfileScreen(navController: NavController, viewModel: ProfileViewModel) { val state = viewModel.uiState.collectAsState() val user = state.value.userForGit - val userName = state.value.userForFirebase.username val repos = state.value.repos LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt index 1562025..24f4925 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt @@ -3,81 +3,84 @@ package com.delecrode.devhub.ui.profile import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.delecrode.devhub.domain.model.Repos import com.delecrode.devhub.domain.repository.AuthRepository import com.delecrode.devhub.domain.repository.UserRepository +import com.delecrode.devhub.utils.Result import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -class ProfileViewModel(private val userRepository: UserRepository, private val authRepository: AuthRepository) : ViewModel() { +class ProfileViewModel( + private val userRepository: UserRepository, + private val authRepository: AuthRepository +) : ViewModel() { private val _uiState = MutableStateFlow(ProfileState()) val uiState: StateFlow = _uiState - init{ - getUserForFirebase() - } fun getUserForFirebase() { _uiState.value = _uiState.value.copy( isLoading = true ) viewModelScope.launch { - try { - val userForFirebase = userRepository.getUserForFirebase() - _uiState.value = _uiState.value.copy( - userForFirebase = userForFirebase - ) - - if (userForFirebase.username.isNotBlank()) { - getUserForGit(userForFirebase.username) - getRepos(userForFirebase.username) - } else { - _uiState.value = _uiState.value.copy(isLoading = false) + when (val result = userRepository.getUserForFirebase()) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + userForFirebase = result.data, + isLoading = false + ) + getUserForGit(result.data.username) } - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false - ) + is Result.Error -> { + _uiState.value = _uiState.value.copy( + error = result.message, + isLoading = false + ) + } } } } fun getUserForGit(userName: String) { viewModelScope.launch { - try { - val userForGit = userRepository.getUserForGitHub(userName) - _uiState.value = _uiState.value.copy( - userForGit = userForGit, - isLoading = false - ) - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false - ) + when (val result = userRepository.getUserForGitHub(userName)) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + userForGit = result.data, + isLoading = false + ) + getRepos(userName) + } + + is Result.Error -> { + _uiState.value = _uiState.value.copy( + error = result.message, + isLoading = false + ) + } } } } fun getRepos(userName: String) { viewModelScope.launch { - try { - val repos: List = userRepository.getRepos(userName) - _uiState.value = _uiState.value.copy( - repos = repos, - isLoading = false - ) - } catch (e: Exception) { - Log.e("HomeViewModel", "Erro ao buscar repositórios", e) - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false - ) + when (val result = userRepository.getRepos(userName)) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + repos = result.data, + isLoading = false + ) + } + + is Result.Error -> { + _uiState.value = _uiState.value.copy( + error = result.message, + isLoading = false + ) + } } } } @@ -96,11 +99,11 @@ class ProfileViewModel(private val userRepository: UserRepository, private val a } } - fun clearState(){ + fun clearState() { _uiState.value = _uiState.value.copy( isLoading = false, error = null ) } - } + diff --git a/app/src/main/java/com/delecrode/devhub/ui/register/RegisterScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterScreen.kt index a3f4521..b1054d9 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/register/RegisterScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterScreen.kt @@ -75,6 +75,18 @@ fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { } } + LaunchedEffect(email) { + if (state.emailError != null) { + viewModel.clearEmailError() + } + } + + LaunchedEffect(password) { + if (state.passwordError != null) { + viewModel.clearPasswordError() + } + } + Scaffold( @@ -140,7 +152,9 @@ fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { label = "Nome Completo", leadingIcon = Icons.Default.Person, keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next + imeAction = ImeAction.Next, + isError = state.nameError != null, + errorMessage = state.nameError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) @@ -159,7 +173,9 @@ fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { label = "Nome de Usuario", leadingIcon = Icons.Default.Person, keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next + imeAction = ImeAction.Next, + isError = state.usernameError != null, + errorMessage = state.usernameError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) @@ -177,8 +193,8 @@ fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { value = email, onValueChange = { email = it }, imeAction = ImeAction.Next, - //isError = uiState.emailError != null, - //errorMessage = uiState.emailError ?: "" + isError = state.emailError != null, + errorMessage = state.emailError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) @@ -198,8 +214,8 @@ fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { imeAction = ImeAction.Next, isPasswordVisible = passwordVisible, onVisibilityChange = { passwordVisible = !passwordVisible }, - //isError = uiState.passwordError != null, - //errorMessage = uiState.passwordError ?: "" + isError = state.passwordError != null, + errorMessage = state.passwordError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) @@ -222,9 +238,9 @@ fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { onVisibilityChange = { passwordConfirmVisible = !passwordConfirmVisible }, - label = "Confirmar Senha" - //isError = uiState.passwordError != null, - //errorMessage = uiState.passwordError ?: "" + label = "Confirmar Senha", + isError = state.confirmPasswordError != null, + errorMessage = state.confirmPasswordError ?: "" ) Spacer(modifier = Modifier.height(16.dp)) @@ -236,10 +252,11 @@ fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { name = name, username = userName, email = email, - password = password + password = password, + confirmPassword = confirmPassword ) }, - enabled = email.isNotBlank() && password.isNotBlank() && password == confirmPassword + enabled = state.canRegister && name.isNotBlank() && userName.isNotBlank() && email.isNotBlank() && password.isNotBlank() ) } } diff --git a/app/src/main/java/com/delecrode/devhub/ui/register/RegisterState.kt b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterState.kt index c241785..8df8121 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/register/RegisterState.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterState.kt @@ -4,4 +4,17 @@ data class RegisterState( val isLoading: Boolean = false, val isSuccess: Boolean = false, val error: String? = null, -) + + val nameError: String? = null, + val usernameError: String? = null, + val emailError: String? = null, + val passwordError: String? = null, + val confirmPasswordError: String? = null +) { + val hasValidationErrors: Boolean + get() = emailError != null || passwordError != null + + val canRegister: Boolean + get() = !isLoading && !hasValidationErrors && error == null +} + diff --git a/app/src/main/java/com/delecrode/devhub/ui/register/RegisterViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterViewModel.kt index 2d696c2..2796dfc 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/register/RegisterViewModel.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterViewModel.kt @@ -3,6 +3,7 @@ package com.delecrode.devhub.ui.register import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.delecrode.devhub.domain.repository.AuthRepository +import com.delecrode.devhub.utils.Result import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -14,27 +15,103 @@ class RegisterViewModel( private val _state = MutableStateFlow(RegisterState()) val state = _state.asStateFlow() - fun signUp(name: String, username: String, email: String, password: String) { - _state.value = RegisterState(isLoading = true) + fun signUp(name: String, username: String, email: String, password: String, confirmPassword: String) { viewModelScope.launch { - try{ - val response = repository.signUp(name, username, email, password) - _state.value = RegisterState( - isSuccess = response, - isLoading = false + validateName(name)?.let { + _state.value = _state.value.copy(nameError = it) + return@launch + } + + validateUsername(username)?.let { + _state.value = _state.value.copy(usernameError = it) + return@launch + } + val emailValidation = validateEmail(email) + if (emailValidation != null) { + _state.value = _state.value.copy( + emailError = emailValidation, + passwordError = null, + error = null ) - }catch (e: Exception){ - _state.value = RegisterState( - error = e.message, - isLoading = false + return@launch + } + + val passwordValidation = validatePassword(password) + if (passwordValidation != null) { + _state.value = _state.value.copy( + emailError = null, + passwordError = passwordValidation, + error = null ) + return@launch } - } + validateConfirmPassword(password, confirmPassword)?.let { + _state.value = _state.value.copy(confirmPasswordError = it) + return@launch + } + + when (val result = repository.signUp(name, username, email, password)) { + is Result.Success -> { + _state.value = RegisterState(isSuccess = true) + } + is Result.Error -> { + _state.value = RegisterState(error = result.message) + } + } + } } + fun clearState() { _state.value = RegisterState() } + + private fun validateName(name: String): String? = + if (name.isBlank()) "Nome é obrigatório" else null + + private fun validateUsername(username: String): String? = + if (username.isBlank()) "Username é obrigatório" else null + + private fun validateEmail(email: String): String? = + when { + email.isBlank() -> "Email é obrigatório" + !Regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$").matches(email) -> + "Email inválido" + + else -> null + } + + private fun validatePassword(password: String): String? = + when { + password.isBlank() -> "Senha é obrigatória" + password.length < 6 -> "Senha deve ter pelo menos 6 caracteres" + !password.any { it.isDigit() } -> + "Senha deve conter pelo menos 1 número" + + else -> null + } + + private fun validateConfirmPassword( + password: String, + confirmPassword: String + ): String? = + when { + confirmPassword.isBlank() -> "Confirme a senha" + password != confirmPassword -> "As senhas devem ser iguais" + else -> null + } + + + + + fun clearEmailError() { + _state.value = _state.value.copy(emailError = null) + } + + fun clearPasswordError() { + _state.value = _state.value.copy(passwordError = null) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt index fa9da5c..ed82a47 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt @@ -33,9 +33,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -63,8 +60,6 @@ fun RepoDetailScreen( ) { Log.i("RepoDetailScreen", "RepoDetailScreen: $owner, $repo") - var isFavorite by remember { mutableStateOf(false) } - val state by viewModel.uiState.collectAsState() val context = LocalContext.current @@ -97,23 +92,26 @@ fun RepoDetailScreen( actions = { IconButton( onClick = { - isFavorite = !isFavorite - viewModel.favoriteRepo( - id = state.repo?.id ?: 0, - name = state.repo?.name ?: "", - owner = owner, - description = state.repo?.description ?: "", - url = state.repo?.html_url ?: "" - ) + if (state.isFavorite) { + viewModel.deleteRepo(state.repo?.id ?: 0) + } else { + viewModel.favoriteRepo( + id = state.repo?.id ?: 0, + name = state.repo?.name ?: "", + owner = owner, + description = state.repo?.description ?: "", + url = state.repo?.html_url ?: "" + ) + } } ) { Icon( - imageVector = if (isFavorite) + imageVector = if (state.isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, contentDescription = "Favorito", - tint = if (isFavorite) + tint = if (state.isFavorite) Color.Red else MaterialTheme.colorScheme.onBackground diff --git a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt index a43caa2..f919d9a 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt @@ -7,5 +7,6 @@ data class RepoState( val error: String? = null, val userName: String? = null, val repo: RepoDetail? = null, - val languages: List? = null + val languages: List? = null, + val isFavorite: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt index a054b28..6ce6e89 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt @@ -4,8 +4,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.delecrode.devhub.domain.model.RepoFav import com.delecrode.devhub.domain.repository.RepoRepository +import com.delecrode.devhub.utils.Result import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch class RepoDetailViewModel(val repository: RepoRepository) : ViewModel() { @@ -19,56 +21,87 @@ class RepoDetailViewModel(val repository: RepoRepository) : ViewModel() { isLoading = true, error = null ) - try { - val repoDetail = repository.getRepoDetail(owner, repo) - _uiState.value = _uiState.value.copy( - repo = repoDetail, - isLoading = false - ) + checkIfFavorite(repo) + when (val result = repository.getRepoDetail(owner, repo)) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + repo = result.data, + isLoading = false, + error = null + ) + } + is Result.Error -> { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = result.message + ) + } + } + } + } + + private fun checkIfFavorite(repoName: String) { + viewModelScope.launch { + try { + val favorites = repository.getAll().first() + val isFav = favorites.any { it.name == repoName } + _uiState.value = _uiState.value.copy(isFavorite = isFav) } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - error = e.message, - isLoading = false, - ) + _uiState.value = _uiState.value.copy(isFavorite = false) } } } - fun getLanguagesForRepo(owner: String, repo: String){ + fun getLanguagesForRepo(owner: String, repo: String) { viewModelScope.launch { - try{ - val result = repository.getLanguagesRepo(owner, repo) - _uiState.value = _uiState.value.copy( - languages = result.languages - ) - }catch (e: Exception){ - _uiState.value = _uiState.value.copy( - languages = emptyList() - ) + when (val result = repository.getLanguagesRepo(owner, repo)) { + is Result.Success -> { + _uiState.value = _uiState.value.copy( + languages = result.data.languages, + isLoading = false, + error = null + ) + } + + is Result.Error -> { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = result.message + ) + } } } } - fun clearState(){ + fun clearState() { _uiState.value = _uiState.value.copy( + repo = null, + languages = emptyList(), isLoading = false, error = null ) } - fun favoriteRepo(id: Int, owner: String, name: String, description: String, url: String) { viewModelScope.launch { repository.save( RepoFav( id = id, name = name, - userName = owner ?: "", + userName = owner, description = description, url = url ) ) + _uiState.value = _uiState.value.copy(isFavorite = true) + } + } + + fun deleteRepo(id: Int) { + viewModelScope.launch { + repository.delete(id) + _uiState.value = _uiState.value.copy(isFavorite = false) } } } diff --git a/app/src/main/java/com/delecrode/devhub/utils/HttpMap.kt b/app/src/main/java/com/delecrode/devhub/utils/HttpMap.kt new file mode 100644 index 0000000..16ce048 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/utils/HttpMap.kt @@ -0,0 +1,52 @@ +package com.delecrode.devhub.utils + +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException +import com.google.firebase.auth.FirebaseAuthInvalidUserException +import com.google.firebase.auth.FirebaseAuthUserCollisionException +import com.google.firebase.auth.FirebaseAuthWeakPasswordException + + +fun mapHttpError(code: Int): String = + when (code) { + 404 -> "Repositório não encontrado" + 403 -> "Limite da API atingido" + in 500..599 -> "Servidor indisponível" + else -> "Erro ao buscar dados" + } + + +fun mapAuthError(e: Exception): String = + when (e) { + is FirebaseAuthInvalidUserException -> + "Usuário não encontrado." + + is FirebaseAuthInvalidCredentialsException -> + "E-mail ou senha inválidos." + + is FirebaseAuthUserCollisionException -> + "Este e-mail já está em uso." + + is FirebaseAuthWeakPasswordException -> + "A senha deve ter no mínimo 6 caracteres." + + else -> + "Erro ao realizar login. Tente novamente." + } + +fun mapSignUpError(e: Exception): String = + when (e) { + is FirebaseAuthUserCollisionException -> + "Este e-mail já está em uso." + + is FirebaseAuthWeakPasswordException -> + "A senha deve ter no mínimo 6 caracteres." + + is FirebaseAuthInvalidCredentialsException -> + "E-mail inválido." + + else -> + "Erro ao criar conta. Tente novamente." + } + + + diff --git a/app/src/main/java/com/delecrode/devhub/utils/Result.kt b/app/src/main/java/com/delecrode/devhub/utils/Result.kt new file mode 100644 index 0000000..17445cd --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/utils/Result.kt @@ -0,0 +1,6 @@ +package com.delecrode.devhub.utils + +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val message: String) : Result() +}