feat(auth): remove anonymous sign-in, add email verification flow (batch 0.3.0)
This commit is contained in:
parent
423081bdb2
commit
893c88d774
|
|
@ -23,6 +23,7 @@ class FirebaseAuthDataSource @Inject constructor() {
|
|||
val currentUserEmail: String? get() = auth.currentUser?.email
|
||||
val isSignedIn: Boolean get() = auth.currentUser != null
|
||||
val isAnonymous: Boolean get() = auth.currentUser?.isAnonymous ?: false
|
||||
val isEmailVerified: Boolean get() = auth.currentUser?.isEmailVerified ?: false
|
||||
val isGoogleAccount: Boolean
|
||||
get() = auth.currentUser?.providerData?.any { it.providerId == GoogleAuthProvider.PROVIDER_ID } == true
|
||||
|
||||
|
|
@ -42,13 +43,6 @@ class FirebaseAuthDataSource @Inject constructor() {
|
|||
auth.currentUser?.let { AuthState.Authenticated(it.uid, it.isAnonymous) }
|
||||
?: AuthState.Unauthenticated
|
||||
|
||||
suspend fun signInAnonymously(): String =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
auth.signInAnonymously()
|
||||
.addOnSuccessListener { cont.resume(it.user?.uid ?: "") }
|
||||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
}
|
||||
|
||||
suspend fun signInWithEmail(email: String, password: String): String =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
auth.signInWithEmailAndPassword(email, password)
|
||||
|
|
@ -63,6 +57,31 @@ class FirebaseAuthDataSource @Inject constructor() {
|
|||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
}
|
||||
|
||||
suspend fun sendEmailVerification(): Unit =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
val user = auth.currentUser
|
||||
if (user == null) {
|
||||
cont.resumeWithException(IllegalStateException("No signed-in user"))
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
user.sendEmailVerification()
|
||||
.addOnSuccessListener { cont.resume(Unit) }
|
||||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
}
|
||||
|
||||
/** Refreshes the cached FirebaseUser so [isEmailVerified] reflects server state. */
|
||||
suspend fun reloadUser(): Unit =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
val user = auth.currentUser
|
||||
if (user == null) {
|
||||
cont.resumeWithException(IllegalStateException("No signed-in user"))
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
user.reload()
|
||||
.addOnSuccessListener { cont.resume(Unit) }
|
||||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
}
|
||||
|
||||
suspend fun sendPasswordResetEmail(email: String): Unit =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
auth.sendPasswordResetEmail(email)
|
||||
|
|
|
|||
|
|
@ -21,17 +21,13 @@ class FirebaseAuthRepositoryImpl @Inject constructor(
|
|||
override val isSignedIn: Boolean get() = dataSource.isSignedIn
|
||||
override val isAnonymous: Boolean get() = dataSource.isAnonymous
|
||||
override val isGoogleAccount: Boolean get() = dataSource.isGoogleAccount
|
||||
override val isEmailVerified: Boolean get() = dataSource.isEmailVerified
|
||||
|
||||
override suspend fun signInWithGoogle(idToken: String): Result<GoogleSignInResult> =
|
||||
withRateLimit(AuthRateLimiter.Flow.LOGIN) {
|
||||
runCatching { dataSource.signInWithGoogle(idToken) }
|
||||
}
|
||||
|
||||
override suspend fun signInAnonymously(): Result<String> =
|
||||
withRateLimit(AuthRateLimiter.Flow.ANONYMOUS) {
|
||||
runCatching { dataSource.signInAnonymously() }
|
||||
}
|
||||
|
||||
override suspend fun signInWithEmail(email: String, password: String): Result<String> =
|
||||
withRateLimit(AuthRateLimiter.Flow.LOGIN) {
|
||||
runCatching { dataSource.signInWithEmail(email, password) }
|
||||
|
|
@ -42,6 +38,12 @@ class FirebaseAuthRepositoryImpl @Inject constructor(
|
|||
runCatching { dataSource.signUpWithEmail(email, password) }
|
||||
}
|
||||
|
||||
override suspend fun sendEmailVerification(): Result<Unit> =
|
||||
runCatching { dataSource.sendEmailVerification() }
|
||||
|
||||
override suspend fun reloadUser(): Result<Unit> =
|
||||
runCatching { dataSource.reloadUser() }
|
||||
|
||||
override suspend fun sendPasswordResetEmail(email: String): Result<Unit> =
|
||||
withRateLimit(AuthRateLimiter.Flow.PASSWORD_RESET) {
|
||||
runCatching { dataSource.sendPasswordResetEmail(email) }
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ interface AuthRepository {
|
|||
val isSignedIn: Boolean
|
||||
val isAnonymous: Boolean
|
||||
val isGoogleAccount: Boolean
|
||||
val isEmailVerified: Boolean
|
||||
suspend fun signInWithGoogle(idToken: String): Result<GoogleSignInResult>
|
||||
suspend fun signInAnonymously(): Result<String>
|
||||
suspend fun signInWithEmail(email: String, password: String): Result<String>
|
||||
suspend fun signUpWithEmail(email: String, password: String): Result<String>
|
||||
suspend fun sendEmailVerification(): Result<Unit>
|
||||
suspend fun reloadUser(): Result<Unit>
|
||||
suspend fun sendPasswordResetEmail(email: String): Result<Unit>
|
||||
suspend fun signOut()
|
||||
suspend fun reauthenticateWithEmail(email: String, password: String): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package app.closer.ui.auth
|
|||
|
||||
import app.closer.R
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.CustomCredential
|
||||
|
|
@ -31,7 +30,6 @@ import androidx.compose.material3.CircularProgressIndicator
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
|
|
@ -213,21 +211,6 @@ fun LoginScreen(
|
|||
}
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = viewModel::signInAnonymously,
|
||||
enabled = !state.isLoading,
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.62f),
|
||||
contentColor = AuthPrimaryDeep
|
||||
),
|
||||
border = BorderStroke(1.dp, AuthPrimaryDeep.copy(alpha = 0.28f))
|
||||
) {
|
||||
Text("Try without account", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
TextButton(onClick = { onNavigate(AppRoute.SIGN_UP) }) {
|
||||
|
|
|
|||
|
|
@ -58,10 +58,6 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun signInAnonymously() {
|
||||
attemptSignIn { authRepository.signInAnonymously() }
|
||||
}
|
||||
|
||||
fun reportError(message: String) = _uiState.update { it.copy(error = message) }
|
||||
|
||||
private fun attemptSignIn(action: suspend () -> Result<String>) {
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ fun SignUpScreen(
|
|||
},
|
||||
supportingText = {
|
||||
Text(
|
||||
"At least 6 characters",
|
||||
"At least 8 characters, with letters and numbers",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = AuthMuted
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,15 +37,21 @@ class SignUpViewModel @Inject constructor(
|
|||
|
||||
fun signUp() {
|
||||
val state = _uiState.value
|
||||
val pw = state.password
|
||||
when {
|
||||
state.email.isBlank() -> { _uiState.update { it.copy(error = "Please enter your email.") }; return }
|
||||
state.password.length < 6 -> { _uiState.update { it.copy(error = "Password must be at least 6 characters.") }; return }
|
||||
state.password != state.confirmPassword -> { _uiState.update { it.copy(error = "Passwords don't match.") }; return }
|
||||
pw.length < 8 -> { _uiState.update { it.copy(error = "Password must be at least 8 characters.") }; return }
|
||||
!pw.any { it.isLetter() } || !pw.any { it.isDigit() } -> { _uiState.update { it.copy(error = "Password must include both letters and numbers.") }; return }
|
||||
pw != state.confirmPassword -> { _uiState.update { it.copy(error = "Passwords don't match.") }; return }
|
||||
}
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
viewModelScope.launch {
|
||||
authRepository.signUpWithEmail(state.email.trim(), state.password)
|
||||
.onSuccess { _uiState.update { it.copy(isLoading = false, success = true) } }
|
||||
authRepository.signUpWithEmail(state.email.trim(), pw)
|
||||
.onSuccess {
|
||||
// Best-effort: send a verification email. Don't block account creation if it fails.
|
||||
authRepository.sendEmailVerification()
|
||||
_uiState.update { it.copy(isLoading = false, success = true) }
|
||||
}
|
||||
.onFailure { e ->
|
||||
val msg = friendlyError(e)
|
||||
_uiState.update { it.copy(isLoading = false, error = msg) }
|
||||
|
|
|
|||
Loading…
Reference in New Issue