fix: extract Firebase auth calls into AuthRepository — LoginViewModel no longer depends on Firebase directly
This commit is contained in:
parent
370a56069f
commit
8217a3385d
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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<String> =
|
||||
withRateLimit(AuthRateLimiter.Flow.LOGIN) {
|
||||
runCatching { dataSource.signInWithGoogle(idToken) }
|
||||
}
|
||||
|
||||
override suspend fun signInAnonymously(): Result<String> =
|
||||
withRateLimit(AuthRateLimiter.Flow.ANONYMOUS) {
|
||||
runCatching { dataSource.signInAnonymously() }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface AuthRepository {
|
|||
val currentUserId: String?
|
||||
val currentUserEmail: String?
|
||||
val isSignedIn: Boolean
|
||||
suspend fun signInWithGoogle(idToken: String): Result<String>
|
||||
suspend fun signInAnonymously(): Result<String>
|
||||
suspend fun signInWithEmail(email: String, password: String): Result<String>
|
||||
suspend fun signUpWithEmail(email: String, password: String): Result<String>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<String>) {
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
viewModelScope.launch {
|
||||
|
|
|
|||
Loading…
Reference in New Issue