Skip to content
Open
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
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.jetbrains.kotlin.serialization)
alias(libs.plugins.google.services)
}

android {
Expand Down Expand Up @@ -59,4 +60,9 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.material3.adaptive.navigation3)
implementation(libs.kotlinx.serialization.core)

// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.auth)
implementation(libs.play.services.auth)
}
9 changes: 9 additions & 0 deletions app/src/main/java/com/cornellappdev/chimes/GlobalState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.cornellappdev.chimes

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

object GlobalState {
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.

Instead of a global state we should use Repositories and ViewModels for screens to handle state management and business logic

var userName by mutableStateOf("Ithacan")
}
Comment on lines +7 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't use a process-local singleton as the logged-in user source of truth.

This state resets to "Ithacan" on process death/app relaunch, so the UI can drift from the actual signed-in user. For auth-backed data, please hydrate from an app-scoped ViewModel/repository tied to the real session instead of a global object.

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

In `@app/src/main/java/com/cornellappdev/chimes/GlobalState.kt` around lines 7 -
9, GlobalState.object exposes a process-local mutableStateOf("Ithacan")
(userName) which resets on process death; replace uses of GlobalState.userName
with an app-scoped solution: create an Application-scoped ViewModel/Repository
that reads the authenticated session (e.g., from your auth SDK or
DataStore/SavedStateHandle) and exposes a LiveData/StateFlow/MutableState for
the current user's display name; hydrate that state from the real session on app
start and observe it in UI composables instead of referencing
GlobalState.userName so the UI reflects the actual signed-in user across process
restarts.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.cornellappdev.chimes.GlobalState
import com.cornellappdev.chimes.R
import com.cornellappdev.chimes.ui.components.HeaderButton

Expand All @@ -45,7 +46,7 @@ fun HomeScreen () {
horizontalArrangement = Arrangement.spacedBy(40.dp)
) {
Text(
text = "Hi, Arielle ☀\uFE0F",
text = "Hi, ${GlobalState.userName.split(" ").firstOrNull() ?: ""} ☀️",
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.

We should ideally not be using global state for the user name. This should be something we handle in the HomeScreen ViewModel UiState

color = Color(0xFF4F4E4E),
modifier = Modifier.fillMaxHeight(),
fontWeight = FontWeight.SemiBold,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.cornellappdev.chimes.ui.screens

import android.annotation.SuppressLint
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
Expand Down Expand Up @@ -29,13 +32,21 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.cornellappdev.chimes.GlobalState
import com.cornellappdev.chimes.R
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.google.firebase.Firebase
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.auth
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

Expand All @@ -47,6 +58,9 @@ private val VeryLightGray = Color(0xFFBCB2B2)
fun Onboarding(
onLoginClick: () -> Unit = {}
) {
val context = LocalContext.current
val auth = remember { Firebase.auth }
Comment on lines +61 to +62
Copy link
Copy Markdown

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:

# First, find the Onboarding.kt file
fd -t f "Onboarding.kt" app/src/main/java

Repository: cuappdev/chimes-android

Length of output: 134


🏁 Script executed:

# Read the relevant section of Onboarding.kt, focusing on lines 1-120 to see the initialization and preview
cat -n app/src/main/java/com/cornellappdev/chimes/ui/screens/Onboarding.kt | head -130

Repository: cuappdev/chimes-android

Length of output: 6400


🏁 Script executed:

# Also check the OnboardingPreview function around line 373-376
cat -n app/src/main/java/com/cornellappdev/chimes/ui/screens/Onboarding.kt | tail -30

Repository: cuappdev/chimes-android

Length of output: 1234


🏁 Script executed:

# Check if there are any preview guards or LocalInspectionMode usage in this file
rg "LocalInspectionMode|@Preview" app/src/main/java/com/cornellappdev/chimes/ui/screens/Onboarding.kt -A 5 -B 2

Repository: cuappdev/chimes-android

Length of output: 135


🏁 Script executed:

# Check if Firebase is used elsewhere in preview composables to see the pattern
fd -t f "Preview.kt" app/src/main/java --exec grep -l "Firebase" {} \;

Repository: cuappdev/chimes-android

Length of output: 49


🏁 Script executed:

# Check for other preview implementations in the codebase that might use Firebase
rg "@Preview" app/src/main/java -A 5 | rg -B 5 "Firebase" | head -50

Repository: cuappdev/chimes-android

Length of output: 49


Guard Firebase initialization in preview mode.

OnboardingPreview() calls Onboarding() directly, so the Firebase.auth initialization at line 62 executes during preview rendering. Preview processes typically don't have a default FirebaseApp configured, which will break the preview. Add a preview guard (e.g., LocalInspectionMode.current) or inject the auth dependency instead.

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

In `@app/src/main/java/com/cornellappdev/chimes/ui/screens/Onboarding.kt` around
lines 61 - 62, The Firebase.auth initialization in Onboarding (val auth =
remember { Firebase.auth }) runs during previews and can crash
OnboardingPreview; guard this by checking LocalInspectionMode.current (or accept
an injected auth parameter) and only call Firebase.auth when not in inspection
mode — e.g., in Onboarding use a conditional/nullable auth (or a preview
FakeAuth) so OnboardingPreview can render without a FirebaseApp; update the
Onboarding signature or the remember block to reference
LocalInspectionMode.current and avoid calling Firebase.auth during preview.

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.

We should move all firebase authentication logic into some sort of Auth repository and then the viewmodel can make use of some of the defined repository functions and contain the ui states which the screen can then use

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.

We should also add dependency injection with Hilt and Hilt ViewModel ideally to this app and we can then inject the repository into the necessary viewmodels.

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.

You can take a look at some example android mvvm repositories for reference. Resell, hustle, uplift, etc. are some codebases you can maybe take a look at, but they're not perfect, so try not to blindly copy and make sure to do due research.


val bgAlpha = remember { Animatable(1f) }
val whiteAlpha = remember { Animatable(0f) }
val logoAlpha = remember { Animatable(0f) }
Expand Down Expand Up @@ -85,6 +99,26 @@ fun Onboarding(
hourHandRotation = 0f
}

val googleSignInLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
val account = task.getResult(ApiException::class.java)!!
val credential = GoogleAuthProvider.getCredential(account.idToken, null)
auth.signInWithCredential(credential).addOnCompleteListener {
if (it.isSuccessful) {
GlobalState.userName = it.result.user?.displayName ?: ""
onLoginClick()
} else {
Toast.makeText(context, "Firebase Authentication failed: ${it.exception?.message}", Toast.LENGTH_SHORT).show()
}
}
} catch (e: ApiException) {
Toast.makeText(context, "Google Sign-In failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
Comment on lines +102 to +120
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Treat a dismissed Google chooser as cancel, not auth failure.

If the user backs out of the account picker, this callback still tries to parse the intent and shows the failure toast. That turns a normal cancel path into an error state.

Suggested fix
+import android.app.Activity
+
     val googleSignInLauncher = rememberLauncherForActivityResult(
         contract = ActivityResultContracts.StartActivityForResult()
     ) { result ->
+        if (result.resultCode != Activity.RESULT_OK) {
+            return@rememberLauncherForActivityResult
+        }
         val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
         try {
             val account = task.getResult(ApiException::class.java)!!
📝 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
val googleSignInLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
val account = task.getResult(ApiException::class.java)!!
val credential = GoogleAuthProvider.getCredential(account.idToken, null)
auth.signInWithCredential(credential).addOnCompleteListener {
if (it.isSuccessful) {
GlobalState.userName = it.result.user?.displayName ?: ""
onLoginClick()
} else {
Toast.makeText(context, "Firebase Authentication failed: ${it.exception?.message}", Toast.LENGTH_SHORT).show()
}
}
} catch (e: ApiException) {
Toast.makeText(context, "Google Sign-In failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
import android.app.Activity
val googleSignInLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) {
return@rememberLauncherForActivityResult
}
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
val account = task.getResult(ApiException::class.java)!!
val credential = GoogleAuthProvider.getCredential(account.idToken, null)
auth.signInWithCredential(credential).addOnCompleteListener {
if (it.isSuccessful) {
GlobalState.userName = it.result.user?.displayName ?: ""
onLoginClick()
} else {
Toast.makeText(context, "Firebase Authentication failed: ${it.exception?.message}", Toast.LENGTH_SHORT).show()
}
}
} catch (e: ApiException) {
Toast.makeText(context, "Google Sign-In failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/chimes/ui/screens/Onboarding.kt` around
lines 102 - 120, The Google sign-in launcher (googleSignInLauncher) treats a
cancelled account picker as an auth failure because it always tries to parse the
intent; modify the ActivityResult callback to first check the result status and
data (e.g., if result.resultCode != Activity.RESULT_OK or result.data == null)
and treat that path as a cancel (no error toast, optionally return or call a
cancel handler), otherwise proceed to call
GoogleSignIn.getSignedInAccountFromIntent(result.data) and the existing
try/catch for ApiException and Firebase sign-in with
GoogleAuthProvider.getCredential; ensure you do not show the "Google Sign-In
failed" Toast when the user simply dismissed the chooser.


LaunchedEffect(Unit) {
delay(300)

Expand Down Expand Up @@ -172,7 +206,14 @@ fun Onboarding(
text = "Log-in with Google",
iconRes = R.drawable.ic_google_g,
contentDescription = "Google logo",
onClick = onLoginClick
onClick = {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(context.getString(R.string.default_web_client_id))
.requestEmail()
.build()
val client = GoogleSignIn.getClient(context, gso)
googleSignInLauncher.launch(client.signInIntent)
}
)

Spacer(modifier = Modifier.height(15.dp))
Expand Down Expand Up @@ -303,7 +344,7 @@ private fun ClockLogo(

translationX = (0.5f - pivotFraction) * size.width
translationY = (0.5f - pivotFraction) * size.height

rotationZ = hourHandRotation
transformOrigin = TransformOrigin(pivotFraction, pivotFraction)
}
Expand Down
7 changes: 7 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ nav3Core = "1.0.1"
lifecycleViewmodelNav3 = "2.11.0-alpha01"
kotlinxSerializationCore = "1.9.0"
material3AdaptiveNav3 = "1.3.0-alpha09"
firebaseBom = "34.12.0"
googleServices = "4.4.4"
playServicesAuth = "21.5.1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand All @@ -35,8 +38,12 @@ androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", vers
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-auth = { group = "com.google.firebase", name = "firebase-auth" }
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuth" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }