From 89bbad59c102423f45689d4592347ffbf430d12c Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 13:18:34 -0300 Subject: [PATCH 1/8] =?UTF-8?q?Implementado=20fluxo=20de=20autentica=C3=A7?= =?UTF-8?q?=C3=A3o=20e=20componentes=20de=20UI=20reutiliz=C3=A1veis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adicionadas telas de `LoginScreen`, `RegisterScreen` e `ForgotPasswordScreen` para gerenciar o acesso do usuário. - Atualizado `AppNavHost` para incluir as novas rotas de autenticação e definir Login como destino inicial. - Criados componentes de UI reutilizáveis: `PrimaryButton`, `EmailTextField`, `GenericOutlinedTextField` e `PasswordTextField` (com toggle de visibilidade). - Expandido `AppDestinations` com as rotas `Register`, `Login` e `ForgotPassword`. - Adicionados assets vetoriais para ícones de visibilidade de senha. --- .../devhub/navigation/AppDestinations.kt | 3 + .../delecrode/devhub/navigation/AppNavHost.kt | 30 +- .../delecrode/devhub/ui/components/Button.kt | 53 ++++ .../devhub/ui/components/OutlineTextField.kt | 295 ++++++++++++++++++ .../devhub/ui/forgot/ForgotPasswordScreen.kt | 105 +++++++ .../delecrode/devhub/ui/login/LoginScreen.kt | 144 +++++++++ .../devhub/ui/register/RegisterScreen.kt | 230 ++++++++++++++ .../res/drawable/ic_visibility_off_24.xml | 5 + .../main/res/drawable/ic_visibility_on_24.xml | 5 + 9 files changed, 865 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/delecrode/devhub/ui/components/Button.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/components/OutlineTextField.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordScreen.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/login/LoginScreen.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/register/RegisterScreen.kt create mode 100644 app/src/main/res/drawable/ic_visibility_off_24.xml create mode 100644 app/src/main/res/drawable/ic_visibility_on_24.xml diff --git a/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt b/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt index 6d54d65..4e76dd4 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt @@ -7,4 +7,7 @@ sealed class AppDestinations(val route: String) { object RepoDetail : AppDestinations("repoDetail/{owner}/{repo}"){ fun createRoute(owner: String, repo: String) = "repoDetail/$owner/$repo" } + object Register: AppDestinations("register") + object Login: AppDestinations("login") + object ForgotPassword: AppDestinations("forgotPassword") } \ No newline at end of file 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 5a144fa..dfbedb4 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt @@ -6,8 +6,11 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import com.delecrode.devhub.ui.forgot.ForgotPasswordScreen import com.delecrode.devhub.ui.home.HomeScreen import com.delecrode.devhub.ui.home.HomeViewModel +import com.delecrode.devhub.ui.login.LoginScreen +import com.delecrode.devhub.ui.register.RegisterScreen import com.delecrode.devhub.ui.repo.RepoDetailScreen import com.delecrode.devhub.ui.repo.RepoDetailViewModel import org.koin.androidx.compose.koinViewModel @@ -21,22 +24,39 @@ fun AppNavHost() { val repoViewModel: RepoDetailViewModel = koinViewModel() - NavHost(navController = navController, startDestination = AppDestinations.Home.route) { - //Profile Flow + NavHost(navController = navController, startDestination = AppDestinations.Login.route) { + //Home Flow composable(AppDestinations.Home.route) { HomeScreen(navController, profileViewModel) } + //Repositorio Flow composable( AppDestinations.RepoDetail.route, arguments = listOf( - navArgument("owner") { type = NavType.StringType }, - navArgument("repo"){ type = NavType.StringType} - )) { + navArgument("owner") { type = NavType.StringType }, + navArgument("repo") { type = NavType.StringType } + )) { val owner = it.arguments?.getString("owner") ?: "" val repo = it.arguments?.getString("repo") ?: "" RepoDetailScreen(navController, repoViewModel, owner, repo) } + + //Register Flow + composable(AppDestinations.Register.route) { + RegisterScreen(navController) + } + + //Login Flow + composable(AppDestinations.Login.route) { + LoginScreen(navController) + } + + //Forgot Password Flow + composable(AppDestinations.ForgotPassword.route) { + ForgotPasswordScreen(navController) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/components/Button.kt b/app/src/main/java/com/delecrode/devhub/ui/components/Button.kt new file mode 100644 index 0000000..a6e25d5 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/components/Button.kt @@ -0,0 +1,53 @@ +package com.delecrode.devhub.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.delecrode.devhub.ui.theme.DevHubTheme +import com.delecrode.devhub.ui.theme.PrimaryBlue + + +@Composable +fun PrimaryButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + Button( + onClick = onClick, + enabled = enabled, + modifier = modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = PrimaryBlue + ), + shape = RoundedCornerShape(8.dp), + ) { + Text( + text = text, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } +} + +@Preview +@Composable +private fun PrimaryButtonPreview() { + DevHubTheme() { + PrimaryButton(text = "CONFIRMAR", onClick = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/components/OutlineTextField.kt b/app/src/main/java/com/delecrode/devhub/ui/components/OutlineTextField.kt new file mode 100644 index 0000000..08f124c --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/components/OutlineTextField.kt @@ -0,0 +1,295 @@ +package com.delecrode.devhub.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +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.focus.FocusDirection +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.delecrode.devhub.R +import com.delecrode.devhub.ui.theme.DevHubTheme +import com.delecrode.devhub.ui.theme.PrimaryBlue + +@Composable +fun EmailTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String = "Email", + isError: Boolean = false, + errorMessage: String = "", + imeAction: ImeAction = ImeAction.Next +) { + val focusManager = LocalFocusManager.current + + Column(modifier = Modifier.fillMaxWidth()) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.background, + RoundedCornerShape(8.dp) + ), + placeholder = { Text(label) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + tint = PrimaryBlue + ) + }, + isError = isError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = imeAction + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) }, + onDone = { focusManager.clearFocus() } + ), + singleLine = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.background, + unfocusedContainerColor = MaterialTheme.colorScheme.background, + disabledContainerColor = MaterialTheme.colorScheme.background, + focusedIndicatorColor = MaterialTheme.colorScheme.onBackground, + unfocusedIndicatorColor = MaterialTheme.colorScheme.onBackground, + errorIndicatorColor = MaterialTheme.colorScheme.error + ), + shape = RoundedCornerShape(8.dp) + ) + + if (isError && errorMessage.isNotEmpty()) { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun EmailTextFieldPreview() { + DevHubTheme() { + EmailTextField( + value = "user@example.com", + onValueChange = {}, + modifier = Modifier.padding(16.dp) + ) + } +} + + +@Composable +fun PasswordTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String = "Senha", + isError: Boolean = false, + errorMessage: String = "", + imeAction: ImeAction = ImeAction.Done, + isPasswordVisible: Boolean = false, + onVisibilityChange: (() -> Unit)? = null +) { + val focusManager = LocalFocusManager.current + var passwordVisible by remember { mutableStateOf(isPasswordVisible) } + + Column(modifier = Modifier.fillMaxWidth()) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background, RoundedCornerShape(8.dp)), + placeholder = { Text(label) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Lock, + contentDescription = null, + tint = PrimaryBlue + ) + }, + trailingIcon = { + IconButton(onClick = { + if (onVisibilityChange != null) { + onVisibilityChange() + } else { + passwordVisible = !passwordVisible + } + }) { + Icon( + painter = if (onVisibilityChange != null) { + if (isPasswordVisible) painterResource(R.drawable.ic_visibility_off_24) else painterResource( + R.drawable.ic_visibility_on_24 + ) + } else { + if (passwordVisible) painterResource(R.drawable.ic_visibility_off_24) else painterResource( + R.drawable.ic_visibility_on_24 + ) + }, + contentDescription = if (onVisibilityChange != null) { + if (isPasswordVisible) "Hide password" else "Show password" + } else { + if (passwordVisible) "Hide password" else "Show password" + }, + tint = PrimaryBlue + ) + } + }, + isError = isError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = imeAction + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) }, + onDone = { focusManager.clearFocus() } + ), + singleLine = true, + visualTransformation = if (onVisibilityChange != null) { + if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation() + } else { + if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation() + }, + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.background, + unfocusedContainerColor = MaterialTheme.colorScheme.background, + disabledContainerColor = MaterialTheme.colorScheme.background, + focusedIndicatorColor = MaterialTheme.colorScheme.onBackground, + unfocusedIndicatorColor = MaterialTheme.colorScheme.onBackground, + errorIndicatorColor = MaterialTheme.colorScheme.error + ), + shape = RoundedCornerShape(8.dp) + ) + + if (isError && errorMessage.isNotEmpty()) { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun PasswordTextFieldPreview() { + DevHubTheme() { + PasswordTextField( + value = "password123", + onValueChange = {}, + modifier = Modifier.padding(16.dp) + ) + } +} + +@Composable +fun GenericOutlinedTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String, + leadingIcon: ImageVector? = null, + isError: Boolean = false, + errorMessage: String = "", + keyboardType: KeyboardType = KeyboardType.Text, + imeAction: ImeAction = ImeAction.Next, +) { + val focusManager = LocalFocusManager.current + + Column(modifier = Modifier.fillMaxWidth()) { + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier.fillMaxWidth(), + placeholder = { Text(label) }, + leadingIcon = { + leadingIcon?.let { + Icon( + imageVector = it, + contentDescription = null, + tint = PrimaryBlue + ) + } + }, + isError = isError, + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, + imeAction = imeAction + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) }, + onDone = { focusManager.clearFocus() } + ), + singleLine = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.background, + unfocusedContainerColor = MaterialTheme.colorScheme.background, + disabledContainerColor = MaterialTheme.colorScheme.background, + focusedIndicatorColor = MaterialTheme.colorScheme.onBackground, + unfocusedIndicatorColor = MaterialTheme.colorScheme.onBackground, + errorIndicatorColor = MaterialTheme.colorScheme.error + ), + shape = RoundedCornerShape(8.dp) + ) + + if (isError && errorMessage.isNotEmpty()) { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + } +} + + +@Preview(showBackground = true) +@Composable +fun GenericOutlinedTextFieldPreview() { + DevHubTheme() { + GenericOutlinedTextField( + value = "password123", + onValueChange = {}, + label = "Usuario" + ) + } +} + + 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 new file mode 100644 index 0000000..5d790a7 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/forgot/ForgotPasswordScreen.kt @@ -0,0 +1,105 @@ +package com.delecrode.devhub.ui.forgot + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ForgotPasswordScreen(navController: NavController) { + + var email by remember { mutableStateOf("") } + + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = "Recuperar Senha", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + navigationIcon = { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Voltar", + tint = MaterialTheme.colorScheme.onBackground + ) + } + } + ) + } + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + Column(modifier = Modifier.padding(8.dp)) { + Text( + "Digite seu e-mail cadastrado " + + "\npara que possamos enviar um link " + + "para que você possa criar uma nova senha", + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + EmailTextField( + value = email, + onValueChange = { email = it }, + label = "Email", + imeAction = ImeAction.Done + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PrimaryButton( + text = "Enviar e-mail", + onClick = { navController.navigate(AppDestinations.Login.route) }, + enabled = email.isNotBlank() + ) + } + } + } +} + +@Preview +@Composable +fun ForgotPasswordScreenPreview() { + ForgotPasswordScreen(rememberNavController()) +} \ No newline at end of file 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 new file mode 100644 index 0000000..de0e205 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/login/LoginScreen.kt @@ -0,0 +1,144 @@ +package com.delecrode.devhub.ui.login + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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 +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.delecrode.devhub.navigation.AppDestinations +import com.delecrode.devhub.ui.components.EmailTextField +import com.delecrode.devhub.ui.components.PasswordTextField +import com.delecrode.devhub.ui.components.PrimaryButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginScreen(navController: NavController) { + + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = "Login", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) + } + ) { padding -> + + Column(modifier = Modifier.padding(padding)) { + Column( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + Text( + text = "E-mail", + color = Color.Black, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + fontSize = 14.sp + ) + + EmailTextField( + value = email, + onValueChange = { email = it }, + imeAction = ImeAction.Next, + //isError = uiState.emailError != null, + //errorMessage = uiState.emailError ?: "" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Senha", + color = Color.Black, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + fontSize = 14.sp + ) + + PasswordTextField( + value = password, + onValueChange = { password = it }, + imeAction = ImeAction.Done, + isPasswordVisible = passwordVisible, + onVisibilityChange = { passwordVisible = !passwordVisible }, + //isError = uiState.passwordError != null, + //errorMessage = uiState.passwordError ?: "" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextButton(onClick = {navController.navigate(AppDestinations.ForgotPassword.route)}) { + Text("Esqueceu a senha?") + } + + TextButton(onClick = {navController.navigate(AppDestinations.Register.route)}) { + Text("Não tem conta? Cadastre-se") + } + } + + PrimaryButton( + text = "ENTRAR", + onClick = { + //authViewModel.login(email, password) + navController.navigate(AppDestinations.Home.route) + }, + enabled = email.isNotBlank() && password.isNotBlank() + ) + } + } + } +} + + +@Preview +@Composable +fun LoginScreenPreview() { + LoginScreen(rememberNavController()) + +} \ No newline at end of file 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 new file mode 100644 index 0000000..075acb4 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterScreen.kt @@ -0,0 +1,230 @@ +package com.delecrode.devhub.ui.register + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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.focus.focusModifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.delecrode.devhub.navigation.AppDestinations +import com.delecrode.devhub.ui.components.EmailTextField +import com.delecrode.devhub.ui.components.GenericOutlinedTextField +import com.delecrode.devhub.ui.components.PasswordTextField +import com.delecrode.devhub.ui.components.PrimaryButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RegisterScreen(navController: NavController) { + + var userName by remember { mutableStateOf("") } + var name by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + + var passwordVisible by remember { mutableStateOf(false) } + var passwordConfirmVisible by remember { mutableStateOf(false) } + + + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = "Cadastro", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + navigationIcon = { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Voltar", + tint = MaterialTheme.colorScheme.onBackground + ) + } + } + ) + } + ) { padding -> + + Column(modifier = Modifier.padding(padding)) { + + LazyColumn(modifier = Modifier.fillMaxWidth()) { + item { + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.Center + ) { + + Text( + "Preencha os campos abaixo para criar sua conta no DevHub " + + "\nO seu nome de usuario deve ser o mesmo do seu usuario do GitHub", + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Center, + fontSize = 14.sp + ) + + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Nome Completo", + color = Color.Black, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + fontSize = 14.sp + ) + + GenericOutlinedTextField( + value = name, + onValueChange = { name = it }, + label = "Nome Completo", + leadingIcon = Icons.Default.Person, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ) + + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Nome de Usuario", + color = Color.Black, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + fontSize = 14.sp + ) + + GenericOutlinedTextField( + value = userName, + onValueChange = { userName = it }, + label = "Nome de Usuario", + leadingIcon = Icons.Default.Person, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "E-mail", + color = Color.Black, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + fontSize = 14.sp + ) + + EmailTextField( + value = email, + onValueChange = { email = it }, + imeAction = ImeAction.Next, + //isError = uiState.emailError != null, + //errorMessage = uiState.emailError ?: "" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Senha", + color = Color.Black, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + fontSize = 14.sp + ) + + PasswordTextField( + value = password, + onValueChange = { password = it }, + imeAction = ImeAction.Next, + isPasswordVisible = passwordVisible, + onVisibilityChange = { passwordVisible = !passwordVisible }, + //isError = uiState.passwordError != null, + //errorMessage = uiState.passwordError ?: "" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + + Text( + text = "Confirmar Senha", + color = Color.Black, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + fontSize = 14.sp + ) + + PasswordTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + imeAction = ImeAction.Done, + isPasswordVisible = passwordConfirmVisible, + onVisibilityChange = { + passwordConfirmVisible = !passwordConfirmVisible + }, + label = "Confirmar Senha" + //isError = uiState.passwordError != null, + //errorMessage = uiState.passwordError ?: "" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PrimaryButton( + text = "Cadastrar", + onClick = { + //authViewModel.login(email, password) + navController.navigate(AppDestinations.Login.route) + }, + enabled = email.isNotBlank() && password.isNotBlank() && password == confirmPassword + ) + } + } + } + } + } +} + +@Preview +@Composable +fun RegisterScreenPreview() { + RegisterScreen(rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_visibility_off_24.xml b/app/src/main/res/drawable/ic_visibility_off_24.xml new file mode 100644 index 0000000..5993ca3 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_visibility_on_24.xml b/app/src/main/res/drawable/ic_visibility_on_24.xml new file mode 100644 index 0000000..f843e29 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_on_24.xml @@ -0,0 +1,5 @@ + + + + + From eff12e69d8f078cfeb70e92789cb742559437a49 Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 13:25:17 -0300 Subject: [PATCH 2/8] =?UTF-8?q?Aqui=20est=C3=A1=20a=20mensagem=20de=20comm?= =?UTF-8?q?it=20sugerida=20com=20base=20nos=20diffs=20fornecidos:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adicionando configuração do Firebase Analytics e limpeza de imports - Configurado plugin `google-services` no `build.gradle.kts` (raiz e app). - Adicionado dependências do Firebase BOM e Analytics no `app/build.gradle.kts`. - Incluído arquivo de configuração `google-services.json`. - Removido import não utilizado em `RegisterScreen.kt`. --- app/build.gradle.kts | 5 ++++ app/google-services.json | 29 +++++++++++++++++++ .../devhub/ui/register/RegisterScreen.kt | 1 - build.gradle.kts | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 app/google-services.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2b2dde9..07eece3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + id("com.google.gms.google-services") } android { @@ -48,6 +49,10 @@ android { dependencies { + //Firebase + implementation(platform("com.google.firebase:firebase-bom:34.6.0")) + implementation("com.google.firebase:firebase-analytics") + //Koil implementation("io.insert-koin:koin-android:3.5.6") implementation("io.insert-koin:koin-androidx-compose:3.5.6") diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..a590690 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1059643006132", + "project_id": "devhub-4912b", + "storage_bucket": "devhub-4912b.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1059643006132:android:459aaa892b0f25fdf6cbe1", + "android_client_info": { + "package_name": "com.delecrode.devhub" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCclTzmPM0pdKLYQQJESqx0ZHdXJ7-vxdM" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file 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 075acb4..df4898e 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 @@ -24,7 +24,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..f7b5371 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + id("com.google.gms.google-services") version "4.4.4" apply false } \ No newline at end of file From e24e67066f4a1915fe8b681c7748a47a30d094cc Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 16:25:25 -0300 Subject: [PATCH 3/8] =?UTF-8?q?Implementada=20autentica=C3=A7=C3=A3o=20com?= =?UTF-8?q?pleta=20com=20Firebase=20(Login,=20Cadastro=20e=20Sess=C3=A3o)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adicionado `AuthRepository`, wrapper de `FirebaseAuth` e `UserExtraData` para persistência no Firestore. - Implementados `AuthViewModel`, `RegisterViewModel` e `SessionViewModel` para gerenciamento de estados e sessão. - Integrada lógica de autenticação nas telas `LoginScreen` e `RegisterScreen` com feedback de erro e carregamento. - Atualizado `AppNavHost` para redirecionamento baseado no estado da sessão (logado/deslogado). - Configurada injeção de dependência dos novos componentes no `AppModule`. --- app/build.gradle.kts | 2 + .../java/com/delecrode/devhub/MainActivity.kt | 6 +- .../devhub/data/firebase/FirebaseAuth.kt | 42 ++++++++++++ .../devhub/data/firebase/UserExtraData.kt | 16 +++++ .../data/repository/AuthRepositoryImpl.kt | 66 +++++++++++++++++++ .../java/com/delecrode/devhub/di/AppModule.kt | 27 +++++++- .../com/delecrode/devhub/domain/model/User.kt | 12 ++++ .../domain/repository/AuthRepository.kt | 18 +++++ .../devhub/domain/session/SessionViewModel.kt | 24 +++++++ .../delecrode/devhub/navigation/AppNavHost.kt | 20 ++++-- .../delecrode/devhub/ui/login/AuthState.kt | 8 +++ .../devhub/ui/login/AuthViewModel.kt | 42 ++++++++++++ .../delecrode/devhub/ui/login/LoginScreen.kt | 50 +++++++++++--- .../devhub/ui/register/RegisterScreen.kt | 34 ++++++++-- .../devhub/ui/register/RegisterState.kt | 7 ++ .../devhub/ui/register/RegisterViewModel.kt | 41 ++++++++++++ 16 files changed, 395 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/delecrode/devhub/data/firebase/FirebaseAuth.kt create mode 100644 app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt create mode 100644 app/src/main/java/com/delecrode/devhub/data/repository/AuthRepositoryImpl.kt create mode 100644 app/src/main/java/com/delecrode/devhub/domain/repository/AuthRepository.kt create mode 100644 app/src/main/java/com/delecrode/devhub/domain/session/SessionViewModel.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/login/AuthState.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/login/AuthViewModel.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/register/RegisterState.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/register/RegisterViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07eece3..f6f06be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,6 +52,8 @@ dependencies { //Firebase implementation(platform("com.google.firebase:firebase-bom:34.6.0")) implementation("com.google.firebase:firebase-analytics") + implementation("com.google.firebase:firebase-firestore") + implementation("com.google.firebase:firebase-auth") //Koil implementation("io.insert-koin:koin-android:3.5.6") diff --git a/app/src/main/java/com/delecrode/devhub/MainActivity.kt b/app/src/main/java/com/delecrode/devhub/MainActivity.kt index 3a2e6d1..71fec80 100644 --- a/app/src/main/java/com/delecrode/devhub/MainActivity.kt +++ b/app/src/main/java/com/delecrode/devhub/MainActivity.kt @@ -4,16 +4,20 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import com.delecrode.devhub.domain.session.SessionViewModel import com.delecrode.devhub.navigation.AppNavHost import com.delecrode.devhub.ui.theme.DevHubTheme +import org.koin.androidx.compose.koinViewModel class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { DevHubTheme { - AppNavHost() + val sessionViewModel : SessionViewModel = koinViewModel() + AppNavHost(sessionViewModel) } } } diff --git a/app/src/main/java/com/delecrode/devhub/data/firebase/FirebaseAuth.kt b/app/src/main/java/com/delecrode/devhub/data/firebase/FirebaseAuth.kt new file mode 100644 index 0000000..2090d55 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/firebase/FirebaseAuth.kt @@ -0,0 +1,42 @@ +package com.delecrode.devhub.data.firebase + +import com.google.firebase.auth.FirebaseUser +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import com.google.firebase.auth.FirebaseAuth as GoogleFirebaseAuth + +class FirebaseAuth( + private val auth: GoogleFirebaseAuth +) { + + suspend fun signIn(email: String, password: String): FirebaseUser? { + return suspendCoroutine { cont -> + auth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + cont.resume(task.result?.user) + } else { + cont.resumeWithException(task.exception ?: Exception("Erro desconhecido ao fazer login")) + } + } + } + } + + suspend fun signUp(email: String, password: String): String? { + return suspendCoroutine { cont -> + auth.createUserWithEmailAndPassword(email, password) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + cont.resume(task.result?.user?.uid) + } else { + cont.resumeWithException(task.exception ?: Exception("Erro desconhecido ao cadastrar")) + } + } + } + } + + fun signOut() { + auth.signOut() + } +} diff --git a/app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt b/app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt new file mode 100644 index 0000000..0bbe527 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt @@ -0,0 +1,16 @@ +package com.delecrode.devhub.data.firebase + +import com.delecrode.devhub.domain.model.RegisterUser +import com.google.firebase.firestore.FirebaseFirestore + + +class UserExtraData( + private val firestore: FirebaseFirestore +) { + + suspend fun saveUserData(uid: String, user: RegisterUser) { + firestore.collection("users") + .document(uid) + .set(user) + } +} 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 new file mode 100644 index 0000000..432c317 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/repository/AuthRepositoryImpl.kt @@ -0,0 +1,66 @@ +package com.delecrode.devhub.data.repository + +import com.delecrode.devhub.data.firebase.FirebaseAuth +import com.delecrode.devhub.data.firebase.UserExtraData +import com.delecrode.devhub.domain.model.RegisterUser +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 + +class AuthRepositoryImpl( + private val authDataSource: FirebaseAuth, + private val userExtraDataSource: UserExtraData +) : AuthRepository { + + override suspend fun signIn(email: String, password: String): FirebaseUser { + try { + val response = authDataSource.signIn(email, password) + return response ?: throw Exception("Erro ao recuperar usuário após login") + } 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) + } + } + + override suspend fun signUp( + name: String, + username: String, + email: String, + password: String + ): Boolean { + try { + val uid = authDataSource.signUp(email, password) ?: return false + + val userData = RegisterUser( + fullName = name, + username = username, + email = email + ) + + userExtraDataSource.saveUserData(uid, userData) + + return true + } 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) + } + } + + override suspend fun signOut() { + authDataSource.signOut() + } +} 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 f734805..0acf21d 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -1,23 +1,46 @@ package com.delecrode.devhub.di +import com.delecrode.devhub.data.firebase.UserExtraData import com.delecrode.devhub.data.remote.RetrofitInstance +import com.delecrode.devhub.data.repository.AuthRepositoryImpl import com.delecrode.devhub.data.repository.RepoRepositoryImpl import com.delecrode.devhub.data.repository.UserRepositoryImpl +import com.delecrode.devhub.domain.repository.AuthRepository 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.home.HomeViewModel +import com.delecrode.devhub.ui.login.AuthViewModel +import com.delecrode.devhub.ui.register.RegisterViewModel import com.delecrode.devhub.ui.repo.RepoDetailViewModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore import org.koin.dsl.module +import org.koin.androidx.viewmodel.dsl.viewModel + val appModule = module { + single { FirebaseAuth.getInstance() } + single { FirebaseFirestore.getInstance() } + single { RetrofitInstance.userApi } single { RetrofitInstance.repoApi } + + + // Seus data sources customizados + single { com.delecrode.devhub.data.firebase.FirebaseAuth(get()) } + single { UserExtraData(get()) } + single { UserRepositoryImpl(get()) } single { RepoRepositoryImpl(get()) } + single { AuthRepositoryImpl(get(), get()) } - single { HomeViewModel(get()) } - single { RepoDetailViewModel(get()) } + viewModel{ HomeViewModel(get()) } + viewModel { RepoDetailViewModel(get()) } + viewModel { AuthViewModel(get()) } + viewModel { SessionViewModel(get()) } + viewModel { RegisterViewModel(get()) } } \ No newline at end of file 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 28e3da4..f225074 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 @@ -1,5 +1,7 @@ package com.delecrode.devhub.domain.model + +//User for GitHub data class User( val login: String?, val avatar_url : String?, @@ -8,3 +10,13 @@ data class User( val bio: String?, val repos_url : String? ) + + +//User For Firebase +data class RegisterUser( + val fullName: String = "", + val username: String = "", + val birthDate: 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 new file mode 100644 index 0000000..5113038 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/domain/repository/AuthRepository.kt @@ -0,0 +1,18 @@ +package com.delecrode.devhub.domain.repository + +import com.google.firebase.auth.FirebaseUser + +interface AuthRepository { + + suspend fun signIn(email: String, password: String): FirebaseUser + + suspend fun signUp( + name: String, + username: String, + email: String, + password: String + ): Boolean + suspend fun signOut() + + +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/domain/session/SessionViewModel.kt b/app/src/main/java/com/delecrode/devhub/domain/session/SessionViewModel.kt new file mode 100644 index 0000000..148c540 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/domain/session/SessionViewModel.kt @@ -0,0 +1,24 @@ +package com.delecrode.devhub.domain.session + +import androidx.lifecycle.ViewModel +import com.google.firebase.auth.FirebaseAuth +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class SessionViewModel( + private val auth: FirebaseAuth +) : ViewModel() { + + private val _isLoggedIn = MutableStateFlow(auth.currentUser != null) + val isLoggedIn: StateFlow = _isLoggedIn + + init { + auth.addAuthStateListener { + _isLoggedIn.value = it.currentUser != null + } + } + + fun signOut() { + auth.signOut() + } +} \ No newline at end of file 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 dfbedb4..b3f7ca8 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt @@ -1,30 +1,42 @@ package com.delecrode.devhub.navigation import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import com.delecrode.devhub.domain.session.SessionViewModel import com.delecrode.devhub.ui.forgot.ForgotPasswordScreen import com.delecrode.devhub.ui.home.HomeScreen import com.delecrode.devhub.ui.home.HomeViewModel +import com.delecrode.devhub.ui.login.AuthViewModel import com.delecrode.devhub.ui.login.LoginScreen import com.delecrode.devhub.ui.register.RegisterScreen +import com.delecrode.devhub.ui.register.RegisterViewModel import com.delecrode.devhub.ui.repo.RepoDetailScreen import com.delecrode.devhub.ui.repo.RepoDetailViewModel import org.koin.androidx.compose.koinViewModel @Composable -fun AppNavHost() { +fun AppNavHost(sessionViewModel: SessionViewModel) { val navController = rememberNavController() val profileViewModel: HomeViewModel = koinViewModel() val repoViewModel: RepoDetailViewModel = koinViewModel() + val authViewModel: AuthViewModel = koinViewModel() + val registerViewModel: RegisterViewModel = koinViewModel() - NavHost(navController = navController, startDestination = AppDestinations.Login.route) { + val logged = sessionViewModel.isLoggedIn.collectAsState() + + + NavHost( + navController = navController, + startDestination = if (logged.value) AppDestinations.Home.route else AppDestinations.Login.route + ) { //Home Flow composable(AppDestinations.Home.route) { HomeScreen(navController, profileViewModel) @@ -45,12 +57,12 @@ fun AppNavHost() { //Register Flow composable(AppDestinations.Register.route) { - RegisterScreen(navController) + RegisterScreen(navController, registerViewModel) } //Login Flow composable(AppDestinations.Login.route) { - LoginScreen(navController) + LoginScreen(navController, authViewModel) } //Forgot Password Flow 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 new file mode 100644 index 0000000..8c67f4d --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/login/AuthState.kt @@ -0,0 +1,8 @@ +package com.delecrode.devhub.ui.login + +data class AuthState( + val isLoading: Boolean = false, + val isSuccess: Boolean = false, + val error: String? = null, + val userUid: String? = 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 new file mode 100644 index 0000000..1bc4e6c --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/login/AuthViewModel.kt @@ -0,0 +1,42 @@ +package com.delecrode.devhub.ui.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.delecrode.devhub.domain.repository.AuthRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class AuthViewModel( + private val repository: AuthRepository +) : ViewModel() { + + private val _state = MutableStateFlow(AuthState()) + 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, + isLoading = false + ) + } catch (e: Exception) { + _state.value = AuthState( + error = e.message, + isLoading = false + ) + } + } + } + + + fun clearState(){ + _state.value = AuthState() + } +} 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 de0e205..176765f 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 @@ -1,6 +1,9 @@ package com.delecrode.devhub.ui.login +import android.widget.Toast +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -8,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -15,6 +19,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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 @@ -22,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +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 @@ -34,15 +41,36 @@ import com.delecrode.devhub.navigation.AppDestinations import com.delecrode.devhub.ui.components.EmailTextField import com.delecrode.devhub.ui.components.PasswordTextField import com.delecrode.devhub.ui.components.PrimaryButton +import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LoginScreen(navController: NavController) { +fun LoginScreen(navController: NavController, viewModel: AuthViewModel) { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } var passwordVisible by remember { mutableStateOf(false) } + val state by viewModel.state.collectAsState() + val context = LocalContext.current + + + LaunchedEffect(state.error) { + if (state.error != null) { + Toast.makeText(context, state.error, Toast.LENGTH_SHORT).show() + viewModel.clearState() + } + } + + LaunchedEffect(state.isSuccess) { + if (state.isSuccess) { + navController.navigate(AppDestinations.Home.route) { + popUpTo(AppDestinations.Login.route) { inclusive = true } + } + } + } + + Scaffold( topBar = { CenterAlignedTopAppBar( @@ -71,7 +99,7 @@ fun LoginScreen(navController: NavController) { ) { Text( text = "E-mail", - color = Color.Black, + color = MaterialTheme.colorScheme.onBackground, modifier = Modifier .fillMaxWidth() .padding(bottom = 8.dp), @@ -90,7 +118,7 @@ fun LoginScreen(navController: NavController) { Text( text = "Senha", - color = Color.Black, + color = MaterialTheme.colorScheme.onBackground, modifier = Modifier .fillMaxWidth() .padding(bottom = 8.dp), @@ -113,11 +141,11 @@ fun LoginScreen(navController: NavController) { modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - TextButton(onClick = {navController.navigate(AppDestinations.ForgotPassword.route)}) { + TextButton(onClick = { navController.navigate(AppDestinations.ForgotPassword.route) }) { Text("Esqueceu a senha?") } - TextButton(onClick = {navController.navigate(AppDestinations.Register.route)}) { + TextButton(onClick = { navController.navigate(AppDestinations.Register.route) }) { Text("Não tem conta? Cadastre-se") } } @@ -125,13 +153,17 @@ fun LoginScreen(navController: NavController) { PrimaryButton( text = "ENTRAR", onClick = { - //authViewModel.login(email, password) - navController.navigate(AppDestinations.Home.route) + viewModel.signIn(email, password) }, enabled = email.isNotBlank() && password.isNotBlank() ) } } + if (state.isLoading) { + Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(0.5f)), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } } } @@ -139,6 +171,6 @@ fun LoginScreen(navController: NavController) { @Preview @Composable fun LoginScreenPreview() { - LoginScreen(rememberNavController()) + LoginScreen(rememberNavController(), koinViewModel()) -} \ No newline at end of file +} 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 df4898e..a3f4521 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 @@ -1,5 +1,6 @@ package com.delecrode.devhub.ui.register +import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -19,12 +20,15 @@ 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.graphics.Color +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.input.KeyboardType @@ -39,10 +43,11 @@ import com.delecrode.devhub.ui.components.EmailTextField import com.delecrode.devhub.ui.components.GenericOutlinedTextField import com.delecrode.devhub.ui.components.PasswordTextField import com.delecrode.devhub.ui.components.PrimaryButton +import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun RegisterScreen(navController: NavController) { +fun RegisterScreen(navController: NavController, viewModel: RegisterViewModel) { var userName by remember { mutableStateOf("") } var name by remember { mutableStateOf("") } @@ -53,6 +58,23 @@ fun RegisterScreen(navController: NavController) { var passwordVisible by remember { mutableStateOf(false) } var passwordConfirmVisible by remember { mutableStateOf(false) } + val state by viewModel.state.collectAsState() + val context = LocalContext.current + + + LaunchedEffect(state.error) { + if (state.error != null) { + Toast.makeText(context, state.error, Toast.LENGTH_SHORT).show() + viewModel.clearState() + } + } + + LaunchedEffect(state.isSuccess) { + if(state.isSuccess){ + navController.navigate(AppDestinations.Login.route) + } + } + Scaffold( @@ -210,8 +232,12 @@ fun RegisterScreen(navController: NavController) { PrimaryButton( text = "Cadastrar", onClick = { - //authViewModel.login(email, password) - navController.navigate(AppDestinations.Login.route) + viewModel.signUp( + name = name, + username = userName, + email = email, + password = password + ) }, enabled = email.isNotBlank() && password.isNotBlank() && password == confirmPassword ) @@ -225,5 +251,5 @@ fun RegisterScreen(navController: NavController) { @Preview @Composable fun RegisterScreenPreview() { - RegisterScreen(rememberNavController()) + RegisterScreen(rememberNavController(), koinViewModel()) } \ No newline at end of file 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 new file mode 100644 index 0000000..c241785 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterState.kt @@ -0,0 +1,7 @@ +package com.delecrode.devhub.ui.register + +data class RegisterState( + val isLoading: Boolean = false, + val isSuccess: Boolean = false, + val error: String? = 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 new file mode 100644 index 0000000..d0f9f7f --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/register/RegisterViewModel.kt @@ -0,0 +1,41 @@ +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.ui.login.AuthState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class RegisterViewModel( + private val repository: AuthRepository +) : ViewModel() { + + private val _state = MutableStateFlow(RegisterState()) + val state = _state.asStateFlow() + + fun signUp(name: String, username: String, email: String, password: String) { + _state.value = RegisterState(isLoading = true) + viewModelScope.launch { + try{ + val response = repository.signUp(name, username, email, password) + _state.value = RegisterState( + isSuccess = response, + isLoading = false + ) + }catch (e: Exception){ + _state.value = RegisterState( + error = e.message, + isLoading = false + ) + } + } + + } + + fun clearState() { + _state.value = RegisterState() + + } +} \ No newline at end of file From 301d4b953da4cac1287591deadfc626d1bfe822d Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 17:18:18 -0300 Subject: [PATCH 4/8] =?UTF-8?q?Refatora=C3=A7=C3=A3o=20da=20estrutura=20de?= =?UTF-8?q?=20dados=20e=20persist=C3=AAncia=20de=20sess=C3=A3o=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implementado `AuthLocalDataSource` via DataStore para armazenar e recuperar o UID do usuário. - Reestruturado pacote `remote`, separando implementações de `webApi` e `firebase`. - Atualizados modelos (DTOs) e `UserRepository` para distinguir dados do GitHub (`UserForGit`) e Firebase (`UserForFirebase`). - Ajustada rota da `Home` em `AppDestinations` para suportar argumento `uid`. - Atualizado `AuthRepositoryImpl` para salvar o usuário localmente após login. --- app/build.gradle.kts | 3 ++ .../devhub/data/firebase/UserExtraData.kt | 16 -------- .../local/dataStore/AuthLocalDataSource.kt | 8 ++++ .../dataStore/AuthLocalDataSourceImpl.kt | 37 +++++++++++++++++++ .../devhub/data/mapper/UserMapper.kt | 19 ++++++++-- .../devhub/data/model/{UserDto.kt => User.kt} | 8 +++- .../{ => remote}/firebase/FirebaseAuth.kt | 16 +++++--- .../data/remote/firebase/UserExtraData.kt | 24 ++++++++++++ .../{ => webApi/instance}/RetrofitInstance.kt | 8 ++-- .../{ => webApi}/service/RepoApiService.kt | 2 +- .../{ => webApi}/service/UserApiService.kt | 6 +-- .../data/repository/AuthRepositoryImpl.kt | 9 +++-- .../data/repository/RepoRepositoryImpl.kt | 2 +- .../data/repository/UserRepositoryImpl.kt | 28 ++++++++++++-- .../java/com/delecrode/devhub/di/AppModule.kt | 17 +++++---- .../com/delecrode/devhub/domain/model/User.kt | 9 ++++- .../domain/repository/UserRepository.kt | 7 +++- .../devhub/navigation/AppDestinations.kt | 4 +- .../delecrode/devhub/navigation/AppNavHost.kt | 2 +- .../delecrode/devhub/ui/home/HomeScreen.kt | 3 +- .../com/delecrode/devhub/ui/home/HomeState.kt | 4 +- .../delecrode/devhub/ui/home/HomeViewModel.kt | 2 +- .../devhub/ui/login/AuthViewModel.kt | 2 + .../delecrode/devhub/ui/login/LoginScreen.kt | 2 +- .../devhub/ui/register/RegisterViewModel.kt | 1 - gradle/libs.versions.toml | 2 + 26 files changed, 178 insertions(+), 63 deletions(-) delete mode 100644 app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt create mode 100644 app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt create mode 100644 app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt rename app/src/main/java/com/delecrode/devhub/data/model/{UserDto.kt => User.kt} (86%) rename app/src/main/java/com/delecrode/devhub/data/{ => remote}/firebase/FirebaseAuth.kt (69%) create mode 100644 app/src/main/java/com/delecrode/devhub/data/remote/firebase/UserExtraData.kt rename app/src/main/java/com/delecrode/devhub/data/remote/{ => webApi/instance}/RetrofitInstance.kt (84%) rename app/src/main/java/com/delecrode/devhub/data/remote/{ => webApi}/service/RepoApiService.kt (90%) rename app/src/main/java/com/delecrode/devhub/data/remote/{ => webApi}/service/UserApiService.kt (74%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6f06be..eae278a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -49,6 +49,9 @@ android { dependencies { + //DataStore + implementation(libs.data.store) + //Firebase implementation(platform("com.google.firebase:firebase-bom:34.6.0")) implementation("com.google.firebase:firebase-analytics") diff --git a/app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt b/app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt deleted file mode 100644 index 0bbe527..0000000 --- a/app/src/main/java/com/delecrode/devhub/data/firebase/UserExtraData.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.delecrode.devhub.data.firebase - -import com.delecrode.devhub.domain.model.RegisterUser -import com.google.firebase.firestore.FirebaseFirestore - - -class UserExtraData( - private val firestore: FirebaseFirestore -) { - - suspend fun saveUserData(uid: String, user: RegisterUser) { - firestore.collection("users") - .document(uid) - .set(user) - } -} diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt new file mode 100644 index 0000000..1365c05 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt @@ -0,0 +1,8 @@ +package com.delecrode.devhub.data.local.dataStore + +import kotlinx.coroutines.flow.Flow + +interface AuthLocalDataSource { + fun getUID(): Flow + suspend fun saveUser(uid: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt new file mode 100644 index 0000000..700770f --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt @@ -0,0 +1,37 @@ +package com.delecrode.devhub.data.local.dataStore + +import android.content.Context +import android.util.Log +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.delecrode.devhub.domain.model.UserForFirebase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.dataStore by preferencesDataStore("auth_prefs") + +class AuthLocalDataSourceImpl(private val context: Context) : AuthLocalDataSource { + + private object PreferencesKeys { + val UID_KEY = stringPreferencesKey("uid") + } + + override fun getUID(): Flow = context.dataStore.data.map { prefs -> + + val id = prefs[PreferencesKeys.UID_KEY] + + if (id != null) { + return@map id + } else { + throw Exception("Falha ao buscar UID do usuario") + } + } + + override suspend fun saveUser(uid: String) { + Log.i("AuthLocalDataSourceImpl", "saveUser: $uid") + context.dataStore.edit { prefs -> + prefs[PreferencesKeys.UID_KEY] = uid + } + } +} \ No newline at end of file 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 05f6685..b0647e6 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 @@ -1,10 +1,12 @@ package com.delecrode.devhub.data.mapper -import com.delecrode.devhub.data.model.UserDto -import com.delecrode.devhub.domain.model.User +import com.delecrode.devhub.data.model.UserForFirebaseDto +import com.delecrode.devhub.data.model.UserForGitDto +import com.delecrode.devhub.domain.model.UserForFirebase +import com.delecrode.devhub.domain.model.UserForGit -fun UserDto.toUserDomain(): User { - return User( +fun UserForGitDto.toUserDomain(): UserForGit { + return UserForGit( login = login, avatar_url = avatar_url, url = url , @@ -13,3 +15,12 @@ fun UserDto.toUserDomain(): User { repos_url = repos_url ) } + + +fun UserForFirebaseDto.toUserDomain(): UserForFirebase{ + return UserForFirebase( + fullName = fullName, + username = username, + email = email + ) +} diff --git a/app/src/main/java/com/delecrode/devhub/data/model/UserDto.kt b/app/src/main/java/com/delecrode/devhub/data/model/User.kt similarity index 86% rename from app/src/main/java/com/delecrode/devhub/data/model/UserDto.kt rename to app/src/main/java/com/delecrode/devhub/data/model/User.kt index b8500d5..b3628e1 100644 --- a/app/src/main/java/com/delecrode/devhub/data/model/UserDto.kt +++ b/app/src/main/java/com/delecrode/devhub/data/model/User.kt @@ -1,6 +1,6 @@ package com.delecrode.devhub.data.model -data class UserDto( +data class UserForGitDto( val login: String ?, val id: Int?, val node_id: String?, @@ -35,3 +35,9 @@ data class UserDto( val created_at: String?, val updated_at: String? ) + +data class UserForFirebaseDto( + val fullName: String = "", + val username: String = "", + val email: String = "" +) diff --git a/app/src/main/java/com/delecrode/devhub/data/firebase/FirebaseAuth.kt b/app/src/main/java/com/delecrode/devhub/data/remote/firebase/FirebaseAuth.kt similarity index 69% rename from app/src/main/java/com/delecrode/devhub/data/firebase/FirebaseAuth.kt rename to app/src/main/java/com/delecrode/devhub/data/remote/firebase/FirebaseAuth.kt index 2090d55..e634003 100644 --- a/app/src/main/java/com/delecrode/devhub/data/firebase/FirebaseAuth.kt +++ b/app/src/main/java/com/delecrode/devhub/data/remote/firebase/FirebaseAuth.kt @@ -1,13 +1,13 @@ -package com.delecrode.devhub.data.firebase +package com.delecrode.devhub.data.remote.firebase +import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -import com.google.firebase.auth.FirebaseAuth as GoogleFirebaseAuth class FirebaseAuth( - private val auth: GoogleFirebaseAuth + private val auth: FirebaseAuth ) { suspend fun signIn(email: String, password: String): FirebaseUser? { @@ -17,7 +17,9 @@ class FirebaseAuth( if (task.isSuccessful) { cont.resume(task.result?.user) } else { - cont.resumeWithException(task.exception ?: Exception("Erro desconhecido ao fazer login")) + cont.resumeWithException( + task.exception ?: Exception("Erro desconhecido ao fazer login") + ) } } } @@ -30,7 +32,9 @@ class FirebaseAuth( if (task.isSuccessful) { cont.resume(task.result?.user?.uid) } else { - cont.resumeWithException(task.exception ?: Exception("Erro desconhecido ao cadastrar")) + cont.resumeWithException( + task.exception ?: Exception("Erro desconhecido ao cadastrar") + ) } } } @@ -39,4 +43,4 @@ class FirebaseAuth( fun signOut() { auth.signOut() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/remote/firebase/UserExtraData.kt b/app/src/main/java/com/delecrode/devhub/data/remote/firebase/UserExtraData.kt new file mode 100644 index 0000000..639fca7 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/remote/firebase/UserExtraData.kt @@ -0,0 +1,24 @@ +package com.delecrode.devhub.data.remote.firebase + +import com.delecrode.devhub.domain.model.RegisterUser +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.tasks.await + +class UserExtraData( + private val firestore: FirebaseFirestore +) { + + fun saveUserData(uid: String, user: RegisterUser) { + firestore.collection("users") + .document(uid) + .set(user) + } + + suspend fun getUser(uid: String): DocumentSnapshot { + return firestore.collection("users") + .document(uid) + .get() + .await() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/remote/RetrofitInstance.kt b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/instance/RetrofitInstance.kt similarity index 84% rename from app/src/main/java/com/delecrode/devhub/data/remote/RetrofitInstance.kt rename to app/src/main/java/com/delecrode/devhub/data/remote/webApi/instance/RetrofitInstance.kt index 306ff15..de3ebb7 100644 --- a/app/src/main/java/com/delecrode/devhub/data/remote/RetrofitInstance.kt +++ b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/instance/RetrofitInstance.kt @@ -1,8 +1,8 @@ -package com.delecrode.devhub.data.remote +package com.delecrode.devhub.data.remote.webApi.instance import com.delecrode.devhub.BuildConfig -import com.delecrode.devhub.data.remote.service.RepoApiService -import com.delecrode.devhub.data.remote.service.UserApiService +import com.delecrode.devhub.data.remote.webApi.service.RepoApiService +import com.delecrode.devhub.data.remote.webApi.service.UserApiService import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -36,4 +36,4 @@ object RetrofitInstance { val repoApi: RepoApiService by lazy { retrofit.create(RepoApiService::class.java) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/remote/service/RepoApiService.kt b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/RepoApiService.kt similarity index 90% rename from app/src/main/java/com/delecrode/devhub/data/remote/service/RepoApiService.kt rename to app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/RepoApiService.kt index a104c94..dcfee62 100644 --- a/app/src/main/java/com/delecrode/devhub/data/remote/service/RepoApiService.kt +++ b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/RepoApiService.kt @@ -1,4 +1,4 @@ -package com.delecrode.devhub.data.remote.service +package com.delecrode.devhub.data.remote.webApi.service import com.delecrode.devhub.data.model.LanguagesDto import com.delecrode.devhub.data.model.RepoDetailDto diff --git a/app/src/main/java/com/delecrode/devhub/data/remote/service/UserApiService.kt b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/UserApiService.kt similarity index 74% rename from app/src/main/java/com/delecrode/devhub/data/remote/service/UserApiService.kt rename to app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/UserApiService.kt index 8e9e145..213e2ca 100644 --- a/app/src/main/java/com/delecrode/devhub/data/remote/service/UserApiService.kt +++ b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/UserApiService.kt @@ -1,7 +1,7 @@ -package com.delecrode.devhub.data.remote.service +package com.delecrode.devhub.data.remote.webApi.service import com.delecrode.devhub.data.model.ReposDto -import com.delecrode.devhub.data.model.UserDto +import com.delecrode.devhub.data.model.UserForGitDto import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -9,7 +9,7 @@ import retrofit2.http.Path interface UserApiService { @GET("users/{userName}") - suspend fun getUser(@Path("userName") userName: String): Response + suspend fun getUser(@Path("userName") userName: String): Response @GET("users/{userName}/repos") suspend fun getReposForUser(@Path("userName") userName: String) : Response> 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 432c317..b10c057 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,7 +1,8 @@ package com.delecrode.devhub.data.repository -import com.delecrode.devhub.data.firebase.FirebaseAuth -import com.delecrode.devhub.data.firebase.UserExtraData +import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSource +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.repository.AuthRepository import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException @@ -12,12 +13,14 @@ import com.google.firebase.auth.FirebaseUser class AuthRepositoryImpl( private val authDataSource: FirebaseAuth, - private val userExtraDataSource: UserExtraData + private val userExtraDataSource: UserExtraData, + private val authLocalDataSource: AuthLocalDataSource ) : AuthRepository { override suspend fun signIn(email: String, password: String): FirebaseUser { try { val response = authDataSource.signIn(email, password) + authLocalDataSource.saveUser(response?.uid ?: "") return response ?: throw Exception("Erro ao recuperar usuário após login") } catch (e: Exception) { val errorMessage = when (e) { 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 b81e083..d19b975 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 @@ -2,7 +2,7 @@ package com.delecrode.devhub.data.repository import com.delecrode.devhub.data.mapper.toLanguagesDomain import com.delecrode.devhub.data.mapper.toRepoDetailDomain -import com.delecrode.devhub.data.remote.service.RepoApiService +import com.delecrode.devhub.data.remote.webApi.service.RepoApiService import com.delecrode.devhub.domain.model.Languages import com.delecrode.devhub.domain.model.RepoDetail import com.delecrode.devhub.domain.repository.RepoRepository 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 5bc4ad8..f53b057 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,16 +1,18 @@ package com.delecrode.devhub.data.repository import android.util.Log +import com.delecrode.devhub.data.remote.firebase.UserExtraData import com.delecrode.devhub.data.mapper.toReposDomain import com.delecrode.devhub.data.mapper.toUserDomain -import com.delecrode.devhub.data.remote.service.UserApiService +import com.delecrode.devhub.data.model.UserForFirebaseDto +import com.delecrode.devhub.data.remote.webApi.service.UserApiService import com.delecrode.devhub.domain.model.Repos -import com.delecrode.devhub.domain.model.User +import com.delecrode.devhub.domain.model.UserForGit import com.delecrode.devhub.domain.repository.UserRepository -class UserRepositoryImpl(private val userApi: UserApiService) : UserRepository { +class UserRepositoryImpl(private val userApi: UserApiService, private val userExtraData: UserExtraData) : UserRepository { - override suspend fun getUser(userName: String): User { + override suspend fun getUserForGitHub(userName: String): UserForGit { try { val response = userApi.getUser(userName) if (response.isSuccessful) { @@ -29,6 +31,24 @@ class UserRepositoryImpl(private val userApi: UserApiService) : UserRepository { } } + override suspend fun getUserForFirebase(uid: String): UserForFirebaseDto{ + try { + val response = userExtraData.getUser(uid) + if (response.exists()) { + val body = response.toObject(UserForFirebaseDto::class.java) + if (body != null) { + return body + } else { + throw Exception("Resposta vazia do servidor") + } + }else{ + throw Exception("Usuário não encontrado") + } + }catch (e: Exception){ + throw e + } + } + override suspend fun getRepos(userName: String): List { try { val response = userApi.getReposForUser(userName) 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 0acf21d..70d8bdf 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -1,7 +1,9 @@ package com.delecrode.devhub.di -import com.delecrode.devhub.data.firebase.UserExtraData -import com.delecrode.devhub.data.remote.RetrofitInstance +import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSource +import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSourceImpl +import com.delecrode.devhub.data.remote.firebase.UserExtraData +import com.delecrode.devhub.data.remote.webApi.instance.RetrofitInstance import com.delecrode.devhub.data.repository.AuthRepositoryImpl import com.delecrode.devhub.data.repository.RepoRepositoryImpl import com.delecrode.devhub.data.repository.UserRepositoryImpl @@ -15,8 +17,8 @@ import com.delecrode.devhub.ui.register.RegisterViewModel import com.delecrode.devhub.ui.repo.RepoDetailViewModel import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -import org.koin.dsl.module import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module val appModule = module { @@ -27,15 +29,14 @@ val appModule = module { single { RetrofitInstance.userApi } single { RetrofitInstance.repoApi } + single { AuthLocalDataSourceImpl(get()) } - - // Seus data sources customizados - single { com.delecrode.devhub.data.firebase.FirebaseAuth(get()) } + single { com.delecrode.devhub.data.remote.firebase.FirebaseAuth(get()) } single { UserExtraData(get()) } - single { UserRepositoryImpl(get()) } + single { UserRepositoryImpl(get(), get()) } single { RepoRepositoryImpl(get()) } - single { AuthRepositoryImpl(get(), get()) } + single { AuthRepositoryImpl(get(), get(), get()) } viewModel{ HomeViewModel(get()) } viewModel { RepoDetailViewModel(get()) } 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 f225074..9aac089 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 @@ -2,7 +2,7 @@ package com.delecrode.devhub.domain.model //User for GitHub -data class User( +data class UserForGit( val login: String?, val avatar_url : String?, val url : String?, @@ -16,7 +16,12 @@ data class User( data class RegisterUser( val fullName: String = "", val username: String = "", - val birthDate: String = "", + val email: String = "" +) + +data class UserForFirebase( + val fullName: String = "", + val username: String = "", val email: String = "" ) 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 03eeaaf..87ab8ba 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 @@ -1,11 +1,14 @@ package com.delecrode.devhub.domain.repository +import com.delecrode.devhub.data.model.UserForFirebaseDto import com.delecrode.devhub.domain.model.Repos -import com.delecrode.devhub.domain.model.User +import com.delecrode.devhub.domain.model.UserForGit interface UserRepository { - suspend fun getUser(userName: String): User + suspend fun getUserForGitHub(userName: String): UserForGit suspend fun getRepos(userName: String): List + suspend fun getUserForFirebase(uid: String): UserForFirebaseDto + } diff --git a/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt b/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt index 4e76dd4..127b1ba 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt @@ -2,7 +2,9 @@ package com.delecrode.devhub.navigation sealed class AppDestinations(val route: String) { - object Home : AppDestinations("home") + object Home : AppDestinations("home/{uid}"){ + fun createRoute(uid: String) = "home/$uid" + } object RepoDetail : AppDestinations("repoDetail/{owner}/{repo}"){ fun createRoute(owner: String, repo: String) = "repoDetail/$owner/$repo" 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 b3f7ca8..6dad5f0 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt @@ -38,7 +38,7 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { startDestination = if (logged.value) AppDestinations.Home.route else AppDestinations.Login.route ) { //Home Flow - composable(AppDestinations.Home.route) { + composable(AppDestinations.Home.route){ HomeScreen(navController, profileViewModel) } 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 1df4796..3f6f58a 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 @@ -1,5 +1,6 @@ package com.delecrode.devhub.ui.home +import android.util.Log import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -58,7 +59,7 @@ import com.delecrode.devhub.ui.theme.PrimaryBlue @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel) { +fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { val uiState = homeViewModel.uiState.collectAsState() val user = uiState.value.user 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 0325a53..0b08a1d 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 @@ -1,11 +1,11 @@ package com.delecrode.devhub.ui.home import com.delecrode.devhub.domain.model.Repos -import com.delecrode.devhub.domain.model.User +import com.delecrode.devhub.domain.model.UserForGit data class HomeState( val isLoading: Boolean = false, - val user: User? = null, + val user: UserForGit? = null, val repos: List = emptyList(), 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 54e246c..a69257f 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 @@ -21,7 +21,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { error = null ) try { - val user = repository.getUser(userName) + val user = repository.getUserForGitHub(userName) _uiState.value = _uiState.value.copy( user = user, isLoading = false 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 1bc4e6c..694d0c2 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,5 +1,6 @@ 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 @@ -26,6 +27,7 @@ class AuthViewModel( userUid = user.uid, isLoading = false ) + Log.i("AuthViewModel", "signIn: Usuario Logado ${user.uid}") } catch (e: Exception) { _state.value = AuthState( error = e.message, 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 176765f..236184d 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 @@ -64,7 +64,7 @@ fun LoginScreen(navController: NavController, viewModel: AuthViewModel) { LaunchedEffect(state.isSuccess) { if (state.isSuccess) { - navController.navigate(AppDestinations.Home.route) { + navController.navigate(AppDestinations.Home.createRoute(state.userUid ?: "")) { popUpTo(AppDestinations.Login.route) { inclusive = true } } } 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 d0f9f7f..2d696c2 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,7 +3,6 @@ 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.ui.login.AuthState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cafe560..dc03f11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ activityCompose = "1.12.0" composeBom = "2024.09.00" navigationCompose = "2.9.6" coilCompose = "2.7.0" +dataStore = "1.2.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -28,6 +29,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } +data-store = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStore"} [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From ad4f6aaf7919ff4f7bce3a3a300be9b7e87451b1 Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 17:48:56 -0300 Subject: [PATCH 5/8] =?UTF-8?q?Exibindo=20dados=20do=20usu=C3=A1rio=20loga?= =?UTF-8?q?do=20na=20Toolbar=20da=20Home=20e=20ajustes=20de=20estado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Atualizado `UserRepository` para recuperar UID via `AuthLocalDataSource` internamente e retornar modelo de domínio. - Separado estado de usuário em `userForGit` (busca) e `userForFirebase` (logado) no `HomeViewModel` e `HomeState`. - Atualizada `HomeScreen` para exibir nome e username do usuário logado na TopAppBar e corrigida chamada de busca para `getUserForSearchGit`. - Ajustada injeção de dependência do `UserRepository` no `AppModule`. --- .../dataStore/AuthLocalDataSourceImpl.kt | 1 - .../data/repository/UserRepositoryImpl.kt | 17 ++++++---- .../java/com/delecrode/devhub/di/AppModule.kt | 2 +- .../domain/repository/UserRepository.kt | 4 +-- .../delecrode/devhub/ui/home/HomeScreen.kt | 34 ++++++++++++++++--- .../com/delecrode/devhub/ui/home/HomeState.kt | 4 ++- .../delecrode/devhub/ui/home/HomeViewModel.kt | 29 ++++++++++++++-- 7 files changed, 72 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt index 700770f..c4d7c2a 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt @@ -5,7 +5,6 @@ import android.util.Log import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.delecrode.devhub.domain.model.UserForFirebase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map 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 f53b057..b95bd44 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,16 +1,19 @@ package com.delecrode.devhub.data.repository import android.util.Log -import com.delecrode.devhub.data.remote.firebase.UserExtraData +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.model.UserForFirebaseDto +import com.delecrode.devhub.data.remote.firebase.UserExtraData import com.delecrode.devhub.data.remote.webApi.service.UserApiService 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 kotlinx.coroutines.flow.first -class UserRepositoryImpl(private val userApi: UserApiService, private val userExtraData: UserExtraData) : UserRepository { +class UserRepositoryImpl(private val userApi: UserApiService, private val userExtraData: UserExtraData, private val authLocalDataSource: AuthLocalDataSource) : UserRepository { override suspend fun getUserForGitHub(userName: String): UserForGit { try { @@ -31,11 +34,14 @@ class UserRepositoryImpl(private val userApi: UserApiService, private val userEx } } - override suspend fun getUserForFirebase(uid: String): UserForFirebaseDto{ + override suspend fun getUserForFirebase(): UserForFirebase { try { - val response = userExtraData.getUser(uid) + val uid = authLocalDataSource.getUID().first() + Log.i("UserRepositoryImpl", "getUserForFirebase (UID Real): $uid") + + val response = userExtraData.getUser(uid ?: "") if (response.exists()) { - val body = response.toObject(UserForFirebaseDto::class.java) + val body = response.toObject(UserForFirebaseDto::class.java)?.toUserDomain() if (body != null) { return body } else { @@ -67,4 +73,3 @@ class UserRepositoryImpl(private val userApi: UserApiService, private val userEx } } } - 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 70d8bdf..41ac4ac 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -34,7 +34,7 @@ val appModule = module { single { com.delecrode.devhub.data.remote.firebase.FirebaseAuth(get()) } single { UserExtraData(get()) } - single { UserRepositoryImpl(get(), get()) } + single { UserRepositoryImpl(get(), get(), get()) } single { RepoRepositoryImpl(get()) } single { AuthRepositoryImpl(get(), get(), get()) } 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 87ab8ba..a773d55 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 @@ -1,14 +1,14 @@ package com.delecrode.devhub.domain.repository -import com.delecrode.devhub.data.model.UserForFirebaseDto import com.delecrode.devhub.domain.model.Repos +import com.delecrode.devhub.domain.model.UserForFirebase import com.delecrode.devhub.domain.model.UserForGit interface UserRepository { suspend fun getUserForGitHub(userName: String): UserForGit suspend fun getRepos(userName: String): List - suspend fun getUserForFirebase(uid: String): UserForFirebaseDto + suspend fun getUserForFirebase(): UserForFirebase } 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 3f6f58a..fade916 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 @@ -1,6 +1,5 @@ package com.delecrode.devhub.ui.home -import android.util.Log import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -16,6 +15,8 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar @@ -62,7 +63,9 @@ import com.delecrode.devhub.ui.theme.PrimaryBlue fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { val uiState = homeViewModel.uiState.collectAsState() - val user = uiState.value.user + val userForGit = uiState.value.userForGit + val userForFirebase = uiState.value.userForFirebase + val repos = uiState.value.repos var searchText by remember { mutableStateOf("") } @@ -73,7 +76,7 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { LaunchedEffect(search) { if (search && searchText.isNotBlank()) { homeViewModel.getRepos(searchText) - homeViewModel.getUser(searchText) + homeViewModel.getUserForSearchGit(searchText) search = false } } @@ -88,12 +91,33 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { Scaffold( topBar = { CenterAlignedTopAppBar( - title = { Text("DevHub") }, + title = { + Column { + Text( + text = userForFirebase?.fullName ?: "", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = userForFirebase?.username ?: "", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + actions = { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Menu" + ) + } + }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.background ) ) } + ) { padding -> @@ -132,7 +156,7 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { } - user.let { user -> + userForGit.let { user -> Column( modifier = Modifier .padding(8.dp), 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 0b08a1d..21fbc96 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 @@ -1,11 +1,13 @@ package com.delecrode.devhub.ui.home import com.delecrode.devhub.domain.model.Repos +import com.delecrode.devhub.domain.model.UserForFirebase import com.delecrode.devhub.domain.model.UserForGit data class HomeState( val isLoading: Boolean = false, - val user: UserForGit? = null, + val userForGit: UserForGit? = null, + val userForFirebase: UserForFirebase? = null, val repos: List = emptyList(), 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 a69257f..34866a3 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 @@ -14,7 +14,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { private val _uiState = MutableStateFlow(HomeState()) val uiState: StateFlow = _uiState - fun getUser(userName: String) { + fun getUserForSearchGit(userName: String) { viewModelScope.launch { _uiState.value = _uiState.value.copy( isLoading = true, @@ -23,7 +23,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { try { val user = repository.getUserForGitHub(userName) _uiState.value = _uiState.value.copy( - user = user, + userForGit = user, isLoading = false ) } catch (e: Exception) { @@ -36,6 +36,29 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { } } + init{ + getUserForFirebase() + } + + fun getUserForFirebase() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + isLoading = true, + error = null + ) + try { + val user = repository.getUserForFirebase() + _uiState.value = _uiState.value.copy( + userForFirebase = user, + isLoading = false + ) + Log.i("HomeViewModel", "getUserForFirebase: $user") + } catch (e: Exception) { + throw e + } + } + } + fun getRepos(userName: String) { viewModelScope.launch { _uiState.value = _uiState.value.copy( @@ -58,7 +81,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { } } - fun clearStates(){ + fun clearStates() { _uiState.value = _uiState.value.copy( isLoading = false, error = null From 338dd3b6fddfa9fd0831ea91c9be2989f45fd2b7 Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 18:10:33 -0300 Subject: [PATCH 6/8] =?UTF-8?q?Refatorado=20fluxo=20de=20busca=20de=20usu?= =?UTF-8?q?=C3=A1rio=20e=20ajustes=20de=20estado=20na=20Home?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Atualizado `AuthLocalDataSourceImpl` para retornar UID nulo e removido lançamento de exceção. - Ajustado `UserRepositoryImpl` para retornar objeto vazio caso o UID seja nulo. - Movida chamada `getUserForFirebase` do bloco `init` do ViewModel para `LaunchedEffect` na `HomeScreen`. - Renomeado campo `userForGit` para `userForSearchGit` no `HomeState` e classes relacionadas. --- .../dataStore/AuthLocalDataSourceImpl.kt | 13 ++++------ .../data/repository/UserRepositoryImpl.kt | 24 +++++++++++-------- .../delecrode/devhub/ui/home/HomeScreen.kt | 5 +++- .../com/delecrode/devhub/ui/home/HomeState.kt | 2 +- .../delecrode/devhub/ui/home/HomeViewModel.kt | 6 +---- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt index c4d7c2a..7cd452e 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt @@ -16,16 +16,11 @@ class AuthLocalDataSourceImpl(private val context: Context) : AuthLocalDataSourc val UID_KEY = stringPreferencesKey("uid") } - override fun getUID(): Flow = context.dataStore.data.map { prefs -> - - val id = prefs[PreferencesKeys.UID_KEY] - - if (id != null) { - return@map id - } else { - throw Exception("Falha ao buscar UID do usuario") + override fun getUID(): Flow = + context.dataStore.data.map { prefs -> + prefs[PreferencesKeys.UID_KEY] } - } + override suspend fun saveUser(uid: String) { Log.i("AuthLocalDataSourceImpl", "saveUser: $uid") 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 b95bd44..dfe8bdf 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 @@ -38,17 +38,21 @@ class UserRepositoryImpl(private val userApi: UserApiService, private val userEx try { val uid = authLocalDataSource.getUID().first() Log.i("UserRepositoryImpl", "getUserForFirebase (UID Real): $uid") - - val response = userExtraData.getUser(uid ?: "") - if (response.exists()) { - val body = response.toObject(UserForFirebaseDto::class.java)?.toUserDomain() - if (body != null) { - return body - } else { - throw Exception("Resposta vazia do servidor") + if(uid != null){ + val response = userExtraData.getUser(uid ?: "") + if (response.exists()) { + val body = response.toObject(UserForFirebaseDto::class.java)?.toUserDomain() + if (body != null) { + return body + } else { + throw Exception("Resposta vazia do servidor") + } + }else{ + throw Exception("Usuário não encontrado") } - }else{ - throw Exception("Usuário não encontrado") + } + else{ + return UserForFirebase() } }catch (e: Exception){ throw e 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 fade916..21d975a 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 @@ -63,7 +63,7 @@ import com.delecrode.devhub.ui.theme.PrimaryBlue fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { val uiState = homeViewModel.uiState.collectAsState() - val userForGit = uiState.value.userForGit + val userForGit = uiState.value.userForSearchGit val userForFirebase = uiState.value.userForFirebase val repos = uiState.value.repos @@ -87,6 +87,9 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { homeViewModel.clearStates() } } + LaunchedEffect(Unit) { + homeViewModel.getUserForFirebase() + } Scaffold( topBar = { 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 21fbc96..ee78ef9 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 @@ -6,7 +6,7 @@ import com.delecrode.devhub.domain.model.UserForGit data class HomeState( val isLoading: Boolean = false, - val userForGit: UserForGit? = null, + val userForSearchGit: UserForGit? = null, val userForFirebase: UserForFirebase? = null, val repos: List = emptyList(), val error: String? = null, 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 34866a3..42798df 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 @@ -23,7 +23,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { try { val user = repository.getUserForGitHub(userName) _uiState.value = _uiState.value.copy( - userForGit = user, + userForSearchGit = user, isLoading = false ) } catch (e: Exception) { @@ -36,10 +36,6 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { } } - init{ - getUserForFirebase() - } - fun getUserForFirebase() { viewModelScope.launch { _uiState.value = _uiState.value.copy( From 70f148929d926200c13addb493dcbaa7014f8e66 Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 18:39:20 -0300 Subject: [PATCH 7/8] =?UTF-8?q?Adicionado=20funcionalidade=20de=20logout?= =?UTF-8?q?=20e=20exibi=C3=A7=C3=A3o=20de=20perfil=20na=20Home?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Atualizada `HomeScreen` para exibir avatar do usuário logado e menu dropdown com opção de sair. - Implementado fluxo de `signOut` no `HomeViewModel`, `AuthRepository` e `AuthLocalDataSource` para limpar a sessão (`clearUID`). - Adicionada lógica para buscar dados do GitHub do usuário autenticado separadamente da busca de usuários (`getUserForGit`). - Refatorado `AuthLocalDataSource` renomeando `saveUser` para `saveUID`. - Atualizado `AppModule` para injetar `AuthRepository` no `HomeViewModel`. --- .../local/dataStore/AuthLocalDataSource.kt | 4 +- .../dataStore/AuthLocalDataSourceImpl.kt | 8 +- .../data/repository/AuthRepositoryImpl.kt | 3 +- .../data/repository/UserRepositoryImpl.kt | 2 +- .../java/com/delecrode/devhub/di/AppModule.kt | 2 +- .../delecrode/devhub/ui/home/HomeScreen.kt | 100 ++++++++++++++---- .../com/delecrode/devhub/ui/home/HomeState.kt | 1 + .../delecrode/devhub/ui/home/HomeViewModel.kt | 49 ++++++++- 8 files changed, 142 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt index 1365c05..405f969 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt @@ -4,5 +4,7 @@ import kotlinx.coroutines.flow.Flow interface AuthLocalDataSource { fun getUID(): Flow - suspend fun saveUser(uid: String) + suspend fun saveUID(uid: String) + + suspend fun clearUID() } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt index 7cd452e..730dedf 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt @@ -22,10 +22,16 @@ class AuthLocalDataSourceImpl(private val context: Context) : AuthLocalDataSourc } - override suspend fun saveUser(uid: String) { + override suspend fun saveUID(uid: String) { Log.i("AuthLocalDataSourceImpl", "saveUser: $uid") context.dataStore.edit { prefs -> prefs[PreferencesKeys.UID_KEY] = uid } } + + override suspend fun clearUID() { + context.dataStore.edit { prefs -> + prefs.remove(PreferencesKeys.UID_KEY) + } + } } \ 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 b10c057..4fe8709 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 @@ -20,7 +20,7 @@ class AuthRepositoryImpl( override suspend fun signIn(email: String, password: String): FirebaseUser { try { val response = authDataSource.signIn(email, password) - authLocalDataSource.saveUser(response?.uid ?: "") + authLocalDataSource.saveUID(response?.uid ?: "") return response ?: throw Exception("Erro ao recuperar usuário após login") } catch (e: Exception) { val errorMessage = when (e) { @@ -65,5 +65,6 @@ class AuthRepositoryImpl( override suspend fun signOut() { authDataSource.signOut() + authLocalDataSource.clearUID() } } 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 dfe8bdf..88abdfa 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 @@ -39,7 +39,7 @@ class UserRepositoryImpl(private val userApi: UserApiService, private val userEx val uid = authLocalDataSource.getUID().first() Log.i("UserRepositoryImpl", "getUserForFirebase (UID Real): $uid") if(uid != null){ - val response = userExtraData.getUser(uid ?: "") + val response = userExtraData.getUser(uid) if (response.exists()) { val body = response.toObject(UserForFirebaseDto::class.java)?.toUserDomain() if (body != null) { 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 41ac4ac..2801b91 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -38,7 +38,7 @@ val appModule = module { single { RepoRepositoryImpl(get()) } single { AuthRepositoryImpl(get(), get(), get()) } - viewModel{ HomeViewModel(get()) } + viewModel{ HomeViewModel(get(), get()) } viewModel { RepoDetailViewModel(get()) } viewModel { AuthViewModel(get()) } viewModel { SessionViewModel(get()) } 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 21d975a..5216bea 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 @@ -11,6 +11,7 @@ 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.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape @@ -21,6 +22,8 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -57,19 +60,22 @@ import coil.compose.AsyncImage import com.delecrode.devhub.R import com.delecrode.devhub.navigation.AppDestinations import com.delecrode.devhub.ui.theme.PrimaryBlue +import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { +fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel) { val uiState = homeViewModel.uiState.collectAsState() - val userForGit = uiState.value.userForSearchGit + val userForSearchGit = uiState.value.userForSearchGit + val userForGit = uiState.value.userForGit val userForFirebase = uiState.value.userForFirebase 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 @@ -91,29 +97,78 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { homeViewModel.getUserForFirebase() } + LaunchedEffect(userForFirebase) { + val firebaseUser = userForFirebase + if (firebaseUser != null) { + homeViewModel.getUserForGit(firebaseUser.username) + } + } + + Scaffold( topBar = { CenterAlignedTopAppBar( title = { - Column { - Text( - text = userForFirebase?.fullName ?: "", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = userForFirebase?.username ?: "", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Row { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(Color.White) + ) { + AsyncImage( + model = userForGit?.avatar_url ?: R.drawable.git_logo, + contentDescription = "Foto de Perfil", + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + + Column { + Text( + text = userForFirebase?.fullName ?: "", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = userForFirebase?.username ?: "", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } }, actions = { - IconButton(onClick = {}) { + IconButton(onClick = { expanded = true }) { Icon( imageVector = Icons.Default.Menu, contentDescription = "Menu" ) } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text("Meu Perfil") }, + onClick = { + expanded = false + navController.navigate("profile") + } + ) + DropdownMenuItem( + text = { Text("Sair") }, + onClick = { + expanded = false + homeViewModel.signOut() + navController.navigate("login") { + popUpTo(0) + } + } + ) + } }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.background @@ -129,9 +184,11 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { .fillMaxSize() .padding(padding) ) { - Row(modifier = Modifier - .fillMaxWidth() - .padding(8.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { OutlinedTextField( value = searchText, onValueChange = { searchText = it }, @@ -159,7 +216,7 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { } - userForGit.let { user -> + userForSearchGit.let { user -> Column( modifier = Modifier .padding(8.dp), @@ -237,7 +294,14 @@ fun HomeScreen(navController: NavController, homeViewModel: HomeViewModel,) { elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), colors = CardDefaults.cardColors(containerColor = Color.White), shape = RoundedCornerShape(8.dp), - onClick = {navController.navigate(AppDestinations.RepoDetail.createRoute(user?.login ?: "", repo.name))} + onClick = { + navController.navigate( + AppDestinations.RepoDetail.createRoute( + user?.login ?: "", + repo.name + ) + ) + } ) { Row( modifier = Modifier 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 ee78ef9..e262c63 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 @@ -7,6 +7,7 @@ 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 error: String? = null, 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 42798df..382010a 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 @@ -4,12 +4,17 @@ 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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -class HomeViewModel(private val repository: UserRepository) : ViewModel() { +class HomeViewModel( + private val userRepository: UserRepository, + private val authRepository: AuthRepository +) : + ViewModel() { private val _uiState = MutableStateFlow(HomeState()) val uiState: StateFlow = _uiState @@ -21,7 +26,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { error = null ) try { - val user = repository.getUserForGitHub(userName) + val user = userRepository.getUserForGitHub(userName) _uiState.value = _uiState.value.copy( userForSearchGit = user, isLoading = false @@ -36,6 +41,28 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { } } + fun getUserForGit(userName: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + 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 + ) + } + } + } + fun getUserForFirebase() { viewModelScope.launch { _uiState.value = _uiState.value.copy( @@ -43,7 +70,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { error = null ) try { - val user = repository.getUserForFirebase() + val user = userRepository.getUserForFirebase() _uiState.value = _uiState.value.copy( userForFirebase = user, isLoading = false @@ -62,7 +89,7 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { error = null ) try { - val repos: List = repository.getRepos(userName) + val repos: List = userRepository.getRepos(userName) _uiState.value = _uiState.value.copy( repos = repos, isLoading = false @@ -77,6 +104,20 @@ class HomeViewModel(private val repository: UserRepository) : ViewModel() { } } + 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, From cda46eb71c61bf2bd06d298112154acc4ea056b3 Mon Sep 17 00:00:00 2001 From: guilh Date: Fri, 12 Dec 2025 18:45:51 -0300 Subject: [PATCH 8/8] =?UTF-8?q?-=20Remove=20import=20`kotlinx.coroutines.d?= =?UTF-8?q?elay`=20em=20`HomeScreen.kt`=20pois=20n=C3=A3o=20estava=20sendo?= =?UTF-8?q?=20utilizado.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/delecrode/devhub/ui/home/HomeScreen.kt | 1 - 1 file changed, 1 deletion(-) 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 5216bea..ee2ad5e 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 @@ -60,7 +60,6 @@ import coil.compose.AsyncImage import com.delecrode.devhub.R import com.delecrode.devhub.navigation.AppDestinations import com.delecrode.devhub.ui.theme.PrimaryBlue -import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @Composable