diff --git a/app/build.gradle b/app/build.gradle index fdc87cd..885363a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,8 +58,8 @@ android { "GOOGLE_AUTH_CLIENT_ID", secretsProperties["GOOGLE_AUTH_CLIENT_ID"] ) signingConfig signingConfigs.debug - buildConfigField("boolean", "ONBOARDING_FLAG", "true") - buildConfigField("boolean", "CHECK_IN_FLAG", "true") + buildConfigField("boolean", "ONBOARDING_FLAG", "false") + buildConfigField("boolean", "CHECK_IN_FLAG", "false") } } compileOptions { diff --git a/app/src/main/graphql/User.graphql b/app/src/main/graphql/User.graphql index ec6f3b0..933e6fd 100644 --- a/app/src/main/graphql/User.graphql +++ b/app/src/main/graphql/User.graphql @@ -70,3 +70,15 @@ mutation LoginUser($netId: String!) { refreshToken } } + +mutation LogoutUser { + logoutUser { + success + } +} + +mutation RefreshAccessToken { + refreshAccessToken { + newAccessToken + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt b/app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt similarity index 72% rename from app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt rename to app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt index a0bd4ad..9579636 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt @@ -1,5 +1,6 @@ -package com.cornellappdev.uplift.data.repositories +package com.cornellappdev.uplift.data.auth +import android.util.Log import okhttp3.Interceptor import okhttp3.Response import javax.inject.Inject @@ -11,12 +12,12 @@ class AuthInterceptor @Inject constructor( ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val token = tokenManager.getAccessToken() - android.util.Log.d("AuthInterceptor", "token present = ${token != null}") + Log.d("AuthInterceptor", "token present = ${token != null}") val request = chain.request().newBuilder().apply { if (token != null) { - addHeader("Authorization", "Bearer $token") + header("Authorization", "Bearer $token") } }.build() return chain.proceed(request) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt b/app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt new file mode 100644 index 0000000..f48191c --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt @@ -0,0 +1,38 @@ +package com.cornellappdev.uplift.data.auth + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject +import javax.inject.Singleton +import com.cornellappdev.uplift.di.AppModule.ApplicationScope + +@Singleton +class SessionManager @Inject constructor( + private val tokenManager: TokenManager, + @ApplicationScope private val upliftScope: CoroutineScope +) { + // A reactive flow that the UI can collect + val isLoggedIn: StateFlow = tokenManager.tokenFlow + .map { token -> token != null } + .stateIn( + scope = upliftScope, + started = SharingStarted.Eagerly, + initialValue = tokenManager.getAccessToken() != null + ) + + // Call this after LoginUser or CreateUser mutations succeed + fun startSession(userId: Int, name: String, email: String, access: String, refresh: String) { + tokenManager.saveTokens(access, refresh) + tokenManager.saveUserSession(userId, name, email) + } + + // Call this for manual logout or when refresh fails + fun logout() { + tokenManager.clearTokens() + } + + val userId: Int? get() = tokenManager.getUserId() +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt b/app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt new file mode 100644 index 0000000..634a292 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt @@ -0,0 +1,88 @@ +package com.cornellappdev.uplift.data.auth + +import android.util.Log +import com.apollographql.apollo.ApolloClient +import com.cornellappdev.uplift.RefreshAccessTokenMutation +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class TokenAuthenticator @Inject constructor( + private val tokenManager: TokenManager, + private val sessionManager: SessionManager, + @Named("refresh") private val apolloClient: ApolloClient +) : Authenticator { + + override fun authenticate(route: Route?, response: Response): Request? { + if (responseCount(response) >= 2) { + return null + } + + val refreshToken = tokenManager.getRefreshToken() ?: return null + + synchronized(this) { + // Check if the token was already refreshed by another thread + // while this request was waiting for the lock. + val currentToken = tokenManager.getAccessToken() + val requestToken = response.request.header("Authorization")?.substringAfter("Bearer ") + + if (currentToken != null && currentToken != requestToken ) { + return response.request.newBuilder() + .header("Authorization", "Bearer $currentToken") + .build() + } + + // 3. Since OkHttp's Authenticator is synchronous but Apollo is suspend-based, + // we use runBlocking to wait for the refresh mutation result. + return runBlocking { + try { + val mutationResponse = withTimeout(10000L) { + apolloClient.mutation(RefreshAccessTokenMutation()) + // We manually add the Refresh Token to this specific call + // because the "refresh" ApolloClient has no interceptor. + .addHttpHeader("Authorization", "Bearer $refreshToken") + .execute() + } + + val newAccessToken = mutationResponse.data?.refreshAccessToken?.newAccessToken + + if (newAccessToken != null && newAccessToken != requestToken) { + tokenManager.saveTokens(newAccessToken, refreshToken) + + // Retry the original request with the new Access Token + response.request.newBuilder() + .header("Authorization", "Bearer $newAccessToken") + .build() + } else { + // Refresh failed (e.g., refresh token expired on backend) + sessionManager.logout() + null + } + } catch (e: Exception) { + // Network error or server down during refresh + Log.e("TokenAuthenticator", "Refresh timed out or failed", e) + sessionManager.logout() + null + } + } + } + } + + private fun responseCount(response: Response?): Int { + var result = 1 + var current = response + // Traverse the chain of prior responses + while (current?.priorResponse != null) { + result++ + current = current.priorResponse + } + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt b/app/src/main/java/com/cornellappdev/uplift/data/auth/TokenManager.kt similarity index 76% rename from app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt rename to app/src/main/java/com/cornellappdev/uplift/data/auth/TokenManager.kt index 3c387ae..3f5e099 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/auth/TokenManager.kt @@ -1,12 +1,14 @@ -package com.cornellappdev.uplift.data.repositories +package com.cornellappdev.uplift.data.auth import android.content.Context import android.content.SharedPreferences import android.util.Log +import androidx.core.content.edit import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey -import androidx.core.content.edit import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject import javax.inject.Singleton @@ -34,6 +36,9 @@ class TokenManager @Inject constructor(@ApplicationContext private val context: } } + private val _tokenFlow = MutableStateFlow(getAccessToken()) + val tokenFlow = _tokenFlow.asStateFlow() + private fun createEncryptedPrefs(): SharedPreferences { val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) @@ -53,6 +58,7 @@ class TokenManager @Inject constructor(@ApplicationContext private val context: putString("access_token", accessToken) putString("refresh_token", refreshToken) } + _tokenFlow.value = accessToken } fun getAccessToken(): String? = sharedPreferences?.getString("access_token", null) @@ -61,5 +67,17 @@ class TokenManager @Inject constructor(@ApplicationContext private val context: fun clearTokens() { sharedPreferences?.edit { clear() } + _tokenFlow.value = null } + + fun saveUserSession(userId: Int, username: String, userEmail: String) { + sharedPreferences?.edit { + putInt("user_id", userId) + putString("username", username) + putString("user_email", userEmail) + } + } + + fun getUserId(): Int? = sharedPreferences?.takeIf { it.contains("user_id") }?.getInt("user_id", -1) + } diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CapacityRemindersRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CapacityRemindersRepository.kt index ab12e67..2593509 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CapacityRemindersRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CapacityRemindersRepository.kt @@ -7,12 +7,13 @@ import com.cornellappdev.uplift.DeleteCapacityReminderMutation import com.cornellappdev.uplift.EditCapacityReminderMutation import com.cornellappdev.uplift.data.mappers.toResult import javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton @Singleton class CapacityRemindersRepository @Inject constructor( - private val apolloClient: ApolloClient, + @Named("main") private val apolloClient: ApolloClient, private val dataStoreRepository: DatastoreRepository, ) { /** diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt index eb31236..c0f212a 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt @@ -30,13 +30,14 @@ import com.apollographql.apollo.ApolloClient import com.cornellappdev.uplift.LogWorkoutMutation import java.time.Instant import javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton @Singleton class CheckInRepository @Inject constructor( val upliftApiRepository: UpliftApiRepository, private val dataStore: DataStore, - private val apolloClient: ApolloClient, + @Named("main") private val apolloClient: ApolloClient, private val userInfoRepository: UserInfoRepository ){ private val _nearestGymFlow = MutableStateFlow(null) diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/PopularTimesRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/PopularTimesRepository.kt index cf5e11d..012ab8e 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/PopularTimesRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/PopularTimesRepository.kt @@ -4,11 +4,12 @@ import com.apollographql.apollo.ApolloClient import com.cornellappdev.uplift.PopularTimesQuery import com.cornellappdev.uplift.data.mappers.toResult import javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton @Singleton class PopularTimesRepository @Inject constructor( - private val apolloClient: ApolloClient + @Named("main") private val apolloClient: ApolloClient ) { suspend fun getPopularTimes(facilityId: Int): Result { return apolloClient.query( diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt index 52250f1..53a60e2 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt @@ -9,13 +9,14 @@ import com.cornellappdev.uplift.data.models.ProfileData import com.cornellappdev.uplift.data.models.WorkoutDomain import java.time.Instant import javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton @Singleton class ProfileRepository @Inject constructor( private val userInfoRepository: UserInfoRepository, - private val apolloClient: ApolloClient + @Named("main") private val apolloClient: ApolloClient ) { suspend fun getProfile(): Result = runCatching { val netId = userInfoRepository.getNetIdFromDataStore() diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ReportRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ReportRepository.kt index d1bd297..9130ca7 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ReportRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ReportRepository.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo.ApolloClient import com.cornellappdev.uplift.CreateReportMutation import com.cornellappdev.uplift.data.mappers.toResult import javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton /** @@ -12,7 +13,7 @@ import javax.inject.Singleton */ @Singleton class ReportRepository @Inject constructor( - private val apolloClient: ApolloClient + @Named("main") private val apolloClient: ApolloClient ) { /** * @param createdAt The time the report was created. diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UpliftApiRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UpliftApiRepository.kt index c8fa9ab..2ddeafe 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UpliftApiRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UpliftApiRepository.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton +import javax.inject.Named /** * A repository dealing with all API backend connection in Uplift. @@ -31,7 +32,7 @@ import javax.inject.Singleton */ @Singleton class UpliftApiRepository @Inject constructor( - private val apolloClient: ApolloClient + @Named("main") private val apolloClient: ApolloClient ) { private val gymQuery = apolloClient.query(GymListQuery()) private val classQuery = apolloClient.query(ClassListQuery()) 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 10c510a..8a6e663 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 @@ -11,21 +11,22 @@ import com.cornellappdev.uplift.CreateUserMutation import com.cornellappdev.uplift.GetUserByNetIdQuery import com.cornellappdev.uplift.LoginUserMutation import com.cornellappdev.uplift.SetWorkoutGoalsMutation +import com.cornellappdev.uplift.data.auth.SessionManager import kotlinx.coroutines.flow.map; import kotlinx.coroutines.flow.firstOrNull import com.cornellappdev.uplift.data.models.UserInfo -import com.google.common.collect.Iterables.skip import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider import kotlinx.coroutines.tasks.await +import javax.inject.Named @Singleton class UserInfoRepository @Inject constructor( private val firebaseAuth: FirebaseAuth, - private val apolloClient: ApolloClient, + @Named("main") private val apolloClient: ApolloClient, private val dataStore: DataStore, - private val tokenManager: TokenManager + private val sessionManager: SessionManager ){ suspend fun createUser(email: String, name: String, netId: String, skip: Boolean, goal: Int): Boolean { @@ -47,7 +48,11 @@ class UserInfoRepository @Inject constructor( netId = netId ) ).execute() - val id = userFields.id + val id = userFields.id.toIntOrNull() + if (id == null) { + Log.e("UserInfoRepository", "Failed to set goal: non-numeric user ID '${userFields.id}'") + return false + } val loginData = loginResponse.data?.loginUser if (loginData?.accessToken == null || loginData.refreshToken == null) { Log.e("UserInfoRepository", "Login failed after creation: ${loginResponse.errors}") @@ -55,10 +60,8 @@ class UserInfoRepository @Inject constructor( } val accessToken = loginData.accessToken val refreshToken = loginData.refreshToken - tokenManager.saveTokens(accessToken, refreshToken) if (!skip) { - val numericId = id.toIntOrNull() - if (!uploadGoal(numericId, goal)) { + if (!uploadGoal(id, goal, accessToken)) { return false } } @@ -73,6 +76,13 @@ class UserInfoRepository @Inject constructor( goalSkip = skip, goal = goal ) + sessionManager.startSession( + userId = id, + name = name, + email = email, + access = accessToken, + refresh = refreshToken + ) Log.d("UserInfoRepositoryImpl", "User created successfully") return true } catch (e: Exception) { @@ -81,18 +91,54 @@ class UserInfoRepository @Inject constructor( } } - suspend fun uploadGoal(id:Int?, goal: Int): Boolean { + suspend fun loginUser(netId: String) : Boolean { + return try { + val loginResponse = apolloClient.mutation( + LoginUserMutation( + netId = netId + ) + ).execute() + val loginData = loginResponse.data?.loginUser + val userInfo = getUserByNetId(netId) + if (loginData?.accessToken != null && loginData.refreshToken != null && userInfo != null) { + val id = userInfo.id.toIntOrNull() + if (id == null) { + Log.e("UserInfoRepository", "Failed to log in: non-numeric user ID resulting in null '${userInfo.id}'") + return false + } + sessionManager.startSession( + userId = id, + name = userInfo.name, + email = userInfo.email, + access = loginData.accessToken, + refresh = loginData.refreshToken + ) + true + } else { + Log.e("UserInfoRepository", "Login failed: Missing tokens or user info; ${loginResponse.errors}") + false + } + } catch (e: Exception) { + Log.e("UserInfoRepository", "Error logging in: $e") + false + } + } + + suspend fun uploadGoal(id:Int?, goal: Int, manualToken: String? = null): Boolean { if (id == null) { Log.e("UserInfoRepository", "Failed to set goal: non-numeric user ID '$id'") return false } - val goalResponse = apolloClient.mutation( + val call = apolloClient.mutation( SetWorkoutGoalsMutation( userId = id, workoutGoal = goal ) ) - .execute() + if (manualToken != null) { + call.addHttpHeader("Authorization", "Bearer $manualToken") + } + val goalResponse = call.execute() if (goalResponse.hasErrors()) { Log.e("UserInfoRepository", "Failed to set goal: ${goalResponse.errors}") return false @@ -115,55 +161,6 @@ class UserInfoRepository @Inject constructor( } } - suspend fun loginAndStoreTokens(netId: String): Boolean { - return try { - val loginResponse = apolloClient.mutation( - LoginUserMutation(netId = netId) - ).execute() - - val loginData = loginResponse.data?.loginUser - if (loginResponse.hasErrors() || - loginData?.accessToken == null || - loginData.refreshToken == null - ) { - Log.e("UserInfoRepository", "Login failed: ${loginResponse.errors}") - return false - } - - tokenManager.saveTokens( - loginData.accessToken, - loginData.refreshToken - ) - Log.d("UserInfoRepository", "Saved backend tokens successfully") - true - } catch (e: Exception) { - Log.e("UserInfoRepository", "Error logging in user", e) - false - } - } - - suspend fun syncUserToDataStore(netId: String): Boolean { - return try { - if (!loginAndStoreTokens(netId)) return false - val user = getUserByNetId(netId) ?: return false - - storeUserFields( - id = user.id, - username = user.name, - netId = user.netId, - email = user.email, - goalSkip = user.workoutGoal == null, - goal = user.workoutGoal ?: 0 - ) - - Log.d("UserInfoRepositoryImpl", "Synced existing user to DataStore: ${user.id}") - true - } catch (e: Exception) { - Log.e("UserInfoRepositoryImpl", "Error syncing user to DataStore", e) - false - } - } - suspend fun getUserByNetId(netId: String): UserInfo? { try { val response = apolloClient.query( 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 bbd77c6..5170980 100644 --- a/app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt +++ b/app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt @@ -3,12 +3,19 @@ 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 com.cornellappdev.uplift.data.auth.AuthInterceptor +import com.cornellappdev.uplift.data.auth.TokenAuthenticator import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.inject.Qualifier import javax.inject.Singleton @@ -16,17 +23,51 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object AppModule { + @Retention(AnnotationRetention.BINARY) + @Qualifier + annotation class ApplicationScope + + @Provides + @Singleton + @ApplicationScope + fun provideApplicationScope(): CoroutineScope = + CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + @Provides + @Singleton + @Named("refresh") // Use a named annotation to distinguish them + fun provideRefreshApolloClient(): ApolloClient { + // This client does NOT have interceptors to avoid loops + val okHttpClient = OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .build() + + return ApolloClient.Builder() + .serverUrl(BuildConfig.BACKEND_URL) + .okHttpClient(okHttpClient) + .build() + } + @Provides @Singleton - fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient { + @Named("main") + fun provideOkHttpClient( + authInterceptor: AuthInterceptor, + tokenAuthenticator: TokenAuthenticator + ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(authInterceptor) + .authenticator(tokenAuthenticator) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) .build() } @Provides @Singleton - fun provideApolloClient(okHttpClient: OkHttpClient): ApolloClient { + @Named("main") + fun provideApolloClient(@Named("main") okHttpClient: OkHttpClient): ApolloClient { return ApolloClient.Builder() .serverUrl(BuildConfig.BACKEND_URL) .okHttpClient(okHttpClient) 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 90a133e..e3faa75 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -13,6 +13,9 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -55,6 +58,7 @@ import com.cornellappdev.uplift.ui.viewmodels.nav.RootNavigationViewModel import com.cornellappdev.uplift.ui.viewmodels.profile.CheckInViewModel import com.cornellappdev.uplift.ui.viewmodels.profile.ConfettiViewModel import com.cornellappdev.uplift.util.CHECK_IN_FLAG +import com.cornellappdev.uplift.util.ONBOARDING_FLAG import com.cornellappdev.uplift.util.PRIMARY_BLACK import com.cornellappdev.uplift.util.PRIMARY_YELLOW import com.cornellappdev.uplift.util.montserratFamily @@ -67,7 +71,7 @@ import kotlinx.serialization.Serializable /** * The main navigation controller for the app. - */ +*/ @Composable fun MainNavigationWrapper( // Note: For future view models, please add them to the screen they are used in instead of here. @@ -75,7 +79,6 @@ fun MainNavigationWrapper( gymDetailViewModel: GymDetailViewModel = hiltViewModel(), classDetailViewModel: ClassDetailViewModel = hiltViewModel(), rootNavigationViewModel: RootNavigationViewModel = hiltViewModel(), - ) { val rootNavigationUiState = rootNavigationViewModel.collectUiStateValue() val startDestination = rootNavigationUiState.startDestination @@ -100,6 +103,18 @@ fun MainNavigationWrapper( systemUiController.setStatusBarColor(PRIMARY_YELLOW) + val isLoggedIn = rootNavigationUiState.isLoggedIn + var wasLoggedIn by rememberSaveable { mutableStateOf(isLoggedIn) } + + LaunchedEffect(isLoggedIn) { + if (wasLoggedIn && !isLoggedIn && ONBOARDING_FLAG) { + navController.navigate(UpliftRootRoute.Onboarding) { + popUpTo(0) + } + } + wasLoggedIn = isLoggedIn + } + //TODO: Try to consolidate launched effects into one with consumeIn function that takes in coroutine scope LaunchedEffect(rootNavigationUiState.navEvent) { rootNavigationUiState.navEvent?.consumeSuspend { diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt index 846ce67..b728335 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt @@ -1,6 +1,7 @@ package com.cornellappdev.uplift.ui.viewmodels.nav import androidx.lifecycle.viewModelScope +import com.cornellappdev.uplift.data.auth.SessionManager import com.cornellappdev.uplift.data.repositories.UserInfoRepository import com.cornellappdev.uplift.ui.UpliftRootRoute import com.cornellappdev.uplift.ui.nav.RootNavigationRepository @@ -14,15 +15,27 @@ import javax.inject.Inject @HiltViewModel class RootNavigationViewModel @Inject constructor( rootNavigationRepository: RootNavigationRepository, - private val userInfoRepository: UserInfoRepository + private val userInfoRepository: UserInfoRepository, + val sessionManager: SessionManager ) : UpliftViewModel( - initialUiState = RootNavigationUiState() + initialUiState = RootNavigationUiState( + isLoggedIn = sessionManager.isLoggedIn.value, + startDestination = if (!ONBOARDING_FLAG) { + UpliftRootRoute.Home + } else if (sessionManager.isLoggedIn.value) { + UpliftRootRoute.Home + } + else { + UpliftRootRoute.Onboarding + } + ) ) { data class RootNavigationUiState( + val isLoggedIn: Boolean = false, val navEvent: UIEvent? = null, val popBackStack: UIEvent? = null, val navigateUp: UIEvent? = null, - val startDestination: UpliftRootRoute = UpliftRootRoute.Home + val startDestination: UpliftRootRoute = if (ONBOARDING_FLAG) UpliftRootRoute.Onboarding else UpliftRootRoute.Home ) init { @@ -46,18 +59,18 @@ class RootNavigationViewModel @Inject constructor( } viewModelScope.launch { - val hasSkipped = userInfoRepository.getSkipFromDataStore() - var hasUser = false - if (userInfoRepository.hasFirebaseUser()) { - val user = userInfoRepository.getFirebaseUser() - val email = user?.email - val netId = email?.substringBefore('@') - hasUser = netId?.let { userInfoRepository.hasUser(it) } ?: false - } - applyMutation { - copy( - startDestination = if (hasSkipped || hasUser || !ONBOARDING_FLAG) UpliftRootRoute.Home else UpliftRootRoute.Onboarding - ) + sessionManager.isLoggedIn.collect { loggedIn -> + applyMutation { + copy(isLoggedIn = loggedIn) + } + + val hasSkipped = userInfoRepository.getSkipFromDataStore() + val shouldShowHome = loggedIn || hasSkipped || !ONBOARDING_FLAG + applyMutation { + copy( + startDestination = if (shouldShowHome) UpliftRootRoute.Home else UpliftRootRoute.Onboarding + ) + } } } } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt index bcb7bab..9859f46 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt @@ -43,19 +43,17 @@ class LoginViewModel @Inject constructor( } when { userInfoRepository.hasUser(netId) -> { - val synced = userInfoRepository.syncUserToDataStore(netId) - if (synced) { - rootNavigationRepository.navigate(UpliftRootRoute.Home) + val success = userInfoRepository.loginUser(netId) + if (success) { + Log.d("LoginViewModel", "User logged in successfully") } else { - Log.e("Error", "Failed to sync existing user") userInfoRepository.signOut() } } - - userInfoRepository.hasFirebaseUser() -> rootNavigationRepository.navigate( - UpliftRootRoute.ProfileCreation - ) - + userInfoRepository.hasFirebaseUser() -> { + rootNavigationRepository.navigate(UpliftRootRoute.ProfileCreation) + } + //TODO: Handle error else -> { Log.e("Error", "Unexpected credential") userInfoRepository.signOut() 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 9331c5d..f5e21df 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 @@ -48,7 +48,7 @@ class ProfileCreationViewModel @Inject constructor( val isSkipped = state.isGoalSkipped val goal = if (isSkipped) 0 else state.goal.toInt() if (userInfoRepository.createUser(email, name, netId, isSkipped, goal)) { - navigateToHome() + Log.d("ProfileCreationViewModel", "User created successfully") } else { //TODO: Add error handling Log.e("Error", "User not created")