-
Notifications
You must be signed in to change notification settings - Fork 0
Fix: Workout Checkin Logging #117
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
base: main
Are you sure you want to change the base?
Changes from all commits
f806f8a
dd3fb07
fd08ed7
906f695
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Unit>(extraBufferCapacity = 1) | ||
| val workoutLoggedEvent: SharedFlow<Unit> = _workoutLoggedEvent.asSharedFlow() | ||
|
|
||
| fun notifyWorkoutLogged() { | ||
| _workoutLoggedEvent.tryEmit(Unit) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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({}, {}) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -20,8 +21,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 | ||
|
|
@@ -45,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>(CheckInUiState()) { | ||
|
|
||
| private var locationJob: Job? = null | ||
|
|
@@ -67,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()) | ||
| ) | ||
| } | ||
|
|
@@ -126,12 +129,14 @@ 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 marks the | ||
| * user as checked in for the day (triggering the end-of-day cooldown, awaited and retried once | ||
| * if the write fails; the workout mutation itself is never retried), 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 +147,37 @@ 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") | ||
| if (!checkInRepository.markCheckInToday() && !checkInRepository.markCheckInToday()) { | ||
| Log.e(tag, "Workout logged but check-in cooldown could not be persisted") | ||
| } | ||
| applyMutation { | ||
| copy( | ||
| showPopUp = true, | ||
| mode = CheckInMode.Complete | ||
| ) | ||
| } | ||
| confettiRepository.showConfetti(ConfettiViewModel.ConfettiUiState()) | ||
| workoutLogRepository.notifyWorkoutLogged() | ||
|
Comment on lines
150
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '95,160p' app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt
sed -n '140,185p' app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt
rg -n "markCheckInToday|checkInPromptAllowed|onCheckIn|CheckInMode.Failed" app/src/main/javaRepository: cuappdev/uplift-android Length of output: 6483 Make cooldown persistence observable without rerunning the workout.
If the date write fails, 🤖 Prompt for AI Agents |
||
| } 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 | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be trying to merge this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we both had to change them locally to true and since the features/releases that needed the feature flags to be false aren't in progress anymore, I think it would be useful for main to have them on rather than everyone making the same change locally so that workout checkin and the onboarding flow work, especially as we wrap up profiles, but idk if this best practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this diff in particular is fine since it's a debug build config field, but I'm not sure why the flags are on for the release build. @melissavelasquezz could you double check this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I turned them on back in this PR when I implemented the profile page: https://github.com/cuappdev/uplift-android/pull/105/changes#diff-51a0b488f963eb0be6c6599bf5df497313877cf5bdff3950807373912ac1cdc9
true means that user flow does go through onboarding/login and workout check ins are enabled. We had originally added the flags as false to block workout checkin from being released w/o profiles yet. Is this not what we would want in the profiles release?