Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField("String", "GOOGLE_AUTH_CLIENT_ID", secretsProperties["GOOGLE_AUTH_CLIENT_ID"])
buildConfigField("String", "BACKEND_URL", secretsProperties['PROD_ENDPOINT'])
buildConfigField("boolean", "ONBOARDING_FLAG", "false")
buildConfigField("boolean", "CHECK_IN_FLAG", "false")
buildConfigField("boolean", "ONBOARDING_FLAG", "true")
buildConfigField("boolean", "CHECK_IN_FLAG", "true")
}
debug {
buildConfigField("String", "BACKEND_URL", secretsProperties['DEV_ENDPOINT'])
Expand All @@ -58,8 +58,8 @@ android {
"GOOGLE_AUTH_CLIENT_ID", secretsProperties["GOOGLE_AUTH_CLIENT_ID"]
)
signingConfig signingConfigs.debug
buildConfigField("boolean", "ONBOARDING_FLAG", "false")
buildConfigField("boolean", "CHECK_IN_FLAG", "false")
buildConfigField("boolean", "ONBOARDING_FLAG", "true")
buildConfigField("boolean", "CHECK_IN_FLAG", "true")
}
}
compileOptions {
Expand Down
23 changes: 18 additions & 5 deletions app/src/main/graphql/User.graphql
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
fragment userFields on User {
id
email
name
netId
name
encodedImage
activeStreak
maxStreak
streakStart
workoutGoal
lastGoalChange
lastStreak
totalGymDays
workoutHistory {
...workoutFields
}
}

fragment workoutFields on Workout {
id
workoutTime
userId
facilityId
gymName
}

mutation CreateUser($email: String!, $name: String!, $netId: String!) {
Expand All @@ -33,14 +46,14 @@ query getUserByNetId($netId: String!) {
}
}

mutation SetWorkoutGoals($id: Int!, $workoutGoal: Int!) {
setWorkoutGoals(userId: $id, workoutGoal: $workoutGoal) {
mutation SetWorkoutGoals($userId: Int!, $workoutGoal: Int!) {
setWorkoutGoals(userId: $userId, workoutGoal: $workoutGoal) {
...userFields
}
}

mutation LogWorkout($facilityId: Int!, $workoutTime: DateTime!, $id: Int!) {
logWorkout(facilityId: $facilityId, userId: $id, workoutTime: $workoutTime) {
mutation LogWorkout($facilityId: Int!, $workoutTime: DateTime!, $userId: Int!) {
logWorkout(facilityId: $facilityId, userId: $userId, workoutTime: $workoutTime) {
...workoutFields
}
}
Expand Down
13 changes: 4 additions & 9 deletions app/src/main/graphql/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,11 @@ type User {
totalGymDays: Int!

"""
The start date of the most recent active streak, up until the current date.
The start datetime of the most recent active streak (midnight of the day in local timezone), up until the current date.
"""
streakStart: Date
streakStart: DateTime

workoutHistory: [Workout]
}

type Giveaway {
Expand Down Expand Up @@ -421,13 +423,6 @@ type Friendship {
friend: User
}

"""
The `Date` scalar type represents a Date
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar Date

type Workout {
id: ID!

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.cornellappdev.uplift.data.models

import android.net.Uri


data class ProfileData(
val name: String,
val netId: String,
val encodedImage: String?,
val totalGymDays: Int,
val activeStreak: Int,
val maxStreak: Int,
val streakStart: String?,
val workoutGoal: Int,
val workouts: List<WorkoutDomain>,
val weeklyWorkoutDays: List<String>
)
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package com.cornellappdev.uplift.data.models
import kotlinx.serialization.Serializable


@Serializable
data class UserInfo(
val id: String,
val email: String,
val name: String,
val netId: String,
val encodedImage: String?,
val activeStreak: Int?,
val maxStreak: Int?,
val streakStart: String?,
val workoutGoal: Int?,
val totalGymDays: Int
Comment on lines +11 to +16
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the GraphQL schema nullability for user fields
rg -n "activeStreak|maxStreak" app/src/main/graphql/schema.graphqls

Repository: cuappdev/uplift-android

Length of output: 113


Align field nullability with GraphQL schema.

activeStreak and maxStreak are declared as Int! (non-nullable) in the GraphQL schema, but defined as Int? here. Update to match the schema contract.

🤖 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/models/UserInfo.kt` around
lines 11 - 16, In UserInfo.kt update the nullability to match the GraphQL schema
by changing the activeStreak and maxStreak properties from nullable Int? to
non-nullable Int; locate the UserInfo class (fields like encodedImage,
activeStreak, maxStreak, streakStart, workoutGoal, totalGymDays) and remove the
question marks on activeStreak and maxStreak so their types are Int not Int?.

)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.cornellappdev.uplift.data.models

data class WorkoutDomain(
val gymName: String,
val timestamp: Long
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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}")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug log statement before release.

This debug log exposes authentication state information (token present = true/false) in production builds. While it doesn't leak the token itself, it's a development artifact that should be removed or guarded behind a debug build check.

🛡️ Proposed fix
-        android.util.Log.d("AuthInterceptor", "token present = ${token != null}")
+        // Remove or guard with BuildConfig.DEBUG if needed for debugging:
+        // if (BuildConfig.DEBUG) {
+        //     android.util.Log.d("AuthInterceptor", "token present = ${token != null}")
+        // }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
android.util.Log.d("AuthInterceptor", "token present = ${token != null}")
// Remove or guard with BuildConfig.DEBUG if needed for debugging:
// if (BuildConfig.DEBUG) {
// android.util.Log.d("AuthInterceptor", "token present = ${token != 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/repositories/AuthInterceptor.kt`
at line 14, Remove the development debug log in AuthInterceptor that prints
authentication presence; locate the android.util.Log.d("AuthInterceptor", ...)
call inside the AuthInterceptor class/method and either delete it or wrap it
with a debug-only guard (e.g., check BuildConfig.DEBUG) so it is not executed in
release builds; ensure no other calls leak auth state in production and keep
logging minimal and safe for release.

val request = chain.request().newBuilder().apply {
if (token != null) {
addHeader("Authorization", "Bearer $token")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,25 @@ class CheckInRepository @Inject constructor(
* Logs a completed workout to the backend. Returns true if the mutation succeeded, false otherwise.
*/
suspend fun logWorkoutFromCheckIn(gymId: Int): Boolean {
val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull() ?: return false
val userIdString = userInfoRepository.getUserIdFromDataStore()
val userId = userIdString?.toIntOrNull()
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Not sure if you need to separate into two variables if only used once


if (userId == null) {
Log.e("CheckInRepository", "Missing or invalid userId in DataStore: $userIdString")
return false
}

val time = Instant.now().toString()

return try {
val response = apolloClient
.mutation(LogWorkoutMutation(facilityId = gymId, workoutTime = time, id = userId ))
.mutation(
LogWorkoutMutation(
facilityId = gymId,
workoutTime = time,
userId = userId
)
)
.execute()

val ok = response.data?.logWorkout?.workoutFields != null && !response.hasErrors()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ object PreferencesKeys {
val NETID = stringPreferencesKey("netId")
val EMAIL = stringPreferencesKey("email")
val GOAL = intPreferencesKey("workoutGoal")
val SKIP = booleanPreferencesKey("skip")
val GOAL_SETTING_SKIPPED = booleanPreferencesKey("goalSkip")
val FCM_TOKEN = stringPreferencesKey("fcmToken")
val DECLINED_NOTIFICATION_PERMISSION =
booleanPreferencesKey("declinedNotificationPermission")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.cornellappdev.uplift.data.repositories

import android.util.Log
import com.apollographql.apollo.ApolloClient
import com.cornellappdev.uplift.GetUserByNetIdQuery
import com.cornellappdev.uplift.GetWeeklyWorkoutDaysQuery
import com.cornellappdev.uplift.SetWorkoutGoalsMutation
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.Singleton


@Singleton
class ProfileRepository @Inject constructor(
private val userInfoRepository: UserInfoRepository,
private val apolloClient: ApolloClient
) {
suspend fun getProfile(): Result<ProfileData> = runCatching {
val netId = userInfoRepository.getNetIdFromDataStore()
?: throw IllegalStateException("NetId missing")

val userResponse = apolloClient.query(
GetUserByNetIdQuery(netId)
).execute()

if (userResponse.hasErrors()) {
Log.e("ProfileRepo", "User query errors: ${userResponse.errors}")
throw IllegalStateException("User query failed")
}

val user = userResponse.data?.getUserByNetId?.firstOrNull()?.userFields
?: throw IllegalStateException("User not found")

val userId = user.id.toIntOrNull()
?: throw IllegalStateException("Invalid user ID: ${user.id}")

val weeklyResponse = apolloClient.query(GetWeeklyWorkoutDaysQuery(userId)).execute()

if (weeklyResponse.hasErrors()) {
throw IllegalStateException("Weekly workout days query failed: ${weeklyResponse.errors}")
}

val workouts = user.workoutHistory?.filterNotNull().orEmpty()

val workoutDomain = workouts.map {
WorkoutDomain(
gymName = it.workoutFields.gymName,
timestamp = Instant.parse(it.workoutFields.workoutTime.toString())
.toEpochMilli()
)
}
Comment on lines +47 to +53
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how DateTime scalar is configured in Apollo setup
rg -n "DateTime|scalar" --type=kotlin app/src/main/java/com/cornellappdev/uplift/
fd -e kt -x grep -l "ScalarAdapter\|customScalarAdapters" {} \;

Repository: cuappdev/uplift-android

Length of output: 1148


🏁 Script executed:

#!/bin/bash
# 1. Check the ProfileRepository.kt file context around lines 47-53
echo "=== ProfileRepository.kt context ==="
sed -n '40,65p' app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt

# 2. Find GraphQL schema files
echo -e "\n=== GraphQL schema files ==="
find . -name "*.graphql" -o -name "schema.json" | head -20

# 3. Check Apollo configuration
echo -e "\n=== Apollo configuration files ==="
find . -name "apollo.config.*" -o -name "build.gradle*" | head -20

Repository: cuappdev/uplift-android

Length of output: 1410


🏁 Script executed:

#!/bin/bash
# 1. Check User.graphql for workoutTime and DateTime definition
echo "=== User.graphql content ==="
cat app/src/main/graphql/User.graphql

# 2. Check app/build.gradle for Apollo configuration
echo -e "\n=== app/build.gradle Apollo configuration ==="
grep -A 20 -B 5 "apollo\|scalar" app/build.gradle | head -100

# 3. Look for generated Apollo code
echo -e "\n=== Generated Apollo directories ==="
find app -type d -name "generated" -o -type d -name "apolloClient" -o -type d -name "apollo" | head -20

Repository: cuappdev/uplift-android

Length of output: 4116


🏁 Script executed:

#!/bin/bash
# 1. Find generated Apollo types
echo "=== Looking for generated Apollo types ==="
find app/build -type f -name "*.kt" 2>/dev/null | grep -i "user\|workout" | head -20

# 2. Check if there's a build directory
echo -e "\n=== Build generated directories ==="
find app/build/generated -type d 2>/dev/null | head -20

# 3. Search for any kotlin file that might show DateTime definition
echo -e "\n=== Searching for DateTime type definition in generated code ==="
find app/build -type f -name "*.kt" 2>/dev/null -exec grep -l "DateTime\|workoutTime" {} \; | head -10

# 4. If generated files exist, check one
if [ -f "$(find app/build/generated -name '*User*.kt' 2>/dev/null | head -1)" ]; then
  echo -e "\n=== Sample generated User type ==="
  cat "$(find app/build/generated -name '*User*.kt' 2>/dev/null | head -1)" | head -100
fi

Repository: cuappdev/uplift-android

Length of output: 212


🏁 Script executed:

#!/bin/bash
# 1. Search for usage of workoutTime elsewhere in the codebase
echo "=== Usage of workoutTime in codebase ==="
rg -n "workoutTime" --type=kotlin app/src/

# 2. Check if there are any DateTime-related imports in ProfileRepository
echo -e "\n=== ProfileRepository.kt full context (1-80 lines) ==="
head -80 app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt

Repository: cuappdev/uplift-android

Length of output: 3430


🌐 Web query:

Apollo GraphQL 4.1.1 DateTime scalar default mapping Kotlin Android

💡 Result:

In Apollo Kotlin 4.1.1 (Android), a GraphQL scalar DateTime is not mapped to a Kotlin date type by default—you must map the scalar yourself in Gradle using mapScalar(...). Apollo’s docs show mapScalar(...) as the mechanism for custom scalars and that AnyAdapter exists as the generic fallback adapter. [1]

Option A (quick): map DateTime to String

apollo {
  service("service") {
    mapScalarToKotlinString("DateTime")
  }
}

(Example of mapping a date-time scalar to String is commonly used in Android integrations.) [2]

Option B (recommended): map DateTime to kotlinx.datetime.Instant

Apollo’s date/time adapters were moved out of the core artifacts in v4. [3] Add the adapters dependency, then map the scalar to Instant with the provided adapter:

dependencies {
  implementation("com.apollographql.adapters:apollo-adapters-datetime:<version>")
}

apollo {
  service("service") {
    mapScalar("DateTime", "kotlinx.datetime.Instant", "com.apollographql.adapters.InstantAdapter")
  }
}

Apollo’s release notes show the InstantAdapter class name for mapping to kotlinx.datetime.Instant, and Apollo’s KDoc notes older KotlinxInstantAdapter is deprecated in favor of the new adapters coordinates. [4][5]

Sources:
[1] Apollo Kotlin docs – Custom scalars (mapScalar, AnyAdapter) (apollographql.com)
[2] AWS Amplify Android example mapping a DateTime scalar to String (docs.amplify.aws)
[3] Apollo Kotlin docs – Migrating to v4 (artifacts moved; mapScalar) (apollographql.com)
[4] Apollo Kotlin GitHub releases (example mapping with com.apollographql.adapters.InstantAdapter) (github.com)
[5] Apollo Kotlin KDoc (older KotlinxInstantAdapter deprecated; new apollo-adapters-datetime coordinates) (apollographql.com)

Citations:


Add explicit DateTime scalar mapping to prevent runtime parsing errors.

The code assumes workoutTime serializes to ISO-8601 format via toString(), but Apollo 4.1.1 requires explicit mapScalar configuration for custom scalars like DateTime. Without it, the scalar defaults to AnyAdapter, making the runtime type and serialization behavior unpredictable. Instant.parse() will throw DateTimeParseException if toString() doesn't produce a parseable format.

Configure the DateTime scalar in app/build.gradle:

apollo {
  service("service") {
    packageName.set("com.cornellappdev.uplift")
    mapScalarToKotlinString("DateTime")  // Quick: maps to String
    // OR use apollo-adapters for better type safety:
    // mapScalar("DateTime", "kotlinx.datetime.Instant", "com.apollographql.adapters.InstantAdapter")
  }
}

Then update the mapping to use the properly typed value directly instead of calling toString().

🤖 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/ProfileRepository.kt`
around lines 47 - 53, The mapping of workoutFields.workoutTime into
Instant.parse(...) is unsafe because Apollo's DateTime scalar isn't configured
and may deserialize as Any; update your Apollo config in app/build.gradle to map
the DateTime scalar (e.g., use mapScalarToKotlinString("DateTime") for a quick
fix or mapScalar("DateTime", "kotlinx.datetime.Instant",
"com.apollographql.adapters.InstantAdapter") for proper typing), then change the
ProfileRepository mapping that builds workoutDomain (reference: workoutDomain,
WorkoutDomain, workoutFields.workoutTime, Instant.parse) to use the correctly
typed value provided by the scalar mapping instead of calling toString() and
parsing at runtime.


val weeklyDays = weeklyResponse.data?.getWeeklyWorkoutDays?.filterNotNull().orEmpty()

ProfileData(
name = user.name,
netId = user.netId,
encodedImage = user.encodedImage,
totalGymDays = user.totalGymDays,
activeStreak = user.activeStreak,
maxStreak = user.maxStreak,
streakStart = user.streakStart?.toString(),
workoutGoal = user.workoutGoal ?: 0,
workouts = workoutDomain,
weeklyWorkoutDays = weeklyDays
)
}.onFailure { e ->
Log.e("ProfileRepo", "Failed to load profile", e)
}

suspend fun setWorkoutGoal(goal: Int): Result<Unit> = runCatching {
val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull()
?: throw IllegalStateException("Missing user ID")

val response = apolloClient
.mutation(
SetWorkoutGoalsMutation(
userId = userId,
workoutGoal = goal
)
)
.execute()

if (response.hasErrors()) {
throw IllegalStateException("Goal update failed")
}
}.onFailure { e ->
Log.e("ProfileRepo", "Failed to update workout goal", e)
}
}
Loading
Loading