From f806f8a032030291d371bae3c823d71fb39c0c3c Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Thu, 17 Sep 2026 13:38:04 -0400 Subject: [PATCH 1/3] Implement Check In Pop Up Failed State --- .../ui/components/general/CheckInPopUp.kt | 5 ++ .../profile/checkin/CheckInFailed.kt | 79 +++++++++++++++++++ .../ui/viewmodels/profile/CheckInViewModel.kt | 43 ++++++---- 3 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/components/profile/checkin/CheckInFailed.kt diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/general/CheckInPopUp.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/CheckInPopUp.kt index da90428..457a2f2 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/general/CheckInPopUp.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/CheckInPopUp.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.uplift.ui.components.profile.checkin.CheckInComplete +import com.cornellappdev.uplift.ui.components.profile.checkin.CheckInFailed import com.cornellappdev.uplift.ui.components.profile.checkin.CheckInPrompt import com.cornellappdev.uplift.ui.theme.AppColors import com.cornellappdev.uplift.ui.viewmodels.profile.CheckInMode @@ -66,6 +67,10 @@ fun CheckInPopUp( CheckInMode.Complete -> CheckInComplete( onClosePopUp = onClosePopUp ) + CheckInMode.Failed -> CheckInFailed( + onRetry = onCheckIn, + onClosePopUp = onClosePopUp + ) } } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/checkin/CheckInFailed.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/checkin/CheckInFailed.kt new file mode 100644 index 0000000..9d5b47c --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/checkin/CheckInFailed.kt @@ -0,0 +1,79 @@ +package com.cornellappdev.uplift.ui.components.profile.checkin + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.uplift.R +import com.cornellappdev.uplift.ui.theme.AppColors +import com.cornellappdev.uplift.ui.theme.AppTextStyles + +@Composable +fun CheckInFailed( + onRetry: () -> Unit, + onClosePopUp: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Couldn't log your workout.", + style = AppTextStyles.BodySemibold, + color = AppColors.Black + ) + Row( + modifier = Modifier.height(34.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + modifier = Modifier + .width(93.dp) + .height(34.dp), + shape = RoundedCornerShape(size = 11.05263.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = AppColors.LightYellow, + contentColor = AppColors.Black + ), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + onClick = onRetry + ) { + Text( + text = "Retry", + style = AppTextStyles.LabelBig, + color = AppColors.Black + ) + } + + Image( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = "close pop up", + contentScale = ContentScale.None, + modifier = Modifier.clickable { onClosePopUp() } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CheckInFailedPreview() { + CheckInFailed({}, {}) +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt index 0a17dac..bdd539c 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt @@ -20,8 +20,9 @@ private const val tag = "CheckInVM" * UI mode for the Check-In pop up. * - [Prompt]: user is near an open gym and can choose to check in. * -[Complete]: a check in was just logged and the confirmation/congratulation state is shown. + * -[Failed]: the log workout mutation failed and the user can retry or dismiss. */ -enum class CheckInMode {Prompt, Complete} +enum class CheckInMode {Prompt, Complete, Failed} /** * UI state backing the Check-In pop-up @@ -126,12 +127,13 @@ class CheckInViewModel @Inject constructor( } /** - * Marks the user as checked in for the day, triggering a cooldown til the end of day and a - * logworkout mutation through [checkInRepository]. On a successful call, transitions UI into - * [CheckInMode.Complete] and bursts confetti from popup through a [confettiRepository]. + * Logs a workout via [checkInRepository]. Only on a successful mutation does this mark the + * user as checked in for the day (triggering the end-of-day cooldown), transition the UI into + * [CheckInMode.Complete], and burst confetti through [confettiRepository] and notify + * [workoutLogRepository] so screens showing history/streaks can refresh. * - * Note: Temporarily skips over failed backend log workout call to keep functionality while auth and - * sign in are not working. + * If the mutation fails, the UI transitions into [CheckInMode.Failed] instead, so the user + * knows the workout wasn't recorded and can retry rather than seeing a false success state. */ fun onCheckIn() = viewModelScope.launch { val currentGymId = uiStateFlow.value.gymId @@ -142,22 +144,35 @@ class CheckInViewModel @Inject constructor( return@launch } try { - checkInRepository.markCheckInToday() - applyMutation { - copy( - showPopUp = true, - mode = CheckInMode.Complete - ) - } - confettiRepository.showConfetti(ConfettiViewModel.ConfettiUiState()) val logged = checkInRepository.logWorkoutFromCheckIn(gymIdInt) if (logged) { Log.d(tag, "Workout successfully logged to backend") + checkInRepository.markCheckInToday() + applyMutation { + copy( + showPopUp = true, + mode = CheckInMode.Complete + ) + } + confettiRepository.showConfetti(ConfettiViewModel.ConfettiUiState()) + workoutLogRepository.notifyWorkoutLogged() } else { Log.e(tag, "Workout failed to log to backend") + applyMutation { + copy( + showPopUp = true, + mode = CheckInMode.Failed + ) + } } } catch (e: Exception) { Log.e(tag, "Error checking in", e) + applyMutation { + copy( + showPopUp = true, + mode = CheckInMode.Failed + ) + } } } From dd3fb07b550618162f25280d7a6a592948da896e Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Thu, 17 Sep 2026 13:40:58 -0400 Subject: [PATCH 2/3] Fix: Workout Logging Bug and Add Workout Log Event to reload Profile Page --- .../data/repositories/WorkoutLogRepository.kt | 22 +++++++++++++++++++ .../ui/viewmodels/profile/CheckInViewModel.kt | 6 +++-- .../ui/viewmodels/profile/ProfileViewModel.kt | 8 +++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/data/repositories/WorkoutLogRepository.kt diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/WorkoutLogRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/WorkoutLogRepository.kt new file mode 100644 index 0000000..f89e563 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/WorkoutLogRepository.kt @@ -0,0 +1,22 @@ +package com.cornellappdev.uplift.data.repositories + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Broadcasts a signal whenever a workout is successfully logged, so that other currently-active + * screens (e.g. history/streaks) can refresh their data without polling or being tightly coupled + * to whatever triggered the log (check-in, manual entry, etc). + */ +@Singleton +class WorkoutLogRepository @Inject constructor() { + private val _workoutLoggedEvent = MutableSharedFlow(extraBufferCapacity = 1) + val workoutLoggedEvent: SharedFlow = _workoutLoggedEvent.asSharedFlow() + + fun notifyWorkoutLogged() { + _workoutLoggedEvent.tryEmit(Unit) + } +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt index bdd539c..ca7e6e9 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.cornellappdev.uplift.data.repositories.CheckInRepository import com.cornellappdev.uplift.data.repositories.ConfettiRepository import com.cornellappdev.uplift.data.repositories.LocationRepository +import com.cornellappdev.uplift.data.repositories.WorkoutLogRepository import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel import com.cornellappdev.uplift.util.isOpen import com.cornellappdev.uplift.util.todayIndex @@ -46,7 +47,8 @@ data class CheckInUiState( @HiltViewModel class CheckInViewModel @Inject constructor( private val checkInRepository: CheckInRepository, - private val confettiRepository: ConfettiRepository + private val confettiRepository: ConfettiRepository, + private val workoutLogRepository: WorkoutLogRepository ) : UpliftViewModel(CheckInUiState()) { private var locationJob: Job? = null @@ -68,7 +70,7 @@ class CheckInViewModel @Inject constructor( showPopUp = true, mode = if (inComplete) CheckInMode.Complete else CheckInMode.Prompt, gymName = gym.name, - gymId = gym.id, + gymId = gym.facilityId, timeText = checkInRepository.formatTime(System.currentTimeMillis()) ) } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt index 76b15ad..a96a447 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt @@ -4,6 +4,7 @@ import android.net.Uri import android.util.Log import androidx.lifecycle.viewModelScope import com.cornellappdev.uplift.data.repositories.ProfileRepository +import com.cornellappdev.uplift.data.repositories.WorkoutLogRepository import com.cornellappdev.uplift.ui.UpliftRootRoute import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem import com.cornellappdev.uplift.ui.nav.RootNavigationRepository @@ -12,6 +13,7 @@ import com.cornellappdev.uplift.util.timeAgoString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.collectLatest import java.time.DayOfWeek import java.time.Instant import java.time.LocalDate @@ -52,12 +54,18 @@ data class ProfileUiState( class ProfileViewModel @Inject constructor( private val profileRepository: ProfileRepository, private val rootNavigationRepository: RootNavigationRepository, + private val workoutLogRepository: WorkoutLogRepository, ) : UpliftViewModel(ProfileUiState()) { private var loadingJob: Job? = null init { reload() + viewModelScope.launch { + workoutLogRepository.workoutLoggedEvent.collectLatest { + reload() + } + } } fun reload() { From fd08ed739965f3c877b205c25e4d3b2cbee81ec7 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Thu, 17 Sep 2026 13:41:59 -0400 Subject: [PATCH 3/3] Update gitignore --- .gitignore | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index d72e128..feb16df 100644 --- a/.gitignore +++ b/.gitignore @@ -38,18 +38,7 @@ captures/ # IntelliJ *.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml -.idea/assetWizardSettings.xml -.idea/dictionaries -.idea/libraries -.idea/* -# Android Studio 3 in .gitignore file. -.idea/caches -.idea/modules.xml -# Comment next line if keeping position of elements in Navigation Editor is relevant for you -.idea/navEditor.xml +**/.idea/ # Keystore files # Uncomment the following lines if you do not want to check your keystore files in.