From c6de9459a374f24c0dd2aece7ceaa555e26b5169 Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Mon, 23 Feb 2026 14:58:14 -0500 Subject: [PATCH 01/14] started goals page for onboarding --- .DS_Store | Bin 6148 -> 6148 bytes .../screens/onboarding/GoalsPromptScreen.kt | 96 ++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt diff --git a/.DS_Store b/.DS_Store index da07aa6853bb7b7687f6e861317c0d52c61a4ec7..8d2fcea67f1d759230e564672726f5dc907d3e5e 100644 GIT binary patch delta 126 zcmZoMXfc=|&e%4wP>hv>fq{WzVxfo(6OaJ{AexbZL4YASCn-Na2PDKiu~0(HghzREg2)o)i480OdutH3 diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt new file mode 100644 index 00000000..e6b32700 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt @@ -0,0 +1,96 @@ +package com.cornellappdev.uplift.ui.screens.onboarding + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +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 com.cornellappdev.uplift.ui.components.goalsetting.GoalSlider +import com.cornellappdev.uplift.util.GRAY01 +import com.cornellappdev.uplift.util.montserratFamily + +/** + * @param goalValue: value of the goal slider + * @param onGoalValueChange: callback for when the goal slider value is changed + * @return GoalPromptScreen composable + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GoalPromptScreen( + /* TODO: Replace functions with viewmodel calls */ + goalValue: Float, + onGoalValueChange: (Float) -> Unit +) { + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "Set your goals.", + modifier = Modifier.padding(top = 20.dp), + fontSize = 24.sp, + fontFamily = montserratFamily, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Left + ) + }, + modifier = Modifier + .height(120.dp) + .shadow(elevation = 20.dp, ambientColor = GRAY01), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.White, + ) + + ) + }, + modifier = Modifier.fillMaxSize(), + ) { padding -> + Column( + modifier = Modifier + .background(color = Color.White) + .padding( + top = padding.calculateTopPadding(), + ) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.SpaceBetween + ) { + /* Groups the goal slider and workout reminders together */ + Column { + GoalSlider(value = goalValue, onValueChange = onGoalValueChange) + + } + } + + } +} + + +@Preview(showBackground = true) +@Composable +fun GoalPromptScreenPreview() { + var sliderVal by remember { mutableFloatStateOf(1f) } + GoalPromptScreen(goalValue = sliderVal, onGoalValueChange = { sliderVal = it }) +} From ae1b752029ef3bbbcb488860d640ca07564704da Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Mon, 23 Feb 2026 17:40:26 -0500 Subject: [PATCH 02/14] finished ui --- .../screens/onboarding/GoalsPromptScreen.kt | 72 ++++++++++++------- .../screens/onboarding/SignInPromptScreen.kt | 2 +- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt index e6b32700..f9316ff4 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt @@ -2,8 +2,11 @@ package com.cornellappdev.uplift.ui.screens.onboarding 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.fillMaxHeight 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.foundation.rememberScrollState @@ -18,15 +21,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight -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.credentials.Credential import com.cornellappdev.uplift.ui.components.goalsetting.GoalSlider +import com.cornellappdev.uplift.ui.components.onboarding.auth.LogInButton import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.montserratFamily @@ -40,57 +45,72 @@ import com.cornellappdev.uplift.util.montserratFamily fun GoalPromptScreen( /* TODO: Replace functions with viewmodel calls */ goalValue: Float, - onGoalValueChange: (Float) -> Unit + onGoalValueChange: (Float) -> Unit, + onSignInWithGoogle: (credential: Credential) -> Unit, + onSkip: () -> Unit ) { - Scaffold( topBar = { - TopAppBar( - title = { + TopAppBar( + title = { + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.BottomStart + ) { Text( text = "Set your goals.", - modifier = Modifier.padding(top = 20.dp), fontSize = 24.sp, fontFamily = montserratFamily, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Left + modifier = Modifier.padding(bottom = 16.dp) ) - }, - modifier = Modifier - .height(120.dp) - .shadow(elevation = 20.dp, ambientColor = GRAY01), - colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.White, - ) - + } + }, + modifier = Modifier + .height(120.dp) + .shadow(elevation = 20.dp, ambientColor = GRAY01), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.White, ) + ) }, modifier = Modifier.fillMaxSize(), ) { padding -> Column( modifier = Modifier - .background(color = Color.White) - .padding( - top = padding.calculateTopPadding(), - ) + .padding(padding) .fillMaxSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween + .background(color = Color.White), + horizontalAlignment = Alignment.CenterHorizontally ) { - /* Groups the goal slider and workout reminders together */ - Column { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { GoalSlider(value = goalValue, onValueChange = onGoalValueChange) + } + // Buttons pinned to the bottom + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + LogInButton { onSignInWithGoogle } + SkipButton { onSkip } } } - } } - @Preview(showBackground = true) @Composable fun GoalPromptScreenPreview() { var sliderVal by remember { mutableFloatStateOf(1f) } - GoalPromptScreen(goalValue = sliderVal, onGoalValueChange = { sliderVal = it }) + GoalPromptScreen(goalValue = sliderVal, onGoalValueChange = { sliderVal = it }, {}, {}) } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt index 4bf8778f..3c7ae7ae 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt @@ -112,7 +112,7 @@ private fun SignInPromptScreenContent( } @Composable -private fun SkipButton(onClick: () -> Unit) { +fun SkipButton(onClick: () -> Unit) { TextButton( onClick = onClick ) { From 4739034e29b17216aa23938a100caa4ed92418bc Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Wed, 4 Mar 2026 12:26:17 -0500 Subject: [PATCH 03/14] added screen to onboarding flow --- .DS_Store | Bin 6148 -> 6148 bytes .../uplift/ui/MainNavigationWrapper.kt | 8 +++ .../screens/onboarding/GoalsPromptScreen.kt | 22 ++++--- .../onboarding/GoalsPromptViewModel.kt | 58 ++++++++++++++++++ .../onboarding/ProfileCreationViewModel.kt | 7 ++- 5 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt diff --git a/.DS_Store b/.DS_Store index 8d2fcea67f1d759230e564672726f5dc907d3e5e..9042e813efad8845e04a0c1b3c236620ba9a0d86 100644 GIT binary patch delta 127 zcmZoMXfc=|&e%S&P;8=}A|va>0Ba!8qvOfSki<~Rkjaq4kdjiIoRgHFpThtIYz*lP zMGT1yDL{!-WC;$2a)u0`L { ProfileCreationScreen() } + composable { + GoalPromptScreen() + } composable { CapacityReminderScreen() } @@ -329,6 +333,10 @@ sealed class UpliftRootRoute { @Serializable data object Profile : UpliftRootRoute() + @Serializable + data object GoalsPrompt : UpliftRootRoute() + + @Serializable data object CapacityReminders : UpliftRootRoute() diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt index f9316ff4..bd736592 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt @@ -30,8 +30,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.credentials.Credential +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.cornellappdev.uplift.ui.components.goalsetting.GoalSlider import com.cornellappdev.uplift.ui.components.onboarding.auth.LogInButton +import com.cornellappdev.uplift.ui.viewmodels.onboarding.GoalsPromptViewModel import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.montserratFamily @@ -43,12 +46,11 @@ import com.cornellappdev.uplift.util.montserratFamily @OptIn(ExperimentalMaterial3Api::class) @Composable fun GoalPromptScreen( - /* TODO: Replace functions with viewmodel calls */ - goalValue: Float, - onGoalValueChange: (Float) -> Unit, - onSignInWithGoogle: (credential: Credential) -> Unit, - onSkip: () -> Unit + viewModel: GoalsPromptViewModel = hiltViewModel(), ) { + + val currentGoal by viewModel.goalValue.collectAsStateWithLifecycle() + Scaffold( topBar = { TopAppBar( @@ -90,7 +92,7 @@ fun GoalPromptScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - GoalSlider(value = goalValue, onValueChange = onGoalValueChange) + GoalSlider(value = currentGoal, onValueChange = { viewModel.onGoalValueChange(it) }) } // Buttons pinned to the bottom @@ -101,8 +103,8 @@ fun GoalPromptScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - LogInButton { onSignInWithGoogle } - SkipButton { onSkip } + LogInButton { viewModel.onSignInWithGoogle() } + SkipButton { viewModel.onSkip() } } } } @@ -111,6 +113,6 @@ fun GoalPromptScreen( @Preview(showBackground = true) @Composable fun GoalPromptScreenPreview() { - var sliderVal by remember { mutableFloatStateOf(1f) } - GoalPromptScreen(goalValue = sliderVal, onGoalValueChange = { sliderVal = it }, {}, {}) + var sliderVal by remember { mutableFloatStateOf(0f) } + GoalPromptScreen() } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt new file mode 100644 index 00000000..72865874 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt @@ -0,0 +1,58 @@ +package com.cornellappdev.uplift.ui.viewmodels.onboarding + +import android.net.Uri +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.cornellappdev.uplift.data.repositories.UserInfoRepository +import com.cornellappdev.uplift.ui.UpliftRootRoute +import com.cornellappdev.uplift.ui.nav.RootNavigationRepository +import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class GoalsPromptUiState( + val name: String = "", + val imageUri: Uri? = null +) + +@HiltViewModel +class GoalsPromptViewModel @Inject constructor( + private val userInfoRepository: UserInfoRepository, + private val rootNavigationRepository: RootNavigationRepository, +) : UpliftViewModel(GoalsPromptUiState()) { + + private val _goalValue = MutableStateFlow(0f) + val goalValue: StateFlow = _goalValue.asStateFlow() + + init { + viewModelScope.launch { + // TODO: Change this later to reflect correct inputs + val user = userInfoRepository.getFirebaseUser() + val name = user?.displayName ?: "" + applyMutation { + copy(name = name) + } + } + } + + fun onGoalValueChange(newValue: Float) { + _goalValue.value = newValue + } + + // TODO: Change skip and google sign in to reflect correct behavior + fun onSkip() { + navigateToHome() + } + + fun onSignInWithGoogle() { + navigateToHome() + } + + fun navigateToHome() { + rootNavigationRepository.navigate(UpliftRootRoute.Home) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index 8304a629..03327365 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -38,7 +38,7 @@ class ProfileCreationViewModel @Inject constructor( val email = user?.email ?: "" val netId = email.substring(0, email.indexOf('@')) if (userInfoRepository.createUser(email, name, netId)) { - navigateToHome() + navigateToGoals() } else { //TODO: Add error handling Log.e("Error", "User not created") @@ -53,6 +53,11 @@ class ProfileCreationViewModel @Inject constructor( } } + private fun navigateToGoals() { + rootNavigationRepository.navigate(UpliftRootRoute.GoalsPrompt) + } + + // Possibly get rid of this function private fun navigateToHome() { rootNavigationRepository.navigate(UpliftRootRoute.Home) } From 5f559f42dac92953af3d3e2aa1fb11ef910c8b8b Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Sat, 7 Mar 2026 21:26:33 -0500 Subject: [PATCH 04/14] rebasing changes --- .DS_Store | Bin 6148 -> 0 bytes app/build.gradle | 2 +- .../screens/onboarding/GoalsPromptScreen.kt | 36 +++++--- .../screens/onboarding/SignInPromptScreen.kt | 2 +- .../reminders/WorkoutReminderScreen.kt | 82 +++++++++++++----- 5 files changed, 87 insertions(+), 35 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 9042e813efad8845e04a0c1b3c236620ba9a0d86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRW;41K08io~W%mU9JeFsjN4dI2b@3R09*{m^Z8T$^W)L8&6t9gE7A?3eLp zCi50?3_upA`U#i=sHlqgQZp8ftB&N(Ju{-{9&gRM>l*b=JaiSbc7j*zu&WQldUJiQ z_HEO(TPE=U%&^7@A81eylkc&`o_o{`Y=(gfXS_3T!~rcfI1htYUo8@XfnXpQ2nK?I zn`A&+Dy4cT7&aIP27-ZK2K4)osEXOKFKC|*8XEzKQ@Ty~Y_kM0se_mu`-1dPq?8g% zsrZN?rJVlcdD*cqSjr(je2AYcKB0)6&i#|6Lvq2e!9Xx@$$$^(bM61n)Ia9`%c58X z1Hr&eF(A|BYPsN*ytj@nr@b~&U#LIATuZ01R!p>3%!Rh%$3b4wXU4N*U(m|ww{l|q N2q-Th!N7ko@C{hGD1rb0 diff --git a/app/build.gradle b/app/build.gradle index 466d8b21..15e1f056 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,7 +58,7 @@ android { "GOOGLE_AUTH_CLIENT_ID", secretsProperties["GOOGLE_AUTH_CLIENT_ID"] ) signingConfig signingConfigs.debug - buildConfigField("boolean", "ONBOARDING_FLAG", "false") + buildConfigField("boolean", "ONBOARDING_FLAG", "true") buildConfigField("boolean", "CHECK_IN_FLAG", "false") } } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt index bd736592..6bff3f97 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt @@ -29,7 +29,6 @@ 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 androidx.credentials.Credential import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.cornellappdev.uplift.ui.components.goalsetting.GoalSlider @@ -38,12 +37,7 @@ import com.cornellappdev.uplift.ui.viewmodels.onboarding.GoalsPromptViewModel import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.montserratFamily -/** - * @param goalValue: value of the goal slider - * @param onGoalValueChange: callback for when the goal slider value is changed - * @return GoalPromptScreen composable - */ -@OptIn(ExperimentalMaterial3Api::class) +// Use this as reference but delete later @Composable fun GoalPromptScreen( viewModel: GoalsPromptViewModel = hiltViewModel(), @@ -51,6 +45,22 @@ fun GoalPromptScreen( val currentGoal by viewModel.goalValue.collectAsStateWithLifecycle() + GoalPromptContent( + currentGoal, + { viewModel.onGoalValueChange(it) }, + { viewModel.onSkip() }, + { viewModel.onSignInWithGoogle() } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun GoalPromptContent( + currentGoal: Float, + onValueChange: (Float) -> Unit = {}, + onSkip: () -> Unit = {}, + onLogin: () -> Unit = {} +) { Scaffold( topBar = { TopAppBar( @@ -92,7 +102,7 @@ fun GoalPromptScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - GoalSlider(value = currentGoal, onValueChange = { viewModel.onGoalValueChange(it) }) + GoalSlider(value = currentGoal, onValueChange = onValueChange) } // Buttons pinned to the bottom @@ -103,8 +113,7 @@ fun GoalPromptScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - LogInButton { viewModel.onSignInWithGoogle() } - SkipButton { viewModel.onSkip() } + LogInButton { onLogin() } } } } @@ -112,7 +121,10 @@ fun GoalPromptScreen( @Preview(showBackground = true) @Composable -fun GoalPromptScreenPreview() { +private fun GoalPromptScreenPreview() { var sliderVal by remember { mutableFloatStateOf(0f) } - GoalPromptScreen() + GoalPromptContent( + sliderVal, + { sliderVal = it} + ) } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt index 3c7ae7ae..4bf8778f 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/SignInPromptScreen.kt @@ -112,7 +112,7 @@ private fun SignInPromptScreenContent( } @Composable -fun SkipButton(onClick: () -> Unit) { +private fun SkipButton(onClick: () -> Unit) { TextButton( onClick = onClick ) { diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt index d36c5631..cebcd391 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt @@ -56,7 +56,7 @@ data class Reminder( */ @Composable fun WorkoutReminderScreen( - /* TODO: Replace functions with viewmodel calls */ + /* TODO: Add view model calls */ reminders: List = emptyList(), onRemindersChange: (List) -> Unit, goalValue: Float, @@ -66,10 +66,48 @@ fun WorkoutReminderScreen( onNext: () -> Unit = {}, onSkip: () -> Unit = {} ) { + // These states are local to this screen var selectedReminder by remember { mutableStateOf(null) } var addNewReminderState by remember { mutableStateOf(false) } var deleteDialogOpen by remember { mutableStateOf(false) } + WorkoutReminderContent( + reminders = reminders, + selectedReminder = selectedReminder, + addNewReminderState = addNewReminderState, + deleteDialogOpen = deleteDialogOpen, + goalValue = goalValue, + isOnboarding = isOnboarding, + // Pass callbacks to update the state defined above + onSelectedReminderChange = { selectedReminder = it }, + onAddNewReminderStateChange = { addNewReminderState = it }, + onDeleteDialogOpenChange = { deleteDialogOpen = it }, + onRemindersChange = onRemindersChange, + onGoalValueChange = onGoalValueChange, + onBackClick = onBackClick, + onNext = onNext, + onSkip = onSkip + ) +} + + +@Composable +private fun WorkoutReminderContent( + reminders: List, + selectedReminder: Reminder?, + addNewReminderState: Boolean, + deleteDialogOpen: Boolean, + goalValue: Float, + isOnboarding: Boolean, + onSelectedReminderChange: (Reminder?) -> Unit, + onAddNewReminderStateChange: (Boolean) -> Unit, + onDeleteDialogOpenChange: (Boolean) -> Unit, + onRemindersChange: (List) -> Unit, + onGoalValueChange: (Float) -> Unit, + onBackClick: () -> Unit, + onNext: () -> Unit, + onSkip: () -> Unit +) { Scaffold( topBar = { UpliftTopBarWithBack( @@ -83,42 +121,44 @@ fun WorkoutReminderScreen( Column( modifier = Modifier .background(color = Color.White) - .padding( - top = padding.calculateTopPadding(), - ) + .padding(top = padding.calculateTopPadding()) .fillMaxSize() .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween ) { - /* Groups the goal slider and workout reminders together */ Column { GoalSlider(value = goalValue, onValueChange = onGoalValueChange) WorkoutReminders( selectedReminder = selectedReminder, - onSelectedReminderChange = { selectedReminder = it }, + onSelectedReminderChange = onSelectedReminderChange, reminders = reminders, onRemindersChange = onRemindersChange, addNewReminderState = addNewReminderState, - onAddNewReminderStateChange = { addNewReminderState = it }, - openDelete = { deleteDialogOpen = true } + onAddNewReminderStateChange = onAddNewReminderStateChange, + openDelete = { onDeleteDialogOpenChange(true) } ) } - if (isOnboarding) OnboardingButtons(onNext, onSkip) + if (isOnboarding) { + OnboardingButtons(onNext, onSkip) + } + } + + if (deleteDialogOpen) { + DeleteDialog( + deleteDialogOpen = deleteDialogOpen, + onConfirm = { + selectedReminder?.let { reminder -> + onRemindersChange(reminders.filter { it != reminder }) + } + onDeleteDialogOpenChange(false) + onSelectedReminderChange(null) + onAddNewReminderStateChange(false) + }, + onDismiss = { onDeleteDialogOpenChange(false) } + ) } - DeleteDialog( - deleteDialogOpen = deleteDialogOpen, - onConfirm = { - selectedReminder?.let { reminder -> - onRemindersChange(reminders.filter { it != reminder }) - } - deleteDialogOpen = false - selectedReminder = null - addNewReminderState = false - }, - onDismiss = { deleteDialogOpen = false } - ) } } From a9f3ab0fc36e77b795ea545a36fc18edd7b2dddc Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Sat, 7 Mar 2026 21:48:31 -0500 Subject: [PATCH 05/14] finished redoing goals onboarding screen --- .../reminders/WorkoutReminderScreen.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt index cebcd391..d5e4cc99 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.padding @@ -126,25 +127,29 @@ private fun WorkoutReminderContent( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween ) { - Column { + Column(modifier = Modifier.fillMaxSize()) { GoalSlider(value = goalValue, onValueChange = onGoalValueChange) - WorkoutReminders( - selectedReminder = selectedReminder, - onSelectedReminderChange = onSelectedReminderChange, - reminders = reminders, - onRemindersChange = onRemindersChange, - addNewReminderState = addNewReminderState, - onAddNewReminderStateChange = onAddNewReminderStateChange, - openDelete = { onDeleteDialogOpenChange(true) } - ) + if (!isOnboarding) { + WorkoutReminders( + selectedReminder = selectedReminder, + onSelectedReminderChange = onSelectedReminderChange, + reminders = reminders, + onRemindersChange = onRemindersChange, + addNewReminderState = addNewReminderState, + onAddNewReminderStateChange = onAddNewReminderStateChange, + openDelete = { onDeleteDialogOpenChange(true) } + ) + } } + Spacer(modifier = Modifier.weight(1f)) if (isOnboarding) { OnboardingButtons(onNext, onSkip) } } + if (deleteDialogOpen) { DeleteDialog( deleteDialogOpen = deleteDialogOpen, @@ -218,6 +223,7 @@ fun WorkoutReminderScreenPreview() { reminders.clear() reminders.addAll(updatedReminders) }, + isOnboarding = true, goalValue = sliderVal, onGoalValueChange = { sliderVal = it }, onBackClick = {}, From 08ce70f98f8f290a7f32f15171a04ff699e32e88 Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Sat, 7 Mar 2026 23:31:05 -0500 Subject: [PATCH 06/14] connected goal onboarding to viewmodel --- .../data/repositories/DatastoreRepository.kt | 1 + .../data/repositories/UserInfoRepository.kt | 11 +++- .../onboarding/ProfileCreationScreen.kt | 8 +-- .../reminders/WorkoutReminderScreen.kt | 56 +++++++++++++++---- .../onboarding/ProfileCreationViewModel.kt | 46 +++++++++++++-- 5 files changed, 101 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt index 65be80e0..49c11e93 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt @@ -25,6 +25,7 @@ object PreferencesKeys { val USERNAME = stringPreferencesKey("username") val NETID = stringPreferencesKey("netId") val EMAIL = stringPreferencesKey("email") + val GOAL = intPreferencesKey("workoutGoal") val SKIP = booleanPreferencesKey("skip") val FCM_TOKEN = stringPreferencesKey("fcmToken") val DECLINED_NOTIFICATION_PERMISSION = diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index c0166b90..c7762ddd 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -24,7 +24,7 @@ class UserInfoRepository @Inject constructor( private val dataStore: DataStore, ){ - suspend fun createUser(email: String, name: String, netId: String): Boolean { + suspend fun createUser(email: String, name: String, netId: String, skip: Boolean, goal: Int): Boolean { try{ val response = apolloClient.mutation( CreateUserMutation( @@ -38,6 +38,9 @@ class UserInfoRepository @Inject constructor( storeUsername(name) storeEmail(email) storeSkip(false) + if (!skip) { + storeGoal(goal) + } Log.d("UserInfoRepositoryImpl", "User created successfully" + response.data) return true } catch (e: Exception) { @@ -111,6 +114,12 @@ class UserInfoRepository @Inject constructor( } } + private suspend fun storeGoal(goal: Int) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.GOAL] = goal + } + } + suspend fun storeSkip(skip: Boolean) { dataStore.edit { preferences -> preferences[PreferencesKeys.SKIP] = skip diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/ProfileCreationScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/ProfileCreationScreen.kt index 72345093..98b0ec22 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/ProfileCreationScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/ProfileCreationScreen.kt @@ -59,7 +59,7 @@ fun ProfileCreationScreen( ) = with(profileCreationViewModel.collectUiStateValue()) { ProfileCreationScreenContent( profileCreationViewModel::onPhotoSelected, - profileCreationViewModel::createUser, + profileCreationViewModel::navigateToGoals, name ) } @@ -68,7 +68,7 @@ fun ProfileCreationScreen( @OptIn(ExperimentalMaterial3Api::class) private fun ProfileCreationScreenContent( onPhotoSelected: (Uri) -> Unit, - createUser: () -> Unit, + navigateToGoals: () -> Unit, name: String, ) { val checkboxColors: CheckboxColors = @@ -145,7 +145,7 @@ private fun ProfileCreationScreenContent( ReadyToUplift(opacityModifier) UpliftButton( - onClick = createUser, + onClick = navigateToGoals, enabled = allChecked, text = if (allChecked) "Get started" else "Next", width = 144.dp, @@ -256,7 +256,7 @@ private fun InfoCheckboxRow( private fun ProfileCreationScreenPreview() { ProfileCreationScreenContent( onPhotoSelected = {}, - createUser = {}, + navigateToGoals = {}, name = "John Doe", ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt index d5e4cc99..ed2cf7b6 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt @@ -24,11 +24,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.cornellappdev.uplift.ui.components.general.UpliftTopBarWithBack import com.cornellappdev.uplift.ui.components.goalsetting.DeleteDialog import com.cornellappdev.uplift.ui.components.goalsetting.GoalSlider import com.cornellappdev.uplift.ui.components.goalsetting.WorkoutReminders import com.cornellappdev.uplift.ui.components.general.UpliftButton +import com.cornellappdev.uplift.ui.viewmodels.onboarding.ProfileCreationViewModel +import com.cornellappdev.uplift.ui.viewmodels.profile.SettingsViewModel import com.cornellappdev.uplift.util.GRAY04 import com.cornellappdev.uplift.util.montserratFamily @@ -44,10 +48,41 @@ data class Reminder( val enabled: Boolean ) +@Composable +fun WorkoutReminderOnboardingScreen( + viewModel: ProfileCreationViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle() + val goalValue = uiState.goal + + WorkoutReminderScreen( + goalValue = goalValue, + isOnboarding = true, + // Pass callbacks to update the state defined above + onGoalValueChange = { viewModel.updateGoals(it.toInt()) }, + onBackClick = { viewModel.onBackClick() }, + onNext = { viewModel.onNext() }, + onSkip = { viewModel.onSkip() }, + ) +} + +@Composable +fun WorkoutReminderSettingsScreen( + viewModel: SettingsViewModel = hiltViewModel(), +) { + /* TODO: Connect to view model */ + WorkoutReminderScreen( + goalValue = 1.0f, + isOnboarding = false, + // Pass callbacks to update the state defined above + onGoalValueChange = {}, + onBackClick = { viewModel.onBack() }, + ) +} + /** * @param reminders: list of reminders * @param onRemindersChange: callback for when reminders are changed - * @param goalValue: value of the goal slider * @param onGoalValueChange: callback for when the goal slider value is changed * @param onBackClick: callback for when the back button is clicked * @param isOnboarding: whether the user is in onboarding @@ -58,15 +93,16 @@ data class Reminder( @Composable fun WorkoutReminderScreen( /* TODO: Add view model calls */ - reminders: List = emptyList(), - onRemindersChange: (List) -> Unit, goalValue: Float, onGoalValueChange: (Float) -> Unit, + reminders: List = emptyList(), + onRemindersChange: (List) -> Unit = {}, onBackClick: () -> Unit, isOnboarding: Boolean = false, onNext: () -> Unit = {}, onSkip: () -> Unit = {} ) { + // These states are local to this screen var selectedReminder by remember { mutableStateOf(null) } var addNewReminderState by remember { mutableStateOf(false) } @@ -77,7 +113,7 @@ fun WorkoutReminderScreen( selectedReminder = selectedReminder, addNewReminderState = addNewReminderState, deleteDialogOpen = deleteDialogOpen, - goalValue = goalValue, + goalValue = goalValue.toInt(), isOnboarding = isOnboarding, // Pass callbacks to update the state defined above onSelectedReminderChange = { selectedReminder = it }, @@ -98,16 +134,16 @@ private fun WorkoutReminderContent( selectedReminder: Reminder?, addNewReminderState: Boolean, deleteDialogOpen: Boolean, - goalValue: Float, + goalValue: Int, isOnboarding: Boolean, onSelectedReminderChange: (Reminder?) -> Unit, onAddNewReminderStateChange: (Boolean) -> Unit, onDeleteDialogOpenChange: (Boolean) -> Unit, onRemindersChange: (List) -> Unit, - onGoalValueChange: (Float) -> Unit, + onGoalValueChange: (Float) -> Unit = {}, onBackClick: () -> Unit, - onNext: () -> Unit, - onSkip: () -> Unit + onNext: () -> Unit = {}, + onSkip: () -> Unit = {} ) { Scaffold( topBar = { @@ -128,7 +164,7 @@ private fun WorkoutReminderContent( verticalArrangement = Arrangement.SpaceBetween ) { Column(modifier = Modifier.fillMaxSize()) { - GoalSlider(value = goalValue, onValueChange = onGoalValueChange) + GoalSlider(value = goalValue.toFloat(), onValueChange = onGoalValueChange) if (!isOnboarding) { WorkoutReminders( @@ -223,7 +259,7 @@ fun WorkoutReminderScreenPreview() { reminders.clear() reminders.addAll(updatedReminders) }, - isOnboarding = true, + isOnboarding = false, goalValue = sliderVal, onGoalValueChange = { sliderVal = it }, onBackClick = {}, diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index 03327365..c59c28dc 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -2,6 +2,7 @@ package com.cornellappdev.uplift.ui.viewmodels.onboarding import android.net.Uri import android.util.Log +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import com.cornellappdev.uplift.data.repositories.UserInfoRepository import com.cornellappdev.uplift.ui.UpliftRootRoute @@ -13,7 +14,9 @@ import javax.inject.Inject data class ProfileCreationUiState( val name: String = "", - val imageUri: Uri? = null + val imageUri: Uri? = null, + val isGoalSkipped: Boolean = false, + val goal: Float = 0.0f // Goal slider val is stored as float but we could change this ) @HiltViewModel @@ -32,13 +35,18 @@ class ProfileCreationViewModel @Inject constructor( } } - fun createUser() = viewModelScope.launch { + private fun createUser() = viewModelScope.launch { val user = userInfoRepository.getFirebaseUser() val name = user?.displayName ?: "" val email = user?.email ?: "" val netId = email.substring(0, email.indexOf('@')) - if (userInfoRepository.createUser(email, name, netId)) { - navigateToGoals() + val isSkipped = getStateValue().isGoalSkipped + var goal = 0 + if (!isSkipped) { + goal = getStateValue().goal.toInt() + } + if (userInfoRepository.createUser(email, name, netId, isSkipped, goal)) { + navigateToHome() } else { //TODO: Add error handling Log.e("Error", "User not created") @@ -53,11 +61,37 @@ class ProfileCreationViewModel @Inject constructor( } } - private fun navigateToGoals() { + fun onBackClick() { + navigateToProfileCreation() + } + + fun updateGoals(newGoal: Int) { + applyMutation { + copy(goal = newGoal.toFloat()) + } + } + + fun onSkip() { + applyMutation { + copy(isGoalSkipped = true) + } + createUser() + navigateToHome() + } + + fun onNext() { + createUser() + } + + private fun navigateToProfileCreation() { + rootNavigationRepository.navigate(UpliftRootRoute.ProfileCreation) + } + + + fun navigateToGoals() { rootNavigationRepository.navigate(UpliftRootRoute.GoalsPrompt) } - // Possibly get rid of this function private fun navigateToHome() { rootNavigationRepository.navigate(UpliftRootRoute.Home) } From f0f633a41250326f93967c43ee65fd9673a0a666 Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Tue, 10 Mar 2026 21:56:49 -0400 Subject: [PATCH 07/14] finished onboarding networking --- app/src/main/graphql/User.graphql | 17 +++- app/src/main/graphql/schema.graphqls | 93 ++++++++++++------- .../data/repositories/DatastoreRepository.kt | 2 + .../data/repositories/UserInfoRepository.kt | 50 +++++++++- .../uplift/ui/MainNavigationWrapper.kt | 9 +- .../reminders/WorkoutReminderScreen.kt | 1 - .../onboarding/ProfileCreationViewModel.kt | 3 +- 7 files changed, 130 insertions(+), 45 deletions(-) diff --git a/app/src/main/graphql/User.graphql b/app/src/main/graphql/User.graphql index b3765527..09439e63 100644 --- a/app/src/main/graphql/User.graphql +++ b/app/src/main/graphql/User.graphql @@ -33,7 +33,7 @@ query getUserByNetId($netId: String!) { } } -mutation SetWorkoutGoals($id: Int!, $workoutGoal: [String!]!) { +mutation SetWorkoutGoals($id: Int!, $workoutGoal: Int!) { setWorkoutGoals(userId: $id, workoutGoal: $workoutGoal) { ...userFields } @@ -43,4 +43,17 @@ mutation LogWorkout($facilityId: Int!, $workoutTime: DateTime!, $id: Int!) { logWorkout(facilityId: $facilityId, userId: $id, workoutTime: $workoutTime) { ...workoutFields } -} \ No newline at end of file +} + +mutation DeleteUser($userId: Int!) { + deleteUser(userId: $userId) { + ...userFields + } +} + +mutation LoginUser($netId: String!) { + loginUser(netId: $netId) { + accessToken + refreshToken + } +} diff --git a/app/src/main/graphql/schema.graphqls b/app/src/main/graphql/schema.graphqls index 9df7f3a7..2b3e050b 100644 --- a/app/src/main/graphql/schema.graphqls +++ b/app/src/main/graphql/schema.graphqls @@ -36,16 +36,6 @@ type Query { """ getAllReports: [Report] - """ - Get the workout goals of a user by ID. - """ - getWorkoutGoals(id: Int!): [String] - - """ - Get the current and max workout streak of a user. - """ - getUserStreak(id: Int!): JSONString - """ Get all facility hourly average capacities. """ @@ -358,16 +348,22 @@ type User { name: String! - activeStreak: Int + activeStreak: Int! + + maxStreak: Int! + + workoutGoal: Int - maxStreak: Int + lastGoalChange: DateTime - workoutGoal: [DayOfWeekGraphQLEnum] + lastStreak: Int! encodedImage: String giveaways: [Giveaway] + goalHistory: [WorkoutGoalHistory] + friendRequestsSent: [Friendship] friendRequestsReceived: [Friendship] @@ -375,22 +371,16 @@ type User { friendships: [Friendship] friends: [User] -} - -enum DayOfWeekGraphQLEnum { - MONDAY - - TUESDAY - - WEDNESDAY - - THURSDAY - - FRIDAY - SATURDAY + """ + Get the total number of gym days (unique workout days) for user. + """ + totalGymDays: Int! - SUNDAY + """ + The start date of the most recent active streak, up until the current date. + """ + streakStart: Date } type Giveaway { @@ -401,6 +391,18 @@ type Giveaway { users: [User] } +type WorkoutGoalHistory { + id: ID! + + userId: Int! + + workoutGoal: Int! + + effectiveAt: DateTime! + + user: User +} + type Friendship { id: ID! @@ -419,6 +421,13 @@ type Friendship { friend: User } +""" +The `Date` scalar type represents a Date +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar Date + type Workout { id: ID! @@ -427,12 +436,9 @@ type Workout { userId: Int! facilityId: Int! -} -""" -JSON String -""" -scalar JSONString + gymName: String! +} type HourlyAverageCapacity { id: ID! @@ -448,6 +454,22 @@ type HourlyAverageCapacity { history: [Float]! } +enum DayOfWeekGraphQLEnum { + MONDAY + + TUESDAY + + WEDNESDAY + + THURSDAY + + FRIDAY + + SATURDAY + + SUNDAY +} + type CapacityReminder { id: ID! @@ -518,7 +540,7 @@ type Mutation { """ Set a user's workout goals. """ - setWorkoutGoals("The ID of the user." userId: Int!, "The new workout goal for the user in terms of days of the week." workoutGoal: [String]!): User + setWorkoutGoals("The ID of the user." userId: Int!, "The new workout goal for the user in terms of number of days per week." workoutGoal: Int!): User """ Log a user's workout. @@ -545,6 +567,11 @@ type Mutation { """ createReport(createdAt: DateTime!, description: String!, gymId: Int!, issue: String!): CreateReport + """ + Deletes a report by ID. + """ + deleteReport(reportId: Int!): Report + """ Deletes a user by ID. """ diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt index 49c11e93..1a62b3fb 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt @@ -28,6 +28,8 @@ object PreferencesKeys { val GOAL = intPreferencesKey("workoutGoal") val SKIP = booleanPreferencesKey("skip") val FCM_TOKEN = stringPreferencesKey("fcmToken") + val ACCESS_TOKEN = stringPreferencesKey("accessToken") + val REFRESH_TOKEN = stringPreferencesKey("refreshToken") val DECLINED_NOTIFICATION_PERMISSION = booleanPreferencesKey("declinedNotificationPermission") val CAPACITY_REMINDERS_ID = intPreferencesKey("capacityRemindersId") diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index c7762ddd..bb95d061 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -9,6 +9,8 @@ import androidx.datastore.preferences.core.Preferences import com.apollographql.apollo.ApolloClient import com.cornellappdev.uplift.CreateUserMutation import com.cornellappdev.uplift.GetUserByNetIdQuery +import com.cornellappdev.uplift.LoginUserMutation +import com.cornellappdev.uplift.SetWorkoutGoalsMutation import kotlinx.coroutines.flow.map; import kotlinx.coroutines.flow.firstOrNull import com.cornellappdev.uplift.data.models.UserInfo @@ -33,13 +35,45 @@ class UserInfoRepository @Inject constructor( netId = netId, ) ).execute() - storeId(response.data?.createUser?.userFields?.let { storeId(it.id) }.toString()) + val userFields = response.data?.createUser?.userFields + if (userFields == null) { + Log.e("UserInfoRepository", "Server error: ${response.errors}") + return false + } + val loginResponse = apolloClient.mutation( + LoginUserMutation( + netId = netId + ) + ).execute() + val id = userFields.id + val loginData = loginResponse.data?.loginUser + if (loginData?.accessToken == null || loginData.refreshToken == null) { + Log.e("UserInfoRepository", "Login failed after creation: ${loginResponse.errors}") + return false + } + val accessToken = loginData.accessToken + val refreshToken = loginData.refreshToken + storeId(id) storeNetId(netId) storeUsername(name) storeEmail(email) - storeSkip(false) + storeSkip(skip) + storeAccessToken(accessToken) + storeRefreshToken(refreshToken) if (!skip) { storeGoal(goal) + val goalResponse = apolloClient.mutation( + SetWorkoutGoalsMutation( + id = id.toInt(), + workoutGoal = goal + ) + ) + .addHttpHeader("Authorization", "Bearer $accessToken") + .execute() + if (goalResponse.hasErrors()) { + Log.e("UserInfoRepository", "Failed to set goal: ${goalResponse.errors}") + return false + } } Log.d("UserInfoRepositoryImpl", "User created successfully" + response.data) return true @@ -126,6 +160,18 @@ class UserInfoRepository @Inject constructor( } } + private suspend fun storeAccessToken(accessToken: String) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.ACCESS_TOKEN] = accessToken + } + } + + private suspend fun storeRefreshToken(refreshToken: String) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.REFRESH_TOKEN] = refreshToken + } + } + suspend fun getSkipFromDataStore(): Boolean { return dataStore.data.map { preferences -> diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt index e28b00bf..954c1d23 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -40,19 +40,18 @@ import com.cornellappdev.uplift.ui.screens.classes.ClassDetailScreen import com.cornellappdev.uplift.ui.screens.classes.ClassScreen import com.cornellappdev.uplift.ui.screens.gyms.GymDetailScreen import com.cornellappdev.uplift.ui.screens.gyms.HomeScreen -import com.cornellappdev.uplift.ui.screens.onboarding.GoalPromptScreen import com.cornellappdev.uplift.ui.screens.onboarding.ProfileCreationScreen import com.cornellappdev.uplift.ui.screens.onboarding.SignInPromptScreen import com.cornellappdev.uplift.ui.screens.profile.ProfileScreen import com.cornellappdev.uplift.ui.screens.profile.SettingsScreen import com.cornellappdev.uplift.ui.screens.reminders.CapacityReminderScreen import com.cornellappdev.uplift.ui.screens.reminders.MainReminderScreen +import com.cornellappdev.uplift.ui.screens.reminders.WorkoutReminderOnboardingScreen import com.cornellappdev.uplift.ui.screens.report.ReportIssueScreen import com.cornellappdev.uplift.ui.screens.report.ReportSubmittedScreen import com.cornellappdev.uplift.ui.viewmodels.classes.ClassDetailViewModel import com.cornellappdev.uplift.ui.viewmodels.gyms.GymDetailViewModel import com.cornellappdev.uplift.ui.viewmodels.nav.RootNavigationViewModel -import com.cornellappdev.uplift.ui.viewmodels.profile.CheckInUiState import com.cornellappdev.uplift.ui.viewmodels.profile.CheckInViewModel import com.cornellappdev.uplift.util.ONBOARDING_FLAG import com.cornellappdev.uplift.ui.viewmodels.profile.ConfettiViewModel @@ -238,8 +237,8 @@ fun MainNavigationWrapper( composable { ProfileCreationScreen() } - composable { - GoalPromptScreen() + composable { + WorkoutReminderOnboardingScreen() } composable { CapacityReminderScreen() @@ -334,7 +333,7 @@ sealed class UpliftRootRoute { data object Profile : UpliftRootRoute() @Serializable - data object GoalsPrompt : UpliftRootRoute() + data object GoalsOnboarding : UpliftRootRoute() @Serializable diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt index ed2cf7b6..073554bc 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt @@ -58,7 +58,6 @@ fun WorkoutReminderOnboardingScreen( WorkoutReminderScreen( goalValue = goalValue, isOnboarding = true, - // Pass callbacks to update the state defined above onGoalValueChange = { viewModel.updateGoals(it.toInt()) }, onBackClick = { viewModel.onBackClick() }, onNext = { viewModel.onNext() }, diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index c59c28dc..6e557c6a 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -2,7 +2,6 @@ package com.cornellappdev.uplift.ui.viewmodels.onboarding import android.net.Uri import android.util.Log -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import com.cornellappdev.uplift.data.repositories.UserInfoRepository import com.cornellappdev.uplift.ui.UpliftRootRoute @@ -89,7 +88,7 @@ class ProfileCreationViewModel @Inject constructor( fun navigateToGoals() { - rootNavigationRepository.navigate(UpliftRootRoute.GoalsPrompt) + rootNavigationRepository.navigate(UpliftRootRoute.GoalsOnboarding) } private fun navigateToHome() { From 53f80320648314877bd77d9a133a18d60358a4d7 Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Tue, 10 Mar 2026 22:01:07 -0400 Subject: [PATCH 08/14] deleted unused files --- .../screens/onboarding/GoalsPromptScreen.kt | 130 ------------------ .../onboarding/GoalsPromptViewModel.kt | 58 -------- 2 files changed, 188 deletions(-) delete mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt delete mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt deleted file mode 100644 index 6bff3f97..00000000 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalsPromptScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.cornellappdev.uplift.ui.screens.onboarding - -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.fillMaxHeight -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.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -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 androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.cornellappdev.uplift.ui.components.goalsetting.GoalSlider -import com.cornellappdev.uplift.ui.components.onboarding.auth.LogInButton -import com.cornellappdev.uplift.ui.viewmodels.onboarding.GoalsPromptViewModel -import com.cornellappdev.uplift.util.GRAY01 -import com.cornellappdev.uplift.util.montserratFamily - -// Use this as reference but delete later -@Composable -fun GoalPromptScreen( - viewModel: GoalsPromptViewModel = hiltViewModel(), -) { - - val currentGoal by viewModel.goalValue.collectAsStateWithLifecycle() - - GoalPromptContent( - currentGoal, - { viewModel.onGoalValueChange(it) }, - { viewModel.onSkip() }, - { viewModel.onSignInWithGoogle() } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun GoalPromptContent( - currentGoal: Float, - onValueChange: (Float) -> Unit = {}, - onSkip: () -> Unit = {}, - onLogin: () -> Unit = {} -) { - Scaffold( - topBar = { - TopAppBar( - title = { - Box( - modifier = Modifier.fillMaxHeight(), - contentAlignment = Alignment.BottomStart - ) { - Text( - text = "Set your goals.", - fontSize = 24.sp, - fontFamily = montserratFamily, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 16.dp) - ) - } - }, - modifier = Modifier - .height(120.dp) - .shadow(elevation = 20.dp, ambientColor = GRAY01), - colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.White, - ) - ) - }, - modifier = Modifier.fillMaxSize(), - ) { padding -> - Column( - modifier = Modifier - .padding(padding) - .fillMaxSize() - .background(color = Color.White), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - GoalSlider(value = currentGoal, onValueChange = onValueChange) - } - - // Buttons pinned to the bottom - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - LogInButton { onLogin() } - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun GoalPromptScreenPreview() { - var sliderVal by remember { mutableFloatStateOf(0f) } - GoalPromptContent( - sliderVal, - { sliderVal = it} - ) -} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt deleted file mode 100644 index 72865874..00000000 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/GoalsPromptViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.cornellappdev.uplift.ui.viewmodels.onboarding - -import android.net.Uri -import android.util.Log -import androidx.lifecycle.viewModelScope -import com.cornellappdev.uplift.data.repositories.UserInfoRepository -import com.cornellappdev.uplift.ui.UpliftRootRoute -import com.cornellappdev.uplift.ui.nav.RootNavigationRepository -import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -data class GoalsPromptUiState( - val name: String = "", - val imageUri: Uri? = null -) - -@HiltViewModel -class GoalsPromptViewModel @Inject constructor( - private val userInfoRepository: UserInfoRepository, - private val rootNavigationRepository: RootNavigationRepository, -) : UpliftViewModel(GoalsPromptUiState()) { - - private val _goalValue = MutableStateFlow(0f) - val goalValue: StateFlow = _goalValue.asStateFlow() - - init { - viewModelScope.launch { - // TODO: Change this later to reflect correct inputs - val user = userInfoRepository.getFirebaseUser() - val name = user?.displayName ?: "" - applyMutation { - copy(name = name) - } - } - } - - fun onGoalValueChange(newValue: Float) { - _goalValue.value = newValue - } - - // TODO: Change skip and google sign in to reflect correct behavior - fun onSkip() { - navigateToHome() - } - - fun onSignInWithGoogle() { - navigateToHome() - } - - fun navigateToHome() { - rootNavigationRepository.navigate(UpliftRootRoute.Home) - } -} \ No newline at end of file From c761bdc1632f55d9bd9899c7b2fb18781d3fb65a Mon Sep 17 00:00:00 2001 From: Isiah Preston Williams <84547105+isiahpwilliams@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:25:01 -0400 Subject: [PATCH 09/14] Update app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index 6e557c6a..04db84fd 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -75,7 +75,7 @@ class ProfileCreationViewModel @Inject constructor( copy(isGoalSkipped = true) } createUser() - navigateToHome() + } } fun onNext() { From 39fc3a727ebc16d7cefe77170f0feda8d566e07d Mon Sep 17 00:00:00 2001 From: Isiah Preston Williams <84547105+isiahpwilliams@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:27:07 -0400 Subject: [PATCH 10/14] Update app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index 04db84fd..efd812ca 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -61,7 +61,7 @@ class ProfileCreationViewModel @Inject constructor( } fun onBackClick() { - navigateToProfileCreation() + rootNavigationRepository.navigateUp() } fun updateGoals(newGoal: Int) { From 8c6f84dcd5c9ea432f6dd574db5d3e9302aaa568 Mon Sep 17 00:00:00 2001 From: Isiah Preston Williams <84547105+isiahpwilliams@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:43:47 -0400 Subject: [PATCH 11/14] Update app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../uplift/data/repositories/UserInfoRepository.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index bb95d061..917ba172 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -62,9 +62,14 @@ class UserInfoRepository @Inject constructor( storeRefreshToken(refreshToken) if (!skip) { storeGoal(goal) + val numericId = id.toIntOrNull() + if (numericId == null) { + Log.e("UserInfoRepository", "Failed to set goal: non-numeric user ID '$id'") + return false + } val goalResponse = apolloClient.mutation( SetWorkoutGoalsMutation( - id = id.toInt(), + id = numericId, workoutGoal = goal ) ) From 8f7bb3cbd0a2b58882f9661125468ff4cc5654f2 Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Thu, 12 Mar 2026 11:02:36 -0400 Subject: [PATCH 12/14] added token manager and error handling --- app/build.gradle | 3 +- .../uplift/data/repositories/TokenManager.kt | 40 +++++++++++ .../data/repositories/UserInfoRepository.kt | 72 ++++++++++++------- .../reminders/WorkoutReminderScreen.kt | 46 ++++++------ .../onboarding/ProfileCreationViewModel.kt | 11 ++- 5 files changed, 118 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt diff --git a/app/build.gradle b/app/build.gradle index 15e1f056..2604ce37 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,7 +58,7 @@ android { "GOOGLE_AUTH_CLIENT_ID", secretsProperties["GOOGLE_AUTH_CLIENT_ID"] ) signingConfig signingConfigs.debug - buildConfigField("boolean", "ONBOARDING_FLAG", "true") + buildConfigField("boolean", "ONBOARDING_FLAG", "false") buildConfigField("boolean", "CHECK_IN_FLAG", "false") } } @@ -127,6 +127,7 @@ dependencies { implementation 'com.google.firebase:firebase-auth' implementation 'com.google.firebase:firebase-messaging' implementation("com.google.firebase:firebase-analytics") + implementation "androidx.security:security-crypto:1.1.0-alpha06" } apollo { diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt new file mode 100644 index 00000000..2d87cfe5 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt @@ -0,0 +1,40 @@ +package com.cornellappdev.uplift.data.repositories + +import android.content.Context +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import androidx.core.content.edit + +class TokenManager(context: Context) { + private val fileName = "encrypted_tokens" + + // 1. Create or retrieve the Master Key for encryption + private val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + // 2. Initialize EncryptedSharedPreferences + private val sharedPreferences = EncryptedSharedPreferences.create( + context, + fileName, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + fun saveTokens(accessToken: String, refreshToken: String) { + sharedPreferences.edit().apply { + putString("access_token", accessToken) + putString("refresh_token", refreshToken) + apply() + } + } + + fun getAccessToken(): String? = sharedPreferences.getString("access_token", null) + + fun getRefreshToken(): String? = sharedPreferences.getString("refresh_token", null) + + fun clearTokens() { + sharedPreferences.edit { clear() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index bb95d061..44c1ad97 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -24,6 +24,7 @@ class UserInfoRepository @Inject constructor( private val firebaseAuth: FirebaseAuth, private val apolloClient: ApolloClient, private val dataStore: DataStore, + private val tokenManager: TokenManager ){ suspend fun createUser(email: String, name: String, netId: String, skip: Boolean, goal: Int): Boolean { @@ -36,7 +37,7 @@ class UserInfoRepository @Inject constructor( ) ).execute() val userFields = response.data?.createUser?.userFields - if (userFields == null) { + if (response.hasErrors() || userFields == null) { Log.e("UserInfoRepository", "Server error: ${response.errors}") return false } @@ -53,29 +54,13 @@ class UserInfoRepository @Inject constructor( } val accessToken = loginData.accessToken val refreshToken = loginData.refreshToken - storeId(id) - storeNetId(netId) - storeUsername(name) - storeEmail(email) - storeSkip(skip) - storeAccessToken(accessToken) - storeRefreshToken(refreshToken) + tokenManager.saveTokens(accessToken, refreshToken) if (!skip) { - storeGoal(goal) - val goalResponse = apolloClient.mutation( - SetWorkoutGoalsMutation( - id = id.toInt(), - workoutGoal = goal - ) - ) - .addHttpHeader("Authorization", "Bearer $accessToken") - .execute() - if (goalResponse.hasErrors()) { - Log.e("UserInfoRepository", "Failed to set goal: ${goalResponse.errors}") - return false - } + val numericId = id.toIntOrNull() + uploadGoal(numericId, goal) } - Log.d("UserInfoRepositoryImpl", "User created successfully" + response.data) + storeUserFields(id, name, netId, email, skip, accessToken, refreshToken, goal) + Log.d("UserInfoRepositoryImpl", "User created successfully") return true } catch (e: Exception) { Log.e("UserInfoRepositoryImpl", "Error creating user: $e") @@ -83,14 +68,51 @@ class UserInfoRepository @Inject constructor( } } + suspend fun uploadGoal(id:Int?, goal: Int): Boolean { + if (id == null) { + Log.e("UserInfoRepository", "Failed to set goal: non-numeric user ID '$id'") + return false + } + val goalResponse = apolloClient.mutation( + SetWorkoutGoalsMutation( + id = id, + workoutGoal = goal + ) + ) + // Change so that it uses auth interceptor + .addHttpHeader("Authorization", "Bearer ${tokenManager.getAccessToken()}") + .execute() + if (goalResponse.hasErrors()) { + Log.e("UserInfoRepository", "Failed to set goal: ${goalResponse.errors}") + return false + } + return true + } + + + suspend fun storeUserFields(id: String, username: String, netId: String, email: String, skip: Boolean, accessToken: String, refreshToken: String, goal: Int) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.ID] = id + preferences[PreferencesKeys.NETID] = netId + preferences[PreferencesKeys.USERNAME] = username + preferences[PreferencesKeys.EMAIL] = email + preferences[PreferencesKeys.SKIP] = skip + preferences[PreferencesKeys.ACCESS_TOKEN] = accessToken + preferences[PreferencesKeys.REFRESH_TOKEN] = refreshToken + if (!skip) { + preferences[PreferencesKeys.GOAL] = goal + } + } + } + suspend fun getUserByNetId(netId: String): UserInfo? { try { val response = apolloClient.query( GetUserByNetIdQuery( netId = netId ) - ).executeV3() - val user = response.data?.getUserByNetId?.get(0)?.userFields ?: return null + ).execute() + val user = response.data?.getUserByNetId?.firstOrNull()?.userFields ?: return null return UserInfo( id = user.id, name = user.name, @@ -107,7 +129,7 @@ class UserInfoRepository @Inject constructor( return firebaseAuth.currentUser != null } - suspend fun getFirebaseUser(): FirebaseUser? { + fun getFirebaseUser(): FirebaseUser? { return firebaseAuth.currentUser } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt index 073554bc..5e0c5204 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.padding @@ -112,7 +111,7 @@ fun WorkoutReminderScreen( selectedReminder = selectedReminder, addNewReminderState = addNewReminderState, deleteDialogOpen = deleteDialogOpen, - goalValue = goalValue.toInt(), + goalValue = goalValue, isOnboarding = isOnboarding, // Pass callbacks to update the state defined above onSelectedReminderChange = { selectedReminder = it }, @@ -133,7 +132,7 @@ private fun WorkoutReminderContent( selectedReminder: Reminder?, addNewReminderState: Boolean, deleteDialogOpen: Boolean, - goalValue: Int, + goalValue: Float, isOnboarding: Boolean, onSelectedReminderChange: (Reminder?) -> Unit, onAddNewReminderStateChange: (Boolean) -> Unit, @@ -152,35 +151,32 @@ private fun WorkoutReminderContent( withBack = true ) }, + bottomBar = { + if (isOnboarding) { + OnboardingButtons(onNext, onSkip) + } + }, modifier = Modifier.fillMaxSize(), ) { padding -> Column( modifier = Modifier .background(color = Color.White) - .padding(top = padding.calculateTopPadding()) + .padding(padding) .fillMaxSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween + .verticalScroll(rememberScrollState()) ) { - Column(modifier = Modifier.fillMaxSize()) { - GoalSlider(value = goalValue.toFloat(), onValueChange = onGoalValueChange) + GoalSlider(value = goalValue, onValueChange = onGoalValueChange) - if (!isOnboarding) { - WorkoutReminders( - selectedReminder = selectedReminder, - onSelectedReminderChange = onSelectedReminderChange, - reminders = reminders, - onRemindersChange = onRemindersChange, - addNewReminderState = addNewReminderState, - onAddNewReminderStateChange = onAddNewReminderStateChange, - openDelete = { onDeleteDialogOpenChange(true) } - ) - } - } - - Spacer(modifier = Modifier.weight(1f)) - if (isOnboarding) { - OnboardingButtons(onNext, onSkip) + if (!isOnboarding) { + WorkoutReminders( + selectedReminder = selectedReminder, + onSelectedReminderChange = onSelectedReminderChange, + reminders = reminders, + onRemindersChange = onRemindersChange, + addNewReminderState = addNewReminderState, + onAddNewReminderStateChange = onAddNewReminderStateChange, + openDelete = { onDeleteDialogOpenChange(true) } + ) } } @@ -263,4 +259,4 @@ fun WorkoutReminderScreenPreview() { onGoalValueChange = { sliderVal = it }, onBackClick = {}, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index efd812ca..4d619f45 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -37,8 +37,14 @@ class ProfileCreationViewModel @Inject constructor( private fun createUser() = viewModelScope.launch { val user = userInfoRepository.getFirebaseUser() val name = user?.displayName ?: "" - val email = user?.email ?: "" - val netId = email.substring(0, email.indexOf('@')) + val email = user?.email + if (email.isNullOrBlank()) { + Log.e("Error", "Cannot create user: missing or blank email") + userInfoRepository.signOut() + return@launch + } + + val netId = email.substringBefore("@") val isSkipped = getStateValue().isGoalSkipped var goal = 0 if (!isSkipped) { @@ -76,7 +82,6 @@ class ProfileCreationViewModel @Inject constructor( } createUser() } - } fun onNext() { createUser() From 99d0c93b2d9b4ba51a7d157ea33f7fde18e4f45e Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Thu, 12 Mar 2026 12:45:18 -0400 Subject: [PATCH 13/14] added auth interceptor and token manager --- .../data/repositories/AuthInterceptor.kt | 21 +++++++++ .../uplift/data/repositories/TokenManager.kt | 45 +++++++++++++------ .../data/repositories/UserInfoRepository.kt | 11 +++-- .../com/cornellappdev/uplift/di/AppModule.kt | 16 ++++++- .../uplift/ui/MainNavigationWrapper.kt | 2 +- .../onboarding/GoalOnboardingScreen.kt | 25 +++++++++++ .../reminders/WorkoutReminderScreen.kt | 17 ------- .../onboarding/ProfileCreationViewModel.kt | 18 ++++---- 8 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt create mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalOnboardingScreen.kt diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt new file mode 100644 index 00000000..cbdbdff5 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt @@ -0,0 +1,21 @@ +package com.cornellappdev.uplift.data.repositories + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AuthInterceptor @Inject constructor( + private val tokenManager: TokenManager +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val token = tokenManager.getAccessToken() + val request = chain.request().newBuilder().apply { + if (token != null) { + addHeader("Authorization", "Bearer $token") + } + }.build() + return chain.proceed(request) + } +} diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt index 2d87cfe5..0ffa5896 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt @@ -1,26 +1,43 @@ package com.cornellappdev.uplift.data.repositories import android.content.Context +import android.content.SharedPreferences +import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import androidx.core.content.edit +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton -class TokenManager(context: Context) { +@Singleton +class TokenManager @Inject constructor(@ApplicationContext private val context: Context) { private val fileName = "encrypted_tokens" - // 1. Create or retrieve the Master Key for encryption - private val masterKey = MasterKey.Builder(context) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() + // Initialize EncryptedSharedPreferences + private val sharedPreferences: SharedPreferences by lazy { + try { + createEncryptedPrefs() + } catch (e: Exception) { + Log.e("TokenManager", "Failed to initialize EncryptedSharedPreferences", e) + context.deleteSharedPreferences(fileName) + context.getSharedPreferences(fileName, Context.MODE_PRIVATE) + } + } - // 2. Initialize EncryptedSharedPreferences - private val sharedPreferences = EncryptedSharedPreferences.create( - context, - fileName, - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) + private fun createEncryptedPrefs(): SharedPreferences { + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + return EncryptedSharedPreferences.create( + context, + fileName, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } fun saveTokens(accessToken: String, refreshToken: String) { sharedPreferences.edit().apply { @@ -37,4 +54,4 @@ class TokenManager(context: Context) { fun clearTokens() { sharedPreferences.edit { clear() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index 44c1ad97..b0b7bd1a 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.Preferences import com.apollographql.apollo.ApolloClient import com.cornellappdev.uplift.CreateUserMutation +import com.cornellappdev.uplift.DeleteUserMutation import com.cornellappdev.uplift.GetUserByNetIdQuery import com.cornellappdev.uplift.LoginUserMutation import com.cornellappdev.uplift.SetWorkoutGoalsMutation @@ -57,7 +58,12 @@ class UserInfoRepository @Inject constructor( tokenManager.saveTokens(accessToken, refreshToken) if (!skip) { val numericId = id.toIntOrNull() - uploadGoal(numericId, goal) + if (!uploadGoal(numericId, goal)) { + return false + } + } + else { + Log.d("UserInfoRepository", "Skipping goal upload") } storeUserFields(id, name, netId, email, skip, accessToken, refreshToken, goal) Log.d("UserInfoRepositoryImpl", "User created successfully") @@ -79,13 +85,12 @@ class UserInfoRepository @Inject constructor( workoutGoal = goal ) ) - // Change so that it uses auth interceptor - .addHttpHeader("Authorization", "Bearer ${tokenManager.getAccessToken()}") .execute() if (goalResponse.hasErrors()) { Log.e("UserInfoRepository", "Failed to set goal: ${goalResponse.errors}") return false } + Log.d("UserInfoRepository", "Goal set successfully: $goal") return true } diff --git a/app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt b/app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt index 521a0717..bbd77c6d 100644 --- a/app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt +++ b/app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt @@ -1,11 +1,14 @@ package com.cornellappdev.uplift.di import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.network.okHttpClient import com.cornellappdev.uplift.BuildConfig +import com.cornellappdev.uplift.data.repositories.AuthInterceptor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient import javax.inject.Singleton @@ -15,10 +18,19 @@ object AppModule { @Provides @Singleton - fun provideApolloClient(): ApolloClient { + fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .build() + } + + @Provides + @Singleton + fun provideApolloClient(okHttpClient: OkHttpClient): ApolloClient { return ApolloClient.Builder() .serverUrl(BuildConfig.BACKEND_URL) + .okHttpClient(okHttpClient) .build() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt index 954c1d23..5373026c 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -46,7 +46,7 @@ import com.cornellappdev.uplift.ui.screens.profile.ProfileScreen import com.cornellappdev.uplift.ui.screens.profile.SettingsScreen import com.cornellappdev.uplift.ui.screens.reminders.CapacityReminderScreen import com.cornellappdev.uplift.ui.screens.reminders.MainReminderScreen -import com.cornellappdev.uplift.ui.screens.reminders.WorkoutReminderOnboardingScreen +import com.cornellappdev.uplift.ui.screens.onboarding.WorkoutReminderOnboardingScreen import com.cornellappdev.uplift.ui.screens.report.ReportIssueScreen import com.cornellappdev.uplift.ui.screens.report.ReportSubmittedScreen import com.cornellappdev.uplift.ui.viewmodels.classes.ClassDetailViewModel diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalOnboardingScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalOnboardingScreen.kt new file mode 100644 index 00000000..216838fe --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/onboarding/GoalOnboardingScreen.kt @@ -0,0 +1,25 @@ +package com.cornellappdev.uplift.ui.screens.onboarding + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.cornellappdev.uplift.ui.screens.reminders.WorkoutReminderScreen +import com.cornellappdev.uplift.ui.viewmodels.onboarding.ProfileCreationViewModel + +@Composable +fun WorkoutReminderOnboardingScreen( + viewModel: ProfileCreationViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle() + val goalValue = uiState.goal + + WorkoutReminderScreen( + goalValue = goalValue, + isOnboarding = true, + onGoalValueChange = { viewModel.updateGoals(it) }, + onBackClick = { viewModel.onBackClick() }, + onNext = { viewModel.onNext() }, + onSkip = { viewModel.onSkip() }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt index 5e0c5204..5add1b08 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt @@ -47,23 +47,6 @@ data class Reminder( val enabled: Boolean ) -@Composable -fun WorkoutReminderOnboardingScreen( - viewModel: ProfileCreationViewModel = hiltViewModel(), -) { - val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle() - val goalValue = uiState.goal - - WorkoutReminderScreen( - goalValue = goalValue, - isOnboarding = true, - onGoalValueChange = { viewModel.updateGoals(it.toInt()) }, - onBackClick = { viewModel.onBackClick() }, - onNext = { viewModel.onNext() }, - onSkip = { viewModel.onSkip() }, - ) -} - @Composable fun WorkoutReminderSettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index 4d619f45..90ed5123 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -7,11 +7,13 @@ import com.cornellappdev.uplift.data.repositories.UserInfoRepository import com.cornellappdev.uplift.ui.UpliftRootRoute import com.cornellappdev.uplift.ui.nav.RootNavigationRepository import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel +import com.google.firebase.auth.FirebaseUser import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject data class ProfileCreationUiState( + val user: FirebaseUser? = null, val name: String = "", val imageUri: Uri? = null, val isGoalSkipped: Boolean = false, @@ -29,13 +31,14 @@ class ProfileCreationViewModel @Inject constructor( val user = userInfoRepository.getFirebaseUser() val name = user?.displayName ?: "" applyMutation { - copy(name = name) + copy(user = user, name = name) } } } private fun createUser() = viewModelScope.launch { - val user = userInfoRepository.getFirebaseUser() + val state = getStateValue() + val user = state.user val name = user?.displayName ?: "" val email = user?.email if (email.isNullOrBlank()) { @@ -45,11 +48,8 @@ class ProfileCreationViewModel @Inject constructor( } val netId = email.substringBefore("@") - val isSkipped = getStateValue().isGoalSkipped - var goal = 0 - if (!isSkipped) { - goal = getStateValue().goal.toInt() - } + val isSkipped = state.isGoalSkipped + val goal = if (isSkipped) 0 else state.goal.toInt() if (userInfoRepository.createUser(email, name, netId, isSkipped, goal)) { navigateToHome() } else { @@ -70,9 +70,9 @@ class ProfileCreationViewModel @Inject constructor( rootNavigationRepository.navigateUp() } - fun updateGoals(newGoal: Int) { + fun updateGoals(newGoal: Float) { applyMutation { - copy(goal = newGoal.toFloat()) + copy(goal = newGoal) } } From b6937eb891392020341006e6c6d9fd5da745f4fb Mon Sep 17 00:00:00 2001 From: Preston Williams Date: Thu, 12 Mar 2026 18:45:31 -0400 Subject: [PATCH 14/14] fixed copilot suggestions --- .../data/repositories/DatastoreRepository.kt | 2 -- .../uplift/data/repositories/TokenManager.kt | 22 +++++++++++++------ .../data/repositories/UserInfoRepository.kt | 19 ++-------------- .../reminders/WorkoutReminderScreen.kt | 3 +-- .../onboarding/ProfileCreationViewModel.kt | 17 ++++++-------- 5 files changed, 25 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt index 1a62b3fb..49c11e93 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt @@ -28,8 +28,6 @@ object PreferencesKeys { val GOAL = intPreferencesKey("workoutGoal") val SKIP = booleanPreferencesKey("skip") val FCM_TOKEN = stringPreferencesKey("fcmToken") - val ACCESS_TOKEN = stringPreferencesKey("accessToken") - val REFRESH_TOKEN = stringPreferencesKey("refreshToken") val DECLINED_NOTIFICATION_PERMISSION = booleanPreferencesKey("declinedNotificationPermission") val CAPACITY_REMINDERS_ID = intPreferencesKey("capacityRemindersId") diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt index 0ffa5896..3c387aec 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt @@ -15,13 +15,22 @@ class TokenManager @Inject constructor(@ApplicationContext private val context: private val fileName = "encrypted_tokens" // Initialize EncryptedSharedPreferences - private val sharedPreferences: SharedPreferences by lazy { + private val sharedPreferences: SharedPreferences? by lazy { try { createEncryptedPrefs() } catch (e: Exception) { Log.e("TokenManager", "Failed to initialize EncryptedSharedPreferences", e) + // Clear corrupted state context.deleteSharedPreferences(fileName) - context.getSharedPreferences(fileName, Context.MODE_PRIVATE) + try { + // Could have failed due to previous corrupted state + // One more attempt after cleaning the corruption + createEncryptedPrefs() + } catch (retryException: Exception) { + // Probably broken return null + Log.e("TokenManager", "Failed to initialize EncryptedSharedPreferences again", retryException) + null + } } } @@ -40,18 +49,17 @@ class TokenManager @Inject constructor(@ApplicationContext private val context: } fun saveTokens(accessToken: String, refreshToken: String) { - sharedPreferences.edit().apply { + sharedPreferences?.edit { putString("access_token", accessToken) putString("refresh_token", refreshToken) - apply() } } - fun getAccessToken(): String? = sharedPreferences.getString("access_token", null) + fun getAccessToken(): String? = sharedPreferences?.getString("access_token", null) - fun getRefreshToken(): String? = sharedPreferences.getString("refresh_token", null) + fun getRefreshToken(): String? = sharedPreferences?.getString("refresh_token", null) fun clearTokens() { - sharedPreferences.edit { clear() } + sharedPreferences?.edit { clear() } } } diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index b0b7bd1a..b674864d 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -8,7 +8,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.Preferences import com.apollographql.apollo.ApolloClient import com.cornellappdev.uplift.CreateUserMutation -import com.cornellappdev.uplift.DeleteUserMutation import com.cornellappdev.uplift.GetUserByNetIdQuery import com.cornellappdev.uplift.LoginUserMutation import com.cornellappdev.uplift.SetWorkoutGoalsMutation @@ -65,7 +64,7 @@ class UserInfoRepository @Inject constructor( else { Log.d("UserInfoRepository", "Skipping goal upload") } - storeUserFields(id, name, netId, email, skip, accessToken, refreshToken, goal) + storeUserFields(id, name, netId, email, skip, goal) Log.d("UserInfoRepositoryImpl", "User created successfully") return true } catch (e: Exception) { @@ -95,15 +94,13 @@ class UserInfoRepository @Inject constructor( } - suspend fun storeUserFields(id: String, username: String, netId: String, email: String, skip: Boolean, accessToken: String, refreshToken: String, goal: Int) { + suspend fun storeUserFields(id: String, username: String, netId: String, email: String, skip: Boolean, goal: Int) { dataStore.edit { preferences -> preferences[PreferencesKeys.ID] = id preferences[PreferencesKeys.NETID] = netId preferences[PreferencesKeys.USERNAME] = username preferences[PreferencesKeys.EMAIL] = email preferences[PreferencesKeys.SKIP] = skip - preferences[PreferencesKeys.ACCESS_TOKEN] = accessToken - preferences[PreferencesKeys.REFRESH_TOKEN] = refreshToken if (!skip) { preferences[PreferencesKeys.GOAL] = goal } @@ -187,18 +184,6 @@ class UserInfoRepository @Inject constructor( } } - private suspend fun storeAccessToken(accessToken: String) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.ACCESS_TOKEN] = accessToken - } - } - - private suspend fun storeRefreshToken(refreshToken: String) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.REFRESH_TOKEN] = refreshToken - } - } - suspend fun getSkipFromDataStore(): Boolean { return dataStore.data.map { preferences -> diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt index 5add1b08..b8e0cbf2 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/reminders/WorkoutReminderScreen.kt @@ -24,13 +24,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.cornellappdev.uplift.ui.components.general.UpliftTopBarWithBack import com.cornellappdev.uplift.ui.components.goalsetting.DeleteDialog import com.cornellappdev.uplift.ui.components.goalsetting.GoalSlider import com.cornellappdev.uplift.ui.components.goalsetting.WorkoutReminders import com.cornellappdev.uplift.ui.components.general.UpliftButton -import com.cornellappdev.uplift.ui.viewmodels.onboarding.ProfileCreationViewModel import com.cornellappdev.uplift.ui.viewmodels.profile.SettingsViewModel import com.cornellappdev.uplift.util.GRAY04 import com.cornellappdev.uplift.util.montserratFamily @@ -63,6 +61,7 @@ fun WorkoutReminderSettingsScreen( /** * @param reminders: list of reminders + * @param goalValue: the value of the goal slider * @param onRemindersChange: callback for when reminders are changed * @param onGoalValueChange: callback for when the goal slider value is changed * @param onBackClick: callback for when the back button is clicked diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt index 90ed5123..9331c5d9 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt @@ -24,17 +24,14 @@ data class ProfileCreationUiState( class ProfileCreationViewModel @Inject constructor( private val userInfoRepository: UserInfoRepository, private val rootNavigationRepository: RootNavigationRepository, -) : UpliftViewModel(ProfileCreationUiState()) { - - init { - viewModelScope.launch { - val user = userInfoRepository.getFirebaseUser() - val name = user?.displayName ?: "" - applyMutation { - copy(user = user, name = name) - } - } +) : UpliftViewModel( + userInfoRepository.getFirebaseUser().let { user -> + ProfileCreationUiState( + user = user, + name = user?.displayName ?: "" + ) } +) { private fun createUser() = viewModelScope.launch { val state = getStateValue()