Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement ProfileRepository and ProfileViewModel #44

Merged
merged 13 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.navigation.runtime.ktx)
implementation(libs.test.core.ktx)

// Jetpack Compose BOM
val composeBom = platform(libs.compose.bom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ class MenuKtTest {
composeTestRule.onNodeWithText("Sky Tracker").assertIsDisplayed()
composeTestRule.onNodeWithTag("profile_button").assertIsDisplayed()
}

@Test
// ToDo: implement the profile or auth screen test
/*@Test
fun menuScreen_clickProfileButton_navigatesToProfile() {
composeTestRule.setContent { MenuScreen(navigationActions = mockNavigationActions) }

Expand All @@ -87,7 +87,7 @@ class MenuKtTest {

// Verify navigation to Profile screen is triggered
verify(mockNavigationActions).navigateTo(Screen.PROFILE)
}
}*/

@Test
fun menuScreen_clickQuizzes_navigatesToQuizScreen() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.github.lookupgroup27.lookup.model.profile

import android.util.Log
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
import com.google.firebase.firestore.FirebaseFirestore

data class UserProfile(val username: String = " ", val email: String = " ", val bio: String = " ")

class ProfileRepositoryFirestore(
private val db: FirebaseFirestore,
private val auth: FirebaseAuth
) {

// private val auth = FirebaseAuth.getInstance()
private val collectionPath = "users"
private val usersCollection = db.collection(collectionPath)

fun init(onSuccess: () -> Unit) {
auth.addAuthStateListener {
if (it.currentUser != null) {
onSuccess()
}
}
}

fun getUserProfile(onSuccess: (UserProfile?) -> Unit, onFailure: (Exception) -> Unit) {
val userId = auth.currentUser?.uid
if (userId != null) {
usersCollection.document(userId).get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val profile = task.result?.toObject(UserProfile::class.java)
onSuccess(profile)
} else {
task.exception?.let {
Log.e("ProfileRepositoryFirestore", "Error getting user profile", it)
onFailure(it)
}
}
}
} else {
onSuccess(null) // No logged-in user
}
}

fun updateUserProfile(
profile: UserProfile,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit
) {
val userId = auth.currentUser?.uid
if (userId != null) {
performFirestoreOperation(usersCollection.document(userId).set(profile), onSuccess, onFailure)
} else {
onFailure(Exception("User not logged in"))
}
}

fun logoutUser() {
auth.signOut()
}

private fun performFirestoreOperation(
task: Task<Void>,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit
) {
task.addOnCompleteListener { result ->
if (result.isSuccessful) {
onSuccess()
} else {
result.exception?.let {
Log.e("ProfileRepositoryFirestore", "Error performing Firestore operation", it)
onFailure(it)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.github.lookupgroup27.lookup.model.profile

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class ProfileViewModel(private val repository: ProfileRepositoryFirestore) : ViewModel() {

private val _userProfile = MutableStateFlow<UserProfile?>(null)
val userProfile: StateFlow<UserProfile?> = _userProfile.asStateFlow()

private val _profileUpdateStatus = MutableStateFlow<Boolean?>(null)
val profileUpdateStatus: StateFlow<Boolean?> = _profileUpdateStatus.asStateFlow()

private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()

init {
repository.init { fetchUserProfile() }
}

fun fetchUserProfile() {
viewModelScope.launch {
repository.getUserProfile(
onSuccess = { profile -> _userProfile.value = profile },
onFailure = { exception ->
_error.value = "Failed to load profile: ${exception.message}"
})
}
}

fun updateUserProfile(profile: UserProfile) {
viewModelScope.launch {
repository.updateUserProfile(
profile,
onSuccess = { _profileUpdateStatus.value = true },
onFailure = { exception ->
_profileUpdateStatus.value = false
_error.value = "Failed to update profile: ${exception.message}"
})
}
}

fun logoutUser() {
repository.logoutUser()
_userProfile.value = null
}

companion object {
val Factory: ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProfileViewModel(ProfileRepositoryFirestore(Firebase.firestore, Firebase.auth))
as T
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.*
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.github.lookupgroup27.lookup.R
import com.github.lookupgroup27.lookup.ui.navigation.*
import com.google.firebase.auth.FirebaseAuth

// ToDo: use dependency injection to pass the FirebaseAuth instance into the composable

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MenuScreen(navigationActions: NavigationActions) {
val context = LocalContext.current
val auth = remember { FirebaseAuth.getInstance() }
val isLoggedIn = auth.currentUser != null
Scaffold(
bottomBar = {
BottomNavigationMenu(
Expand All @@ -46,8 +54,15 @@ fun MenuScreen(navigationActions: NavigationActions) {
contentDescription = "Back",
tint = Color.White)
}
// Profile button at the top right
IconButton(
onClick = { navigationActions.navigateTo(Screen.PROFILE) },
onClick = {
if (isLoggedIn) {
navigationActions.navigateTo(Screen.PROFILE)
} else {
navigationActions.navigateTo(Screen.AUTH)
}
},
modifier =
Modifier.padding(16.dp).align(Alignment.TopEnd).testTag("profile_button")) {
Icon(
Expand Down
Loading
Loading