diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 57bc4a1..bc3b5fa 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -119,6 +119,8 @@ dependencies { // Coil Image Loading implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) + // Splash Screen API + implementation(libs.androidx.splashscreen) } // Allow references to generated code diff --git a/app/src/main/java/com/cornellappdev/hustle/MainActivity.kt b/app/src/main/java/com/cornellappdev/hustle/MainActivity.kt index ac5d872..4361720 100644 --- a/app/src/main/java/com/cornellappdev/hustle/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/hustle/MainActivity.kt @@ -4,18 +4,31 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.cornellappdev.hustle.ui.HustleApp import com.cornellappdev.hustle.ui.theme.HustleTheme +import com.cornellappdev.hustle.ui.viewmodels.RootViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { + private val rootViewModel: RootViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) + + splashScreen.setKeepOnScreenCondition { + rootViewModel.uiStateFlow.value.isLoading + } + enableEdgeToEdge() setContent { HustleTheme { - HustleApp() + val rootUiState = rootViewModel.collectUiStateValue() + if (!rootUiState.isLoading) { + HustleApp(isSignedIn = rootUiState.isSignedIn) + } } } } diff --git a/app/src/main/java/com/cornellappdev/hustle/data/repository/AuthRepository.kt b/app/src/main/java/com/cornellappdev/hustle/data/repository/AuthRepository.kt index d2e0058..e8c5bf1 100644 --- a/app/src/main/java/com/cornellappdev/hustle/data/repository/AuthRepository.kt +++ b/app/src/main/java/com/cornellappdev/hustle/data/repository/AuthRepository.kt @@ -38,6 +38,7 @@ class AuthRepositoryImpl @Inject constructor( override val currentUserFlow: StateFlow = _currentUserFlow.asStateFlow() init { + _currentUserFlow.value = firebaseAuth.currentUser?.toUser() firebaseAuth.addAuthStateListener { _currentUserFlow.value = it.currentUser?.toUser() } diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/HustleApp.kt b/app/src/main/java/com/cornellappdev/hustle/ui/HustleApp.kt index a12da9b..36ed48a 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/HustleApp.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/HustleApp.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import com.cornellappdev.hustle.ui.navigation.HustleNavigation @Composable -fun HustleApp() { - HustleNavigation() +fun HustleApp( + isSignedIn: Boolean +) { + HustleNavigation(isSignedIn) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt index b08664d..64b88d8 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt @@ -1,49 +1,46 @@ package com.cornellappdev.hustle.ui.navigation -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.outlined.FavoriteBorder -import androidx.compose.material.icons.outlined.Home -import androidx.compose.material.icons.outlined.Person +import androidx.annotation.DrawableRes import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState +import com.cornellappdev.hustle.R data class BottomNavigationItem( val route: T, val title: String, - val icon: ImageVector, - val selectedIcon: ImageVector = icon, + @DrawableRes val icon: Int, + @DrawableRes val selectedIcon: Int = icon, ) val bottomNavigationItems = listOf( BottomNavigationItem( route = HomeTab, title = "Home", - icon = Icons.Outlined.Home, - selectedIcon = Icons.Filled.Home, + icon = R.drawable.ic_home, + selectedIcon = R.drawable.ic_home, ), BottomNavigationItem( - route = FavoritesTab, - title = "Favorites", - icon = Icons.Outlined.FavoriteBorder, - selectedIcon = Icons.Filled.Favorite, + route = MessagesTab, + title = "Messages", + icon = R.drawable.ic_messages, + selectedIcon = R.drawable.ic_messages, ), BottomNavigationItem( route = ProfileTab, title = "Profile", - icon = Icons.Outlined.Person, - selectedIcon = Icons.Filled.Person, + icon = R.drawable.ic_profile, + selectedIcon = R.drawable.ic_profile, ), ) @@ -67,8 +64,9 @@ fun BottomNavigationBar(navController: NavHostController) { NavigationBarItem(icon = { Icon( - if (selected) item.selectedIcon else item.icon, - contentDescription = item.title + painter = painterResource(id = if (selected) item.selectedIcon else item.icon), + contentDescription = item.title, + tint = Color.Unspecified ) }, selected = selected, onClick = { navController.navigate(item.route) { @@ -78,6 +76,8 @@ fun BottomNavigationBar(navController: NavHostController) { launchSingleTop = true restoreState = true } + }, label = { + Text(item.title) }) } } diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt index fb483fd..fabc973 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt @@ -8,20 +8,29 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import com.cornellappdev.hustle.ui.navigation.navgraphs.homeNavGraph import com.cornellappdev.hustle.ui.navigation.navgraphs.messagesNavGraph +import com.cornellappdev.hustle.ui.navigation.navgraphs.onboardingNavGraph import com.cornellappdev.hustle.ui.navigation.navgraphs.profileNavGraph @Composable -fun HustleNavigation() { +fun HustleNavigation( + isSignedIn: Boolean = false +) { val navController = rememberNavController() + val startDestination = if (isSignedIn) HomeTab else Onboarding Scaffold( - bottomBar = { BottomNavigationBar(navController = navController) } + bottomBar = { + if (isSignedIn) { + BottomNavigationBar(navController = navController) + } + } ) { innerPadding -> NavHost( navController = navController, - startDestination = HomeTab, + startDestination = startDestination, modifier = Modifier.padding(innerPadding) ) { + onboardingNavGraph(navController = navController) homeNavGraph(navController = navController) messagesNavGraph(navController = navController) profileNavGraph(navController = navController) diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt index 41b129c..286a2c0 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt @@ -4,11 +4,14 @@ import kotlinx.serialization.Serializable sealed interface AppDestination +@Serializable +data object Onboarding: AppDestination + @Serializable data object HomeTab : AppDestination @Serializable -data object FavoritesTab : AppDestination +data object MessagesTab : AppDestination @Serializable data object ProfileTab : AppDestination @@ -21,9 +24,9 @@ sealed interface HomeDestination : AppDestination { data class ServiceDetail(val serviceId: String) : HomeDestination } -sealed interface FavoritesDestination : AppDestination { +sealed interface MessagesDestination : AppDestination { @Serializable - data object Favorites : FavoritesDestination + data object Messages : MessagesDestination } sealed interface ProfileDestination : AppDestination { @@ -32,4 +35,9 @@ sealed interface ProfileDestination : AppDestination { @Serializable data object EditProfile : ProfileDestination +} + +sealed interface OnboardingDestination: AppDestination { + @Serializable + data object SignIn : OnboardingDestination } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/OnboardingNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/OnboardingNavigation.kt new file mode 100644 index 0000000..c707688 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/OnboardingNavigation.kt @@ -0,0 +1,22 @@ +package com.cornellappdev.hustle.ui.navigation.navgraphs + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.cornellappdev.hustle.ui.navigation.HomeTab +import com.cornellappdev.hustle.ui.navigation.Onboarding +import com.cornellappdev.hustle.ui.navigation.OnboardingDestination +import com.cornellappdev.hustle.ui.screens.onboarding.SignInScreen + +fun NavGraphBuilder.onboardingNavGraph(navController: NavHostController) { + navigation(startDestination = OnboardingDestination.SignIn) { + composable { + SignInScreen(navigateToHome = { + navController.navigate(HomeTab) { + popUpTo(0) { inclusive = true } + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/ProfileNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/ProfileNavigation.kt index e4587da..6b16bb2 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/ProfileNavigation.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/ProfileNavigation.kt @@ -4,13 +4,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.navigation +import com.cornellappdev.hustle.ui.navigation.Onboarding import com.cornellappdev.hustle.ui.navigation.ProfileDestination import com.cornellappdev.hustle.ui.navigation.ProfileTab +import com.cornellappdev.hustle.ui.screens.profile.ProfileScreen fun NavGraphBuilder.profileNavGraph(navController: NavHostController) { navigation(startDestination = ProfileDestination.Profile) { composable { - + ProfileScreen(navigateToSignIn = { + navController.navigate(Onboarding) { + popUpTo(0) { inclusive = true } + } + }) } composable { diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt index 53efd34..082f1f4 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt @@ -3,6 +3,7 @@ package com.cornellappdev.hustle.ui.screens.home import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.cornellappdev.hustle.ui.viewmodels.home.HomeScreenViewModel @Composable fun HomeScreen(viewModel: HomeScreenViewModel = hiltViewModel()) { diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/screens/onboarding/SignInScreen.kt b/app/src/main/java/com/cornellappdev/hustle/ui/screens/onboarding/SignInScreen.kt new file mode 100644 index 0000000..601e6da --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/screens/onboarding/SignInScreen.kt @@ -0,0 +1,89 @@ +package com.cornellappdev.hustle.ui.screens.onboarding + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.cornellappdev.hustle.ui.components.general.ErrorMessage +import com.cornellappdev.hustle.ui.components.onboarding.GoogleSignInButton +import com.cornellappdev.hustle.ui.viewmodels.ActionState +import com.cornellappdev.hustle.ui.viewmodels.onboarding.SignInScreenViewModel + +@Composable +fun SignInScreen( + navigateToHome: () -> Unit, + modifier: Modifier = Modifier, + signInScreenViewModel: SignInScreenViewModel = hiltViewModel() +) { + val signInUiState = signInScreenViewModel.collectUiStateValue() + + LaunchedEffect(signInUiState.isSignedIn) { + if (signInUiState.isSignedIn) navigateToHome() + } + + SignInScreenContent( + onGoogleSignInButtonClick = signInScreenViewModel::signInWithGoogle, + isSignInLoading = signInUiState.actionState is ActionState.Loading, + errorMessage = when (signInUiState.actionState) { + is ActionState.Error -> signInUiState.actionState.message + else -> null + }, + onDismissError = signInScreenViewModel::clearActionState, + modifier = modifier + ) +} + +@Composable +private fun SignInScreenContent( + onGoogleSignInButtonClick: () -> Unit, + isSignInLoading: Boolean, + errorMessage: String?, + onDismissError: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.fillMaxSize()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() + ) { + GoogleSignInButton( + onClick = onGoogleSignInButtonClick, isLoading = isSignInLoading + ) + } + errorMessage?.let { error -> + ErrorMessage( + message = error, + onDismiss = onDismissError, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + } +} + +class SignInErrorMessageProvider : PreviewParameterProvider { + override val values = sequenceOf( + null, + "Sign in failed. Please sign in with your Cornell email" + ) +} + +@Preview(showBackground = true) +@Composable +private fun SignInScreenPreview( + @PreviewParameter(SignInErrorMessageProvider::class) errorMessage: String? +) { + SignInScreenContent( + onGoogleSignInButtonClick = {}, + isSignInLoading = false, + errorMessage = errorMessage, + onDismissError = {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/hustle/ui/screens/profile/ProfileScreen.kt new file mode 100644 index 0000000..c28a66e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/screens/profile/ProfileScreen.kt @@ -0,0 +1,104 @@ +package com.cornellappdev.hustle.ui.screens.profile + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import coil3.compose.AsyncImage +import com.cornellappdev.hustle.data.model.user.User +import com.cornellappdev.hustle.ui.viewmodels.ActionState +import com.cornellappdev.hustle.ui.viewmodels.profile.ProfileScreenViewModel + +@Composable +fun ProfileScreen( + navigateToSignIn: () -> Unit, + modifier: Modifier = Modifier, + profileScreenViewModel: ProfileScreenViewModel = hiltViewModel() +) { + val profileUiState = profileScreenViewModel.collectUiStateValue() + val user = profileUiState.user + + LaunchedEffect(user) { + if (user == null) navigateToSignIn() + } + + user?.let { + ProfileScreenContent( + user = it, + onSignOut = profileScreenViewModel::signOut, + isSignOutLoading = profileUiState.authActionState is ActionState.Loading, + modifier = modifier + ) + } +} + +@Composable +private fun ProfileScreenContent( + user: User, + onSignOut: () -> Unit, + isSignOutLoading: Boolean, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // User Info + AsyncImage( + model = user.photoUrl, + contentDescription = "Profile picture", + modifier = Modifier + .size(120.dp) + .clip(CircleShape) + ) + + Text(text = user.displayName ?: "Unknown User") + + Text(text = user.email ?: "") + + // Sign Out Button + OutlinedButton( + onClick = onSignOut, + enabled = !isSignOutLoading, + modifier = Modifier.fillMaxWidth() + ) { + if (isSignOutLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), strokeWidth = 2.dp + ) + } else { + Text("Sign Out") + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ProfileScreenPreview() { + ProfileScreenContent( + user = User( + firebaseUid = "1", + displayName = "John Doe", + email = "jd123@cornell.edu", + photoUrl = null + ), onSignOut = {}, isSignOutLoading = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/RootViewModel.kt b/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/RootViewModel.kt new file mode 100644 index 0000000..c6762e0 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/RootViewModel.kt @@ -0,0 +1,32 @@ +package com.cornellappdev.hustle.ui.viewmodels + +import androidx.lifecycle.viewModelScope +import com.cornellappdev.hustle.data.repository.AuthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class RootUiState( + val isSignedIn: Boolean = false, + val isLoading: Boolean = true, +) + +@HiltViewModel +class RootViewModel @Inject constructor( + private val authRepository: AuthRepository +) : HustleViewModel( + initialUiState = RootUiState() +) { + init { + viewModelScope.launch { + authRepository.currentUserFlow.collect { user -> + applyMutation { + copy( + isSignedIn = user != null, + isLoading = false + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/onboarding/AuthViewModel.kt b/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/onboarding/AuthViewModel.kt deleted file mode 100644 index fed23b0..0000000 --- a/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/onboarding/AuthViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.cornellappdev.hustle.ui.viewmodels.onboarding - -import androidx.lifecycle.viewModelScope -import com.cornellappdev.hustle.data.model.user.User -import com.cornellappdev.hustle.data.repository.AuthRepository -import com.cornellappdev.hustle.ui.viewmodels.ActionState -import com.cornellappdev.hustle.ui.viewmodels.HustleViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -data class AuthUiState( - val user: User? = null, - val isSignedIn: Boolean = false, - val actionState: ActionState = ActionState.Idle, -) - -@HiltViewModel -class AuthViewModel @Inject constructor( - private val authRepository: AuthRepository -) : HustleViewModel( - initialUiState = AuthUiState() -) { - - init { - viewModelScope.launch { - authRepository.currentUserFlow.collect { user -> - applyMutation { - copy( - user = user, - isSignedIn = user != null - ) - } - } - } - } - - fun signInWithGoogle() { - executeAuthAction { - authRepository.signInWithGoogle() - } - } - - fun signOut() { - executeAuthAction { - authRepository.signOut() - } - } - - fun clearActionState() { - applyMutation { copy(actionState = ActionState.Idle) } - } - - private fun executeAuthAction( - authAction: suspend () -> Result<*> - ) { - viewModelScope.launch { - applyMutation { copy(actionState = ActionState.Loading) } - authAction() - .onSuccess { - applyMutation { copy(actionState = ActionState.Success) } - } - .onFailure { exception -> - applyMutation { - copy( - actionState = ActionState.Error( - exception.message ?: "Authentication failed" - ) - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/onboarding/SignInScreenViewModel.kt b/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/onboarding/SignInScreenViewModel.kt new file mode 100644 index 0000000..97c10a4 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/onboarding/SignInScreenViewModel.kt @@ -0,0 +1,41 @@ +package com.cornellappdev.hustle.ui.viewmodels.onboarding + +import androidx.lifecycle.viewModelScope +import com.cornellappdev.hustle.data.repository.AuthRepository +import com.cornellappdev.hustle.ui.viewmodels.ActionState +import com.cornellappdev.hustle.ui.viewmodels.HustleViewModel +import com.cornellappdev.hustle.util.viewmodels.executeActionStatefully +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class SignInScreenUiState( + val isSignedIn: Boolean = false, + val actionState: ActionState = ActionState.Idle, +) + +@HiltViewModel +class SignInScreenViewModel @Inject constructor( + private val authRepository: AuthRepository +) : HustleViewModel( + initialUiState = SignInScreenUiState() +) { + + init { + viewModelScope.launch { + authRepository.currentUserFlow.collect { user -> + applyMutation { copy(isSignedIn = user != null) } + } + } + } + + fun signInWithGoogle() { + executeActionStatefully( + action = { authRepository.signInWithGoogle() }, + updateActionState = { copy(actionState = it) }) + } + + fun clearActionState() { + applyMutation { copy(actionState = ActionState.Idle) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/profile/ProfileScreenViewModel.kt b/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/profile/ProfileScreenViewModel.kt new file mode 100644 index 0000000..3029271 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/profile/ProfileScreenViewModel.kt @@ -0,0 +1,38 @@ +package com.cornellappdev.hustle.ui.viewmodels.profile + +import androidx.lifecycle.viewModelScope +import com.cornellappdev.hustle.data.model.user.User +import com.cornellappdev.hustle.data.repository.AuthRepository +import com.cornellappdev.hustle.ui.viewmodels.ActionState +import com.cornellappdev.hustle.ui.viewmodels.HustleViewModel +import com.cornellappdev.hustle.util.viewmodels.executeActionStatefully +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class ProfileScreenUiState( + val user: User? = null, + val authActionState: ActionState = ActionState.Idle, +) + +@HiltViewModel +class ProfileScreenViewModel @Inject constructor( + private val authRepository: AuthRepository +) : HustleViewModel( + initialUiState = ProfileScreenUiState() +) { + + init { + viewModelScope.launch { + authRepository.currentUserFlow.collect { user -> + applyMutation { copy(user = user) } + } + } + } + + fun signOut() { + executeActionStatefully( + action = { authRepository.signOut() }, + updateActionState = { copy(authActionState = it) }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/util/viewmodels/ViewModelUtils.kt b/app/src/main/java/com/cornellappdev/hustle/util/viewmodels/ViewModelUtils.kt new file mode 100644 index 0000000..e9f1a26 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/util/viewmodels/ViewModelUtils.kt @@ -0,0 +1,28 @@ +package com.cornellappdev.hustle.util.viewmodels + +import androidx.lifecycle.viewModelScope +import com.cornellappdev.hustle.ui.viewmodels.ActionState +import com.cornellappdev.hustle.ui.viewmodels.HustleViewModel +import kotlinx.coroutines.launch + +fun HustleViewModel.executeActionStatefully( + action: suspend () -> Result<*>, + updateActionState: UiState.(ActionState) -> UiState +) { + viewModelScope.launch { + applyMutation { updateActionState(ActionState.Loading) } + action() + .onSuccess { + applyMutation { updateActionState(ActionState.Success) } + } + .onFailure { exception -> + applyMutation { + updateActionState( + ActionState.Error( + exception.message ?: "An unknown error occurred" + ) + ) + } + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c89c8e..906ede8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ credentialManager = "1.5.0" googleId = "1.1.1" google-services = "4.4.3" coil = "3.3.0" +splashScreen = "1.0.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -57,6 +58,7 @@ credential-manager-play-services = { group = "androidx.credentials", name = "cre google-id = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "googleId" } coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } +androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }