Skip to main content
EV Sum 2 uses Firebase Authentication to provide secure user authentication with email and password. The authentication system supports user registration, login, password recovery, and session management.

Architecture

The authentication feature follows a layered architecture:
  • AuthService (services/AuthService.kt:7) - Service layer that handles business logic
  • AuthRepository (data/repositories/AuthRepository.kt:11) - Data layer that communicates with Firebase
  • UI Screens - Login, Register, and Recover screens for user interaction

Key features

Email & Password

Standard email and password authentication

Password Recovery

Email-based password reset functionality

Session Management

Real-time authentication state monitoring

Voice Input

Speech-to-text for email and password entry

Implementation

AuthService

The AuthService class provides the main authentication methods:
class AuthService(
    private val repo: AuthRepository = AuthRepository(),
) {
    suspend fun login(email: String, password: String) {
        try {
            repo.login(email, password)
        } catch (e: Exception) {
            throw mapFirebaseAuthError(e)
        }
    }

    suspend fun register(email: String, password: String) {
        try {
            repo.register(email, password)
        } catch (e: Exception) {
            throw mapFirebaseAuthError(e)
        }
    }

    suspend fun recover(email: String) {
        try {
            repo.sendPasswordReset(email)
        } catch (e: Exception) {
            throw mapFirebaseAuthError(e)
        }
    }

    fun logout() = repo.logout()
    fun isLoggedIn(): Boolean = repo.isLoggedIn()
    fun authStateFlow(): Flow<FirebaseUser?> = repo.authStateFlow()
    fun currentEmail(): String? = repo.currentEmail()
}

AuthRepository

The repository layer interacts directly with Firebase Authentication:
class AuthRepository(
    private val auth: FirebaseAuth = FirebaseAuth.getInstance(),
) {
    suspend fun login(email: String, password: String) {
        auth.signInWithEmailAndPassword(email, password).await()
        auth.currentUser != null
    }

    suspend fun register(email: String, password: String) {
        auth.createUserWithEmailAndPassword(email, password).await()
    }

    suspend fun sendPasswordReset(email: String) {
        auth.sendPasswordResetEmail(email).await()
    }

    fun logout() {
        FirebaseModule.auth.signOut()
    }

    fun authStateFlow(): Flow<FirebaseUser?> = callbackFlow {
        val listener = FirebaseAuth.AuthStateListener {
            fa -> trySend(fa.currentUser).isSuccess
        }
        auth.addAuthStateListener(listener)
        trySend(auth.currentUser)
        awaitClose { auth.removeAuthStateListener(listener) }
    }
}

Usage examples

val authService = AuthService()
val scope = rememberCoroutineScope()

Button(
    onClick = {
        scope.launch {
            try {
                authService.login(email, password)
                // Navigate to home screen
            } catch(e: Exception) {
                // Show error message
            }
        }
    }
) {
    Text("Login")
}

Login screen implementation

The login screen (ui/auth/LoginScreen.kt:71) demonstrates how to use the authentication service:
1

Initialize services

Create instances of AuthService and set up state management:
val authService = remember { AuthService() }
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
val scope = rememberCoroutineScope()
2

Validate input

Validate email format and ensure fields are not empty:
when {
    cleanEmail.isBlank() || password.isBlank() -> {
        message = "Complete your email and password"
        isError = true
    }
    !cleanEmail.isBasicEmailValid() -> {
        message = "Invalid email (e.g., [email protected])"
        isError = true
    }
    else -> {
        // Proceed with login
    }
}
3

Execute login

Call the authentication service in a coroutine:
scope.launch {
    try {
        authService.login(cleanEmail, password)
        message = null
        isError = false
        onLoginClick()
    } catch(e: Exception) {
        message = e.message ?: "Invalid credentials"
        isError = true
    }
}

Voice input integration

The login screen supports voice input for both email and password fields using speech recognition with Spanish normalization.
Voice input uses the Speech-to-Text feature with specialized normalization for authentication fields.
val speechController = remember { SpeechController(context) }
var dictationTarget by remember { mutableStateOf(DictationTarget.EMAIL) }

speechController.setListener(
    onPartial = { partial ->
        when (dictationTarget) {
            DictationTarget.EMAIL -> email = normalizeEmailFromSpeech(partial)
            DictationTarget.PASSWORD -> password = normalizePasswordFromSpeech(partial)
        }
    },
    onFinal = { final ->
        when (dictationTarget) {
            DictationTarget.EMAIL -> email = normalizeEmailFromSpeech(final)
            DictationTarget.PASSWORD -> password = normalizePasswordFromSpeech(final)
        }
    }
)

Error handling

The authentication service maps Firebase errors to user-friendly messages:
private fun mapFirebaseAuthError(e: Exception): Exception {
    val msg = e.message ?: "Authentication error"
    return Exception(msg)
}
Always handle authentication errors gracefully and provide clear feedback to users. Common errors include invalid credentials, network issues, and account already exists.

Session management

Check authentication state throughout your app:
val authService = AuthService()

// Check if user is logged in
if (authService.isLoggedIn()) {
    // Show authenticated content
} else {
    // Show login screen
}

// Get current user email
val email = authService.currentEmail()

// Logout
authService.logout()

Best practices

Always validate email format before attempting authentication:
if (!email.isBasicEmailValid()) {
    // Show validation error
    return
}
  • Minimum 6 characters (Firebase requirement)
  • Use PasswordVisualTransformation() for password fields
  • Never log or display passwords in plain text
Provide clear, actionable error messages:
  • “Complete your email and password” for empty fields
  • “Invalid email format” for malformed emails
  • “Invalid credentials” for authentication failures
Show loading indicators during authentication operations:
var isLoading by remember { mutableStateOf(false) }

scope.launch {
    isLoading = true
    try {
        authService.login(email, password)
    } finally {
        isLoading = false
    }
}

Speech-to-Text

Voice input for authentication fields

Phrase Management

User-specific data storage with Firestore

Build docs developers (and LLMs) love