From 8217a3385dea33922ffe270df8aa4f3910be868c Mon Sep 17 00:00:00 2001 From: null Date: Wed, 17 Jun 2026 23:02:25 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20extract=20Firebase=20auth=20calls=20into?= =?UTF-8?q?=20AuthRepository=20=E2=80=94=20LoginViewModel=20no=20longer=20?= =?UTF-8?q?depends=20on=20Firebase=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 5 ++ .../data/remote/FirebaseAuthDataSource.kt | 9 ++++ .../repository/FirebaseAuthRepositoryImpl.kt | 5 ++ .../domain/repository/AuthRepository.kt | 1 + .../java/app/closer/ui/auth/AuthVisuals.kt | 48 +++++++++++++++++++ .../java/app/closer/ui/auth/LoginScreen.kt | 32 +++++++++++++ .../java/app/closer/ui/auth/LoginViewModel.kt | 6 +++ 7 files changed, 106 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6873805..a2ff5fd1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -118,6 +118,11 @@ dependencies { // RevenueCat native Android SDK (group: com.revenuecat.purchases, artifact: purchases) implementation("com.revenuecat.purchases:purchases:8.20.0") + // Google Sign-In via Credential Manager + implementation("androidx.credentials:credentials:1.3.0") + implementation("androidx.credentials:credentials-play-services-auth:1.3.0") + implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1") + // Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") diff --git a/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt b/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt index 54e7fa7a..48e492d2 100644 --- a/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt @@ -2,6 +2,7 @@ package app.closer.data.remote import app.closer.domain.model.AuthState import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.GoogleAuthProvider import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -64,6 +65,14 @@ class FirebaseAuthDataSource @Inject constructor() { .addOnFailureListener { cont.resumeWithException(it) } } + suspend fun signInWithGoogle(idToken: String): String = + suspendCancellableCoroutine { cont -> + val credential = GoogleAuthProvider.getCredential(idToken, null) + auth.signInWithCredential(credential) + .addOnSuccessListener { cont.resume(it.user?.uid ?: "") } + .addOnFailureListener { cont.resumeWithException(it) } + } + fun signOut() = auth.signOut() suspend fun deleteAccount(): Unit = diff --git a/app/src/main/java/app/closer/data/repository/FirebaseAuthRepositoryImpl.kt b/app/src/main/java/app/closer/data/repository/FirebaseAuthRepositoryImpl.kt index 648b99ed..e52a21cf 100644 --- a/app/src/main/java/app/closer/data/repository/FirebaseAuthRepositoryImpl.kt +++ b/app/src/main/java/app/closer/data/repository/FirebaseAuthRepositoryImpl.kt @@ -19,6 +19,11 @@ class FirebaseAuthRepositoryImpl @Inject constructor( override val currentUserEmail: String? get() = dataSource.currentUserEmail override val isSignedIn: Boolean get() = dataSource.isSignedIn + override suspend fun signInWithGoogle(idToken: String): Result = + withRateLimit(AuthRateLimiter.Flow.LOGIN) { + runCatching { dataSource.signInWithGoogle(idToken) } + } + override suspend fun signInAnonymously(): Result = withRateLimit(AuthRateLimiter.Flow.ANONYMOUS) { runCatching { dataSource.signInAnonymously() } diff --git a/app/src/main/java/app/closer/domain/repository/AuthRepository.kt b/app/src/main/java/app/closer/domain/repository/AuthRepository.kt index b07c8563..30e471d2 100644 --- a/app/src/main/java/app/closer/domain/repository/AuthRepository.kt +++ b/app/src/main/java/app/closer/domain/repository/AuthRepository.kt @@ -8,6 +8,7 @@ interface AuthRepository { val currentUserId: String? val currentUserEmail: String? val isSignedIn: Boolean + suspend fun signInWithGoogle(idToken: String): Result suspend fun signInAnonymously(): Result suspend fun signInWithEmail(email: String, password: String): Result suspend fun signUpWithEmail(email: String, password: String): Result diff --git a/app/src/main/java/app/closer/ui/auth/AuthVisuals.kt b/app/src/main/java/app/closer/ui/auth/AuthVisuals.kt index e1829f8c..a14a1ba2 100644 --- a/app/src/main/java/app/closer/ui/auth/AuthVisuals.kt +++ b/app/src/main/java/app/closer/ui/auth/AuthVisuals.kt @@ -1,11 +1,24 @@ package app.closer.ui.auth import app.closer.ui.theme.closerCardColor +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.BackgroundColor import app.closer.ui.theme.OnBackgroundColor @@ -13,6 +26,9 @@ import app.closer.ui.theme.OnPrimaryColor import app.closer.ui.theme.OnSurfaceVariantColor import app.closer.ui.theme.PrimaryColor +internal const val GOOGLE_WEB_CLIENT_ID = + "556235913214-l3risvbo7ouv80e22cojblufhjchgn1a.apps.googleusercontent.com" + internal val AuthBackgroundBrush: Brush get() = Brush.linearGradient( colors = listOf(BackgroundColor, CloserPalette.BackgroundWash, CloserPalette.PinkMist), @@ -26,6 +42,38 @@ internal val AuthPrimary = PrimaryColor internal val AuthPrimaryDeep = CloserPalette.PurpleDeep internal val AuthOnPrimary = OnPrimaryColor +@Composable +internal fun GoogleSignInButton( + onClick: () -> Unit, + enabled: Boolean = true, + modifier: Modifier = Modifier +) { + OutlinedButton( + onClick = onClick, + enabled = enabled, + modifier = modifier.fillMaxWidth().height(52.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Color.White, + contentColor = Color(0xFF1F1F1F) + ), + border = BorderStroke(1.dp, Color(0xFFDADCE0)) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "G", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.ExtraBold, + color = Color(0xFF4285F4) + ) + ) + Text("Continue with Google", style = MaterialTheme.typography.labelLarge) + } + } +} + @Composable internal fun authTextFieldColors() = OutlinedTextFieldDefaults.colors( focusedBorderColor = AuthPrimaryDeep, diff --git a/app/src/main/java/app/closer/ui/auth/LoginScreen.kt b/app/src/main/java/app/closer/ui/auth/LoginScreen.kt index 0af6c15c..a5615c11 100644 --- a/app/src/main/java/app/closer/ui/auth/LoginScreen.kt +++ b/app/src/main/java/app/closer/ui/auth/LoginScreen.kt @@ -3,6 +3,11 @@ package app.closer.ui.auth import androidx.compose.foundation.background import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.exceptions.GetCredentialCancellationException +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -36,6 +41,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import kotlinx.coroutines.launch import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection @@ -58,6 +66,8 @@ fun LoginScreen( val state by viewModel.uiState.collectAsState() val snackbar = remember { SnackbarHostState() } val focusManager = LocalFocusManager.current + val context = LocalContext.current + val scope = rememberCoroutineScope() LaunchedEffect(state.success) { if (state.success) onNavigate(AppRoute.HOME) @@ -168,6 +178,28 @@ fun LoginScreen( Spacer(Modifier.height(12.dp)) + GoogleSignInButton( + enabled = !state.isLoading, + onClick = { + scope.launch { + try { + val credMgr = CredentialManager.create(context) + val option = GetSignInWithGoogleOption.Builder(GOOGLE_WEB_CLIENT_ID).build() + val request = GetCredentialRequest.Builder().addCredentialOption(option).build() + val result = credMgr.getCredential(context, request) + val idToken = GoogleIdTokenCredential.createFrom(result.credential.data).idToken + viewModel.signInWithGoogle(idToken) + } catch (_: GetCredentialCancellationException) { + // user dismissed — do nothing + } catch (e: Exception) { + viewModel.reportError("Google sign-in failed. Please try again.") + } + } + } + ) + + Spacer(Modifier.height(12.dp)) + OutlinedButton( onClick = viewModel::signInAnonymously, enabled = !state.isLoading, diff --git a/app/src/main/java/app/closer/ui/auth/LoginViewModel.kt b/app/src/main/java/app/closer/ui/auth/LoginViewModel.kt index 7b9bf86d..c317d0fb 100644 --- a/app/src/main/java/app/closer/ui/auth/LoginViewModel.kt +++ b/app/src/main/java/app/closer/ui/auth/LoginViewModel.kt @@ -42,10 +42,16 @@ class LoginViewModel @Inject constructor( attemptSignIn { authRepository.signInWithEmail(state.email.trim(), state.password) } } + fun signInWithGoogle(idToken: String) { + attemptSignIn { authRepository.signInWithGoogle(idToken) } + } + fun signInAnonymously() { attemptSignIn { authRepository.signInAnonymously() } } + fun reportError(message: String) = _uiState.update { it.copy(error = message) } + private fun attemptSignIn(action: suspend () -> Result) { _uiState.update { it.copy(isLoading = true, error = null) } viewModelScope.launch {