Skip to content

Token and Session Management#106

Merged
isiahpwilliams merged 11 commits intomainfrom
preston/tokens
Mar 25, 2026
Merged

Token and Session Management#106
isiahpwilliams merged 11 commits intomainfrom
preston/tokens

Conversation

@isiahpwilliams
Copy link
Copy Markdown
Contributor

@isiahpwilliams isiahpwilliams commented Mar 23, 2026

Overview

Handled user token expiration and created way to manage the user's session

Changes Made

Created a token manager to encrypt and store access and refresh tokens for users [TokenManager.kt]
Created a token authenticator to attach access token header to all mutations [TokenAuthenticator.kt]
Created a session manager to automatically refresh expired tokens and log users out when their tokens aren't able to be refreshed [SessionManager.kt]
Made another apollo client to enable token refresh and created an application scope for project [AppModule.kt]
Added name flag to Apollo Client fields of multiple repositories to distinguish between the two

Test Coverage

Was able to test by deleting my account and going through onboarding again to test whether the access tokens were available or not with the refactor changes

Related PRs or Issues (delete if not applicable)

Improved on Token management component of "Goals For Onboarding" PR

Screenshots (delete if not applicable)

Log Cat Testing Screenshot 2026-03-23 at 3 38 28 PM This is proof that the access token is being checked for consistently by the authenticator and session manager and is being refreshed

Summary by CodeRabbit

  • New Features

    • Automatic access-token refresh and new session management to keep users logged in reliably.
    • GraphQL endpoints added to support logout and token refresh flows.
  • Improvements

    • Logout now clears stored session data for full sign-out.
    • Login/onboarding navigation updated to react to session state.
    • Debug build: onboarding and check-in flags defaulted to disabled.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds reactive session management and automatic token refresh: introduces SessionManager, TokenAuthenticator, moves auth utilities to a dedicated package, provides named Apollo clients ("main" and "refresh"), updates DI and repositories to use @Named("main"), alters login/session flows, adds GraphQL mutations, and toggles debug build flags.

Changes

Cohort / File(s) Summary
Build Configuration
app/build.gradle
Debug buildConfigField booleans ONBOARDING_FLAG, CHECK_IN_FLAG changed from truefalse.
GraphQL Mutations
app/src/main/graphql/User.graphql
Added LogoutUser and RefreshAccessToken mutations.
DI — AppModule
app/src/main/java/.../di/AppModule.kt
Added @ApplicationScope qualifier and scope provider; introduced @Named("main") and @Named("refresh") Apollo clients; main OkHttpClient now uses AuthInterceptor + TokenAuthenticator; refresh client is bare with short timeouts.
Auth package (moved/renamed)
app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt, .../TokenManager.kt
Moved classes to data.auth, adjusted logging/imports; TokenManager now exposes tokenFlow, user-session helpers and getUserId().
Auth package (new)
app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt, .../TokenAuthenticator.kt
Added SessionManager (isLoggedIn StateFlow, startSession, logout, userId) and TokenAuthenticator (OkHttp Authenticator performing synchronized refresh via @Named("refresh") ApolloClient; logs out on failure).
Repositories — DI qualifier updates
app/src/main/java/.../data/repositories/*Repository.kt
Multiple repositories updated to inject @Named("main") ApolloClient (CapacityReminders, CheckIn, PopularTimes, Profile, Report, UpliftApi, etc.).
UserInfoRepository — login/session changes
app/src/main/java/.../data/repositories/UserInfoRepository.kt
Replaced previous token persistence/sync flow with loginUser(netId) using SessionManager.startSession(...); parses user id safely (toIntOrNull) and updates uploadGoal to accept an optional manual token header. Removed old sync/login helpers.
ViewModels — navigation & onboarding flows
app/src/main/java/.../ui/viewmodels/nav/RootNavigationViewModel.kt, .../ui/MainNavigationWrapper.kt, .../onboarding/*ViewModel.kt
RootNavigationViewModel injects SessionManager and reacts to isLoggedIn for startDestination; MainNavigationWrapper observes login state and navigates to onboarding when logged out; Login/ProfileCreation ViewModels updated to call new login flow and adjust navigation/logging.
Misc
app/src/main/java/.../data/auth/AuthInterceptor.kt
Switched header insertion to header(...), package updated, minor logging/newline tweaks.

Sequence Diagrams

sequenceDiagram
    participant Client as App (OkHttp)
    participant TokenAuth as TokenAuthenticator
    participant TokenMgr as TokenManager
    participant ApolloRefresh as ApolloClient (Refresh)
    participant Server as GraphQL Server
    participant SessionMgr as SessionManager

    Client->>Server: Request with Authorization: Bearer <access>
    Server-->>Client: 401 Unauthorized

    Client->>TokenAuth: OkHttp calls authenticate(response)
    TokenAuth->>TokenMgr: getRefreshToken()
    TokenMgr-->>TokenAuth: refresh token or null

    alt refresh token present
        rect rgba(200,150,255,0.5)
            TokenAuth->>ApolloRefresh: RefreshAccessTokenMutation (Authorization: Bearer <refresh>)
            ApolloRefresh->>Server: POST GraphQL mutation
            Server-->>ApolloRefresh: { newAccessToken }
        end
        TokenAuth->>TokenMgr: saveTokens(newAccessToken, existingRefresh)
        TokenAuth->>Client: return original request with Authorization: Bearer <newAccessToken>
        Client->>Server: Retry request with new token
        Server-->>Client: 200 OK
    else no refresh token or refresh failed
        TokenAuth->>SessionMgr: logout()
        SessionMgr-->>TokenMgr: clearTokens()
        TokenAuth-->>Client: null (stop retries)
    end
Loading
sequenceDiagram
    participant User as User
    participant LoginVM as LoginViewModel
    participant UserRepo as UserInfoRepository
    participant ApolloMain as ApolloClient (Main)
    participant Server as GraphQL Server
    participant SessionMgr as SessionManager
    participant TokenMgr as TokenManager

    User->>LoginVM: Submit netId
    LoginVM->>UserRepo: loginUser(netId)

    rect rgba(100,200,150,0.5)
        UserRepo->>ApolloMain: LoginUserMutation
        ApolloMain->>Server: POST GraphQL mutation
        Server-->>ApolloMain: { accessToken, refreshToken, user }
    end

    UserRepo->>UserRepo: getUserByNetId(), parse userId (toIntOrNull)
    UserRepo->>SessionMgr: startSession(userId, name, email, access, refresh)
    SessionMgr->>TokenMgr: saveUserSession(userId, name, email)
    SessionMgr->>TokenMgr: saveTokens(access, refresh)
    SessionMgr->>SessionMgr: emit isLoggedIn = true
    UserRepo-->>LoginVM: true
    LoginVM-->>User: Login complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • AndrewCheung360

Poem

🐰 I hopped through tokens, hopped through code,

SessionManager hummed a safe abode,
Authenticator chased the stale token night,
Refresh client leapt to set things right,
A rabbit cheers — login flows take flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Token and Session Management' is concise and directly summarizes the main change: introducing comprehensive token and session management features throughout the codebase.
Description check ✅ Passed PR description addresses all major template sections with clear overview, detailed changes made, and test coverage with supporting screenshot.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch preston/tokens

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt (1)

96-98: navigateToHome() appears unused after this change.

Since navigation is now reactive via SessionManager, this private method may be dead code. Consider removing it if no longer needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt`
around lines 96 - 98, The private method navigateToHome() (which calls
rootNavigationRepository.navigate(UpliftRootRoute.Home)) appears to be unused
now that navigation is driven reactively via SessionManager; remove this dead
method and any now-unused imports or references to it, or if it's referenced by
tests or external callers keep it and instead convert its callers to rely on
SessionManager; alternatively annotate with `@Suppress`("unused") if you must keep
it for reflection/tests—prefer deletion if there are no usages.
app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt (1)

62-64: Consider renaming or scoping clearTokens() to match its behavior.

clearTokens() calls clear() which removes all preferences including user session data (user_id, username, user_email). If this is intentional, consider renaming to clearAll() or clearSession(). If only tokens should be cleared, update to remove specific keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt`
around lines 62 - 64, The clearTokens() function currently calls
sharedPreferences?.edit { clear() } which wipes all stored prefs (including
user_id, username, user_email); either rename the method to reflect clearing
everything (e.g., clearAll() or clearSession()) or limit its behavior to only
token-related keys by editing sharedPreferences to remove the specific token
keys (use sharedPreferences?.edit { remove("access_token");
remove("refresh_token") } or equivalent). Update the function name and any
callers if you choose renaming, or replace the clear() call with targeted
remove(...) calls inside TokenManager.clearTokens() to preserve other user
session data.
app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt (2)

81-83: Simplify SessionManager access - redundant ViewModel instantiation.

The current pattern calls hiltViewModel<RootNavigationViewModel>() twice unnecessarily. The let block assigns to it which is never used.

♻️ Proposed fix
-    sessionManager: SessionManager = hiltViewModel<RootNavigationViewModel>().let {
-        hiltViewModel<RootNavigationViewModel>().sessionManager
-    }
+    sessionManager: SessionManager = rootNavigationViewModel.sessionManager
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt`
around lines 81 - 83, The sessionManager initialization redundantly calls
hiltViewModel<RootNavigationViewModel>() twice and uses a useless let block;
replace this with a single hiltViewModel<RootNavigationViewModel>() invocation
and read the sessionManager from that instance (e.g., obtain the
RootNavigationViewModel once and use its sessionManager property). Update the
expression that sets sessionManager to reference the single ViewModel instance
(RootNavigationViewModel) instead of calling hiltViewModel twice or using an
unused it.

111-117: Potential duplicate navigation logic with startDestination.

Both RootNavigationViewModel.startDestination and this LaunchedEffect react to isLoggedIn state. On app startup, both may attempt to set the initial route, potentially causing redundant navigation. Consider guarding against navigating to Onboarding if already there:

♻️ Suggested guard
     LaunchedEffect(isLoggedIn) {
-        if (!isLoggedIn && ONBOARDING_FLAG) {
+        val currentRoute = navController.currentDestination?.route
+        if (!isLoggedIn && ONBOARDING_FLAG && currentRoute != UpliftRootRoute.Onboarding::class.qualifiedName) {
             navController.navigate(UpliftRootRoute.Onboarding) {
                 popUpTo(0)
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt`
around lines 111 - 117, The LaunchedEffect block in MainNavigationWrapper
(reacting to isLoggedIn and ONBOARDING_FLAG) can duplicate navigation already
handled by RootNavigationViewModel.startDestination; update the LaunchedEffect
to first check the current navController destination and only call
navController.navigate(UpliftRootRoute.Onboarding) if the current route is not
already Onboarding (e.g., inspect
navController.currentBackStackEntry?.destination?.route or a similar marker) so
you avoid redundant navigation attempts when startDestination has already set
the route.
app/src/main/java/com/cornellappdev/uplift/data/repositories/SessionManager.kt (1)

17-22: Consider adding input validation to startSession.

The method accepts parameters directly without validation. Depending on how this is called, consider guarding against invalid inputs (e.g., empty tokens, negative userId) to fail fast rather than persisting invalid session state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/SessionManager.kt`
around lines 17 - 22, Validate inputs in startSession: check userId > 0, name
and email are not blank, and access/refresh tokens are not null/blank before
calling tokenManager.saveTokens or tokenManager.saveUserSession and before
setting _isLoggedIn; if any check fails, do not persist anything and fail fast
(e.g., throw IllegalArgumentException or return a failure result) so invalid
session state is never stored. Ensure these checks live inside startSession and
reference tokenManager.saveTokens, tokenManager.saveUserSession, and _isLoggedIn
to prevent partial updates.
app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt (1)

50-59: Potential startup flicker and repeated DataStore reads.

The startDestination initializes to Onboarding (when ONBOARDING_FLAG is true) before the collector runs and potentially updates it to Home. This could cause a brief navigation flicker on app startup.

Additionally, getSkipFromDataStore() is called on every isLoggedIn emission, which may involve I/O. Consider caching this value or reading it once during initialization.

♻️ Suggested optimization
     viewModelScope.launch {
+        val hasSkipped = userInfoRepository.getSkipFromDataStore()
         sessionManager.isLoggedIn.collect { loggedIn ->
-            val hasSkipped = userInfoRepository.getSkipFromDataStore()
             val shouldShowHome = loggedIn || hasSkipped || !ONBOARDING_FLAG
             applyMutation {
                 copy(
                     startDestination = if (shouldShowHome) UpliftRootRoute.Home else UpliftRootRoute.Onboarding
                 )
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt`
around lines 50 - 59, The startup flicker and repeated DataStore reads are
caused by computing startDestination before knowing the user's skip flag and by
calling userInfoRepository.getSkipFromDataStore() on every
sessionManager.isLoggedIn emission; fix it in RootNavigationViewModel by reading
and caching the skip value once (e.g., call
userInfoRepository.getSkipFromDataStore() once during init or use first()) and
computing the initial startDestination using ONBOARDING_FLAG, the cached
hasSkipped, and the current session state, then launch the collector
(viewModelScope.launch { sessionManager.isLoggedIn.collect { ... } }) to only
update on real changes while referencing the cached skip value; update
applyMutation(copy(startDestination = ...)) to use this cached value so you
avoid repeated I/O and eliminate the brief navigation flicker.
app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt (1)

21-32: LGTM - Consider adding timeout configuration.

The separation of "refresh" and "main" ApolloClient instances correctly prevents authentication loops. The refresh client intentionally omits interceptors.

As an optional improvement, consider configuring timeouts on the OkHttpClient to prevent indefinite hangs during network issues:

♻️ Optional timeout configuration
     fun provideRefreshApolloClient(): ApolloClient {
         // This client does NOT have interceptors to avoid loops
-        val okHttpClient = OkHttpClient.Builder().build()
+        val okHttpClient = OkHttpClient.Builder()
+            .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
+            .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
+            .build()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt` around lines 21 -
32, The provideRefreshApolloClient() currently builds an OkHttpClient without
any timeouts; update the OkHttpClient.Builder() in provideRefreshApolloClient to
configure sensible timeouts (e.g., connectTimeout, readTimeout, writeTimeout and
callTimeout) to avoid indefinite hangs during network issues—use
OkHttpClient.Builder().connectTimeout(..., TimeUnit.SECONDS).readTimeout(...,
TimeUnit.SECONDS).writeTimeout(..., TimeUnit.SECONDS).callTimeout(...,
TimeUnit.SECONDS) (or the Kotlin Duration overloads) and ensure TimeUnit is
imported; keep this change confined to the provideRefreshApolloClient
OkHttpClient construction so the refresh client still has no interceptors.
app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt (1)

44-61: LGTM with a note on error handling.

The refactored login flow correctly delegates navigation to session state. When loginUser returns true, the SessionManager will update isLoggedIn, triggering navigation via MainNavigationWrapper.

Consider addressing the TODO comments (Lines 26, 40, 56) to provide user feedback on authentication failures instead of silently signing out.

Would you like me to open an issue to track adding user-facing error handling (e.g., toast messages) for the login failure cases?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt`
around lines 44 - 61, The current branches (userInfoRepository.hasUser /
userInfoRepository.hasFirebaseUser / else) silently sign out or log an error
without user feedback; replace the TODOs by emitting a user-facing error event
from LoginViewModel (e.g., a LiveData/StateFlow like loginError or single-shot
UI event) when loginUser returns false and in the else branch, and use that
event to show a toast/snackbar or error screen instead of only calling
userInfoRepository.signOut; update calls surrounding
userInfoRepository.loginUser(netId) and userInfoRepository.hasFirebaseUser() and
rootNavigationRepository.navigate(UpliftRootRoute.ProfileCreation) to trigger
navigation only on success while reporting clear error messages via the new
ViewModel event so the UI can present feedback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenAuthenticator.kt`:
- Around line 58-61: The catch block in TokenAuthenticator (the catch (e:
Exception) that returns null during token refresh) is silently swallowing
exceptions; update it to log the caught exception with context (e.g., using
Log.e or Timber.e or your project's logger) so network/server errors during
refresh are recorded, then continue to return null; locate the catch in the
TokenAuthenticator class (the token refresh/authentication method) and replace
the empty swallow with a descriptive error log including e.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt`:
- Line 74: getUserId() currently returns sharedPreferences?.getInt("user_id",
-1) which yields -1 when the key is absent instead of null; update
TokenManager.getUserId() to check sharedPreferences?.contains("user_id") (or
read the int and map the sentinel -1 to null) and return null when the key
doesn't exist, otherwise return the stored Int value; reference
sharedPreferences and the "user_id" key when making this change so callers
receive null for "no user" rather than -1.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`:
- Around line 99-111: The session start block uses userInfo.id.toIntOrNull() ?:
-1 and logs a generic error using loginResponse.errors; change it to explicitly
handle a null or non-parsable userId and distinguish failure reasons: before
calling sessionManager.startSession, validate that userInfo is non-null and that
userInfo.id parses to Int (using userInfo.id.toIntOrNull() and bail with a clear
Log.e mentioning "missing userInfo" or "invalid user id" instead of -1), and
when tokens are missing log whether access/refresh tokens are absent versus
userInfo being null (include loginResponse.errors only when applicable). Update
the flow around sessionManager.startSession and the Log.e calls to produce
precise, separate log messages for missing tokens, null userInfo, and invalid
user id.
- Around line 58-64: The code currently calls sessionManager.startSession with
userId = id.toIntOrNull() ?: -1 which silently uses -1 on parse failure; update
the logic in the surrounding function (the caller performing id.toIntOrNull and
the call to sessionManager.startSession) to validate the id first: if
id.toIntOrNull() is null, do not call sessionManager.startSession and
return/propagate an error (e.g., return false or throw a descriptive exception)
instead of starting a session with -1, otherwise pass the valid parsed Int into
sessionManager.startSession; adjust any callers or return value handling
accordingly so invalid IDs fail early.

---

Nitpick comments:
In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/SessionManager.kt`:
- Around line 17-22: Validate inputs in startSession: check userId > 0, name and
email are not blank, and access/refresh tokens are not null/blank before calling
tokenManager.saveTokens or tokenManager.saveUserSession and before setting
_isLoggedIn; if any check fails, do not persist anything and fail fast (e.g.,
throw IllegalArgumentException or return a failure result) so invalid session
state is never stored. Ensure these checks live inside startSession and
reference tokenManager.saveTokens, tokenManager.saveUserSession, and _isLoggedIn
to prevent partial updates.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt`:
- Around line 62-64: The clearTokens() function currently calls
sharedPreferences?.edit { clear() } which wipes all stored prefs (including
user_id, username, user_email); either rename the method to reflect clearing
everything (e.g., clearAll() or clearSession()) or limit its behavior to only
token-related keys by editing sharedPreferences to remove the specific token
keys (use sharedPreferences?.edit { remove("access_token");
remove("refresh_token") } or equivalent). Update the function name and any
callers if you choose renaming, or replace the clear() call with targeted
remove(...) calls inside TokenManager.clearTokens() to preserve other user
session data.

In `@app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt`:
- Around line 21-32: The provideRefreshApolloClient() currently builds an
OkHttpClient without any timeouts; update the OkHttpClient.Builder() in
provideRefreshApolloClient to configure sensible timeouts (e.g., connectTimeout,
readTimeout, writeTimeout and callTimeout) to avoid indefinite hangs during
network issues—use OkHttpClient.Builder().connectTimeout(...,
TimeUnit.SECONDS).readTimeout(..., TimeUnit.SECONDS).writeTimeout(...,
TimeUnit.SECONDS).callTimeout(..., TimeUnit.SECONDS) (or the Kotlin Duration
overloads) and ensure TimeUnit is imported; keep this change confined to the
provideRefreshApolloClient OkHttpClient construction so the refresh client still
has no interceptors.

In `@app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt`:
- Around line 81-83: The sessionManager initialization redundantly calls
hiltViewModel<RootNavigationViewModel>() twice and uses a useless let block;
replace this with a single hiltViewModel<RootNavigationViewModel>() invocation
and read the sessionManager from that instance (e.g., obtain the
RootNavigationViewModel once and use its sessionManager property). Update the
expression that sets sessionManager to reference the single ViewModel instance
(RootNavigationViewModel) instead of calling hiltViewModel twice or using an
unused it.
- Around line 111-117: The LaunchedEffect block in MainNavigationWrapper
(reacting to isLoggedIn and ONBOARDING_FLAG) can duplicate navigation already
handled by RootNavigationViewModel.startDestination; update the LaunchedEffect
to first check the current navController destination and only call
navController.navigate(UpliftRootRoute.Onboarding) if the current route is not
already Onboarding (e.g., inspect
navController.currentBackStackEntry?.destination?.route or a similar marker) so
you avoid redundant navigation attempts when startDestination has already set
the route.

In
`@app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt`:
- Around line 50-59: The startup flicker and repeated DataStore reads are caused
by computing startDestination before knowing the user's skip flag and by calling
userInfoRepository.getSkipFromDataStore() on every sessionManager.isLoggedIn
emission; fix it in RootNavigationViewModel by reading and caching the skip
value once (e.g., call userInfoRepository.getSkipFromDataStore() once during
init or use first()) and computing the initial startDestination using
ONBOARDING_FLAG, the cached hasSkipped, and the current session state, then
launch the collector (viewModelScope.launch { sessionManager.isLoggedIn.collect
{ ... } }) to only update on real changes while referencing the cached skip
value; update applyMutation(copy(startDestination = ...)) to use this cached
value so you avoid repeated I/O and eliminate the brief navigation flicker.

In
`@app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt`:
- Around line 44-61: The current branches (userInfoRepository.hasUser /
userInfoRepository.hasFirebaseUser / else) silently sign out or log an error
without user feedback; replace the TODOs by emitting a user-facing error event
from LoginViewModel (e.g., a LiveData/StateFlow like loginError or single-shot
UI event) when loginUser returns false and in the else branch, and use that
event to show a toast/snackbar or error screen instead of only calling
userInfoRepository.signOut; update calls surrounding
userInfoRepository.loginUser(netId) and userInfoRepository.hasFirebaseUser() and
rootNavigationRepository.navigate(UpliftRootRoute.ProfileCreation) to trigger
navigation only on success while reporting clear error messages via the new
ViewModel event so the UI can present feedback.

In
`@app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt`:
- Around line 96-98: The private method navigateToHome() (which calls
rootNavigationRepository.navigate(UpliftRootRoute.Home)) appears to be unused
now that navigation is driven reactively via SessionManager; remove this dead
method and any now-unused imports or references to it, or if it's referenced by
tests or external callers keep it and instead convert its callers to rely on
SessionManager; alternatively annotate with `@Suppress`("unused") if you must keep
it for reflection/tests—prefer deletion if there are no usages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75c38f1e-58e7-412b-b55b-2f88669bd909

📥 Commits

Reviewing files that changed from the base of the PR and between 9f77d68 and a49fda6.

📒 Files selected for processing (17)
  • app/build.gradle
  • app/src/main/graphql/User.graphql
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/CapacityRemindersRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/PopularTimesRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/ReportRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/SessionManager.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenAuthenticator.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/UpliftApiRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/ProfileCreationViewModel.kt

Comment thread app/src/main/java/com/cornellappdev/uplift/data/repositories/TokenManager.kt Outdated
Comment thread app/src/main/java/com/cornellappdev/uplift/data/repositories/SessionManager.kt Outdated
@AndrewCheung360
Copy link
Copy Markdown
Member

For the AuthInterceptor code which isn't here, since we are adding authenticator with retries, we might want to change addHeader to just header to make sure we replace the header with the old access token.

Comment thread app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt Outdated
Comment thread app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt Outdated
Comment thread app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt Outdated
@@ -62,4 +62,15 @@ class TokenManager @Inject constructor(@ApplicationContext private val context:
fun clearTokens() {
sharedPreferences?.edit { clear() }
}

fun saveUserSession(userId: Int, username: String, userEmail: String) {
Copy link
Copy Markdown
Member

@AndrewCheung360 AndrewCheung360 Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It's probably fine to have this here, but the naming of this class implies mainly handling tokens so having other user data stuff here might be a little confusing

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the above clearTokens function will now clear these data fields as well, which could be the intended behavior anyways but same "issue" as above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think clearing the data fields makes sense since if you don't have tokens, you would get logged out and hence, you wouldn't use the user data anyway

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to clear data fields, mainly just a minor naming mismatch with clearTokens.

) : Authenticator {

override fun authenticate(route: Route?, response: Response): Request? {
val refreshToken = tokenManager.getRefreshToken() ?: return null
Copy link
Copy Markdown
Member

@AndrewCheung360 AndrewCheung360 Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably out of scope of this PR, but for refresh tokens, if we know when refresh tokens expire from the backend and if backend has some endpoint that allows us to get a new refresh token aside from forcing the user to log in, it would probably be better UX to get the new refresh token before it expires and keep the user logged in for longer and then fall back to logging the user out for cases where maybe the user hasn't opened the app in a long time.

Comment thread app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt Outdated
Comment thread app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt Outdated
Copy link
Copy Markdown
Member

@AndrewCheung360 AndrewCheung360 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some initial comments for you to look at for some possible improvements, but overall nice work!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt (1)

4-4: Redundant import: TokenManager is in the same package.

Since TokenManager is now in com.cornellappdev.uplift.data.auth (same as this file), the explicit import is unnecessary.

🧹 Proposed fix
 import android.util.Log
-import com.cornellappdev.uplift.data.auth.TokenManager
 import okhttp3.Interceptor
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt` at
line 4, The import of TokenManager at the top of AuthInterceptor.kt is redundant
because TokenManager resides in the same package; remove the explicit import
statement referencing TokenManager so the file relies on the package scope,
keeping class AuthInterceptor unchanged.
app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt (1)

63-74: Session starts before goal upload - partial state on failure.

If uploadGoal() fails (line 71-73), the function returns false but the session is already active (started at line 63). This leaves the user in a partially logged-in state where tokens are saved but onboarding isn't complete. Consider moving startSession() after the goal upload succeeds, or handling the rollback on failure.

♻️ Proposed restructure
-            sessionManager.startSession(
-                userId = id,
-                name = name,
-                email = email,
-                access = accessToken,
-                refresh = refreshToken
-            )
             if (!skip) {
                 if (!uploadGoal(id, goal)) {
                     return false
                 }
             }
             else {
                 Log.d("UserInfoRepository", "Skipping goal upload")
             }
+            sessionManager.startSession(
+                userId = id,
+                name = name,
+                email = email,
+                access = accessToken,
+                refresh = refreshToken
+            )
             storeUserFields(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`
around lines 63 - 74, The current flow calls sessionManager.startSession(...)
before uploadGoal(...), which can leave tokens saved if uploadGoal fails; change
the order so uploadGoal(id, goal) is attempted first (when skip is false) and
only call sessionManager.startSession(...) after uploadGoal returns true, or if
you prefer to keep the current order implement a rollback by calling
sessionManager.clearSession()/endSession and removing stored tokens when
uploadGoal(...) returns false; update the logic around startSession, uploadGoal,
and sessionManager to ensure no active session exists on uploadGoal failure.
app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt (1)

16-20: Consider adding @Singleton annotation for consistency and clarity.

TokenAuthenticator is implicitly scoped to the singleton component since it's only injected in provideOkHttpClient() (a @Singleton provider). While the code works correctly as-is, explicitly annotating @Singleton would improve consistency with TokenManager and SessionManager, making the scoping intent clearer to future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt`
around lines 16 - 20, The TokenAuthenticator class lacks an explicit scope
annotation; add the `@Singleton` annotation to the TokenAuthenticator class
declaration so its scope is consistent with TokenManager and SessionManager and
matches the singleton provider that injects it (see TokenAuthenticator,
TokenManager, SessionManager, and the provideOkHttpClient provider).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt`:
- Around line 17-20: The interceptor currently uses addHeader("Authorization",
"Bearer $token") inside chain.request().newBuilder().apply which appends a new
Authorization header on retries; change that call to header("Authorization",
"Bearer $token") so the header is set/replaced instead of duplicated (locate the
addHeader usage in AuthInterceptor and swap to header within the request builder
logic).

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`:
- Around line 104-108: Logging uses the nullified parsed variable `id` instead
of the original string; in the method in UserInfoRepository where you parse `val
id = userInfo.id.toIntOrNull()` (e.g., the login routine), update the error log
to include the original `userInfo.id` string (and keep the same descriptive
message) so the log shows the actual input that failed to parse rather than the
null `id` variable.
- Around line 51-55: The log currently prints the parsed `id` variable (which is
null on parse failure) making the message unhelpful; update the error logging in
UserInfoRepository where you call `userFields.id.toIntOrNull()` (the block that
returns false) to log the original string `userFields.id` (and optionally a
short context message) instead of `id` so the malformed input value is visible
in the log.

---

Nitpick comments:
In `@app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt`:
- Line 4: The import of TokenManager at the top of AuthInterceptor.kt is
redundant because TokenManager resides in the same package; remove the explicit
import statement referencing TokenManager so the file relies on the package
scope, keeping class AuthInterceptor unchanged.

In `@app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt`:
- Around line 16-20: The TokenAuthenticator class lacks an explicit scope
annotation; add the `@Singleton` annotation to the TokenAuthenticator class
declaration so its scope is consistent with TokenManager and SessionManager and
matches the singleton provider that injects it (see TokenAuthenticator,
TokenManager, SessionManager, and the provideOkHttpClient provider).

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`:
- Around line 63-74: The current flow calls sessionManager.startSession(...)
before uploadGoal(...), which can leave tokens saved if uploadGoal fails; change
the order so uploadGoal(id, goal) is attempted first (when skip is false) and
only call sessionManager.startSession(...) after uploadGoal returns true, or if
you prefer to keep the current order implement a rollback by calling
sessionManager.clearSession()/endSession and removing stored tokens when
uploadGoal(...) returns false; update the logic around startSession, uploadGoal,
and sessionManager to ensure no active session exists on uploadGoal failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30aaaa26-59f2-4393-b46b-1177035e00ef

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5acaf and 93819e6.

📒 Files selected for processing (8)
  • app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt
  • app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt
  • app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt
  • app/src/main/java/com/cornellappdev/uplift/data/auth/TokenManager.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt
  • app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt (2)

23-26: Add a retry count guard to prevent infinite refresh loops.

If the refreshed token is also rejected by the server (e.g., account revoked, permissions changed), this authenticator will continuously refresh and retry without limit. OkHttp's Response.priorResponse can be used to count attempts.

🛡️ Proposed fix
+    private fun responseCount(response: Response): Int {
+        var count = 1
+        var prior = response.priorResponse
+        while (prior != null) {
+            count++
+            prior = prior.priorResponse
+        }
+        return count
+    }
+
     override fun authenticate(route: Route?, response: Response): Request? {
+        // Prevent infinite retry loops
+        if (responseCount(response) >= 3) {
+            Log.w("TokenAuthenticator", "Max retry attempts reached, giving up")
+            return null
+        }
+
         val refreshToken = tokenManager.getRefreshToken() ?: return null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt`
around lines 23 - 26, The authenticate method in TokenAuthenticator currently
refreshes and retries indefinitely; add a retry-count guard by walking
response.priorResponse to count previous attempts and bail out when a max retry
threshold (e.g., MAX_REFRESH_ATTEMPTS) is reached; if the count exceeds the
limit, return null to avoid further retries. Implement the guard at the start of
authenticate (before calling tokenManager.getRefreshToken/refreshToken) and
reference the authenticate method and
tokenManager.refreshToken/tokenManager.getRefreshToken so the flow stops and no
new request is built when the max attempts are exceeded.

6-7: Redundant same-package imports.

SessionManager and TokenManager are in the same package (com.cornellappdev.uplift.data.auth) as this file, so these imports are unnecessary in Kotlin.

🧹 Proposed fix
 import com.apollographql.apollo.ApolloClient
 import com.cornellappdev.uplift.RefreshAccessTokenMutation
-import com.cornellappdev.uplift.data.auth.SessionManager
-import com.cornellappdev.uplift.data.auth.TokenManager
 import kotlinx.coroutines.runBlocking
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt`
around lines 6 - 7, The file TokenAuthenticator.kt has redundant same-package
imports for SessionManager and TokenManager; remove the import lines for
com.cornellappdev.uplift.data.auth.SessionManager and
com.cornellappdev.uplift.data.auth.TokenManager from the top of
TokenAuthenticator.kt so the classes are referenced directly (they’re in the
same package), and run a quick build or linter to ensure no other unused-import
warnings remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt`:
- Around line 23-26: The authenticate method in TokenAuthenticator currently
refreshes and retries indefinitely; add a retry-count guard by walking
response.priorResponse to count previous attempts and bail out when a max retry
threshold (e.g., MAX_REFRESH_ATTEMPTS) is reached; if the count exceeds the
limit, return null to avoid further retries. Implement the guard at the start of
authenticate (before calling tokenManager.getRefreshToken/refreshToken) and
reference the authenticate method and
tokenManager.refreshToken/tokenManager.getRefreshToken so the flow stops and no
new request is built when the max attempts are exceeded.
- Around line 6-7: The file TokenAuthenticator.kt has redundant same-package
imports for SessionManager and TokenManager; remove the import lines for
com.cornellappdev.uplift.data.auth.SessionManager and
com.cornellappdev.uplift.data.auth.TokenManager from the top of
TokenAuthenticator.kt so the classes are referenced directly (they’re in the
same package), and run a quick build or linter to ensure no other unused-import
warnings remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cca1dd5-8f2c-45ec-a5d6-a63ba479c178

📥 Commits

Reviewing files that changed from the base of the PR and between 93819e6 and 85f38b6.

📒 Files selected for processing (3)
  • app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt
  • app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt
  • app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/cornellappdev/uplift/data/auth/AuthInterceptor.kt
  • app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt (2)

104-108: ⚠️ Potential issue | 🟡 Minor

Same string interpolation bug as above.

'$userInfo.id' should be '${userInfo.id}' to correctly log the ID value.

🐛 Proposed fix
                 val id = userInfo.id.toIntOrNull()
                 if (id == null) {
-                    Log.e("UserInfoRepository", "Failed to log in: non-numeric user ID '$userInfo.id'")
+                    Log.e("UserInfoRepository", "Failed to log in: non-numeric user ID '${userInfo.id}'")
                     return false
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`
around lines 104 - 108, In UserInfoRepository (around the login path), the Log.e
call incorrectly interpolates userInfo.id as '$userInfo.id' which logs the
literal string; change the interpolation to use '${userInfo.id}' so the actual
ID value is logged (update the Log.e message that currently reads "Failed to log
in: non-numeric user ID '$userInfo.id'" to use the corrected '${userInfo.id}'
form).

51-55: ⚠️ Potential issue | 🟡 Minor

String interpolation bug: logs object reference instead of property value.

'$userFields.id' interpolates userFields.toString() followed by the literal string .id, not the actual ID value. This was flagged in a previous review but the fix is incorrect.

🐛 Proposed fix
             val id = userFields.id.toIntOrNull()
             if (id == null) {
-                Log.e("UserInfoRepository", "Failed to set goal: non-numeric user ID '$userFields.id'")
+                Log.e("UserInfoRepository", "Failed to set goal: non-numeric user ID '${userFields.id}'")
                 return false
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`
around lines 51 - 55, The Log.e call is interpolating the object reference
instead of the actual ID string ("'$userFields.id'"); update the log to include
the real value by using the ID variable or Kotlin string template syntax (e.g.,
${userFields.id} or $id) so Log.e("UserInfoRepository", "Failed to set goal:
non-numeric user ID '${userFields.id}'") prints the actual value; change the
string in the Log.e invocation near userFields.id/id in UserInfoRepository.kt
accordingly.
🧹 Nitpick comments (3)
app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt (1)

105-106: Avoid redundant state collection.

rootNavigationUiState is already collected at line 80. Use rootNavigationUiState.isLoggedIn instead of calling collectUiStateValue() again.

♻️ Proposed fix
-    val isLoggedIn = rootNavigationViewModel.collectUiStateValue().isLoggedIn
+    val isLoggedIn = rootNavigationUiState.isLoggedIn
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt`
around lines 105 - 106, The code redundantly re-collects navigation state by
calling collectUiStateValue() again to set isLoggedIn; instead use the
already-collected rootNavigationUiState and read its isLoggedIn property.
Replace the usage that assigns isLoggedIn from collectUiStateValue() with
rootNavigationUiState.isLoggedIn so only the previously-collected state
(rootNavigationUiState) is referenced and avoid duplicate state collection.
app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt (1)

127-131: Misleading log message: id is Int?, not a string that failed parsing.

The error message says "non-numeric user ID" but id is already typed as Int?. When id is null, the log will always show 'null'. Consider a clearer message:

✏️ Suggested improvement
     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'")
+            Log.e("UserInfoRepository", "Failed to set goal: user ID is null")
             return false
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`
around lines 127 - 131, The log message in UserInfoRepository.uploadGoal
incorrectly implies a parsing error ("non-numeric user ID") even though id is an
Int?; change the error text to clearly state the ID is missing/null (e.g.,
"Failed to set goal: user ID is null") and still include the id value for
context, so update the Log.e call in uploadGoal to a clear, accurate message
referencing the id variable.
app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt (1)

52-63: Consider adding timeouts to the main OkHttpClient.

The refresh client has explicit timeouts, but the main client does not. Without timeouts, requests could hang indefinitely if the backend becomes unresponsive, which would also block the runBlocking call in TokenAuthenticator.

♻️ Proposed fix
     `@Provides`
     `@Singleton`
     `@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()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt` around lines 52 -
63, The main OkHttpClient provided by provideOkHttpClient (named "main") lacks
timeouts, so requests can hang and block TokenAuthenticator's runBlocking;
update provideOkHttpClient to set appropriate connectTimeout, readTimeout, and
writeTimeout (and optionally callTimeout) on the OkHttpClient.Builder before
build(), keeping the existing .addInterceptor(authInterceptor) and
.authenticator(tokenAuthenticator) configuration so the main client will fail
fast when backend is unresponsive.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt`:
- Around line 15-19: TokenAuthenticator uses synchronized(this) to guard token
refresh but is not annotated `@Singleton`, so multiple instances can bypass the
lock; annotate the TokenAuthenticator class with `@Singleton` (and add the
javax.inject.Singleton import) so Hilt provides a single instance across the
app, preserving the synchronized(this) protection for methods interacting with
TokenManager/SessionManager/apolloClient used in authenticate().
- Around line 76-83: The responseCount function fails to advance through the
Response chain causing an infinite loop or incorrect count; fix it by iterating
the traversal variable (response) inside the while loop (e.g., set response =
response.priorResponse each iteration) or rewrite to loop while current != null
and increment a counter, ensuring responseCount correctly returns the number of
prior responses used by the retry limiting logic in TokenAuthenticator.

In `@app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt`:
- Around line 83-84: Remove the unused local variable by deleting the line that
defines "val sessionManager = rootNavigationViewModel.sessionManager"; if logout
functionality is intended instead of removing, replace the unused extraction
with an explicit call such as "rootNavigationViewModel.sessionManager.logout()"
(or invoke the appropriate method on sessionManager) and ensure any necessary
imports are present.
- Around line 107-113: LaunchedEffect currently triggers navigation on initial
composition when isLoggedIn is false, duplicating the NavHost's startDestination
behavior; to fix, track the previous login state using a rememberSaveable var
(e.g., prevIsLoggedIn) and only call
navController.navigate(UpliftRootRoute.Onboarding) inside LaunchedEffect when
the state transitions from logged-in to logged-out (prevIsLoggedIn == true &&
!isLoggedIn) or otherwise skip the initial-run navigation; update prevIsLoggedIn
after handling so future logout transitions still navigate, and keep
ONBOARDING_FLAG checks intact.

---

Duplicate comments:
In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`:
- Around line 104-108: In UserInfoRepository (around the login path), the Log.e
call incorrectly interpolates userInfo.id as '$userInfo.id' which logs the
literal string; change the interpolation to use '${userInfo.id}' so the actual
ID value is logged (update the Log.e message that currently reads "Failed to log
in: non-numeric user ID '$userInfo.id'" to use the corrected '${userInfo.id}'
form).
- Around line 51-55: The Log.e call is interpolating the object reference
instead of the actual ID string ("'$userFields.id'"); update the log to include
the real value by using the ID variable or Kotlin string template syntax (e.g.,
${userFields.id} or $id) so Log.e("UserInfoRepository", "Failed to set goal:
non-numeric user ID '${userFields.id}'") prints the actual value; change the
string in the Log.e invocation near userFields.id/id in UserInfoRepository.kt
accordingly.

---

Nitpick comments:
In
`@app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt`:
- Around line 127-131: The log message in UserInfoRepository.uploadGoal
incorrectly implies a parsing error ("non-numeric user ID") even though id is an
Int?; change the error text to clearly state the ID is missing/null (e.g.,
"Failed to set goal: user ID is null") and still include the id value for
context, so update the Log.e call in uploadGoal to a clear, accurate message
referencing the id variable.

In `@app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt`:
- Around line 52-63: The main OkHttpClient provided by provideOkHttpClient
(named "main") lacks timeouts, so requests can hang and block
TokenAuthenticator's runBlocking; update provideOkHttpClient to set appropriate
connectTimeout, readTimeout, and writeTimeout (and optionally callTimeout) on
the OkHttpClient.Builder before build(), keeping the existing
.addInterceptor(authInterceptor) and .authenticator(tokenAuthenticator)
configuration so the main client will fail fast when backend is unresponsive.

In `@app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt`:
- Around line 105-106: The code redundantly re-collects navigation state by
calling collectUiStateValue() again to set isLoggedIn; instead use the
already-collected rootNavigationUiState and read its isLoggedIn property.
Replace the usage that assigns isLoggedIn from collectUiStateValue() with
rootNavigationUiState.isLoggedIn so only the previously-collected state
(rootNavigationUiState) is referenced and avoid duplicate state collection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d55ebb5-59a5-480c-9cc2-7f51747fd50c

📥 Commits

Reviewing files that changed from the base of the PR and between 85f38b6 and 598ebda.

📒 Files selected for processing (7)
  • app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt
  • app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt
  • app/src/main/java/com/cornellappdev/uplift/data/auth/TokenManager.kt
  • app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt
  • app/src/main/java/com/cornellappdev/uplift/di/AppModule.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/cornellappdev/uplift/data/auth/SessionManager.kt
  • app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt

Comment thread app/src/main/java/com/cornellappdev/uplift/data/auth/TokenAuthenticator.kt Outdated
Comment thread app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt Outdated
)
.execute()
if (manualToken != null) {
call.addHttpHeader("Authorization", "Bearer $manualToken")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed anymore if we have the interceptor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, because if you start the session after we try and update the goal, it won't work since we need the access tokens stored first

Copy link
Copy Markdown
Member

@AndrewCheung360 AndrewCheung360 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

@isiahpwilliams isiahpwilliams merged commit 4e81463 into main Mar 25, 2026
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants