Always follow the established architecture pattern:
- AuthBackend (Interface) → Platform-agnostic contract
- Platform Implementations → Android/iOS specific Firebase code
- AuthRepository → High-level API with validation
- UI Layer → ViewModels and Compose screens
- Keep platform-specific code in
androidMainandiosMain - Use
expect/actualpattern for platform abstraction - Never expose platform types in common code
- All platform implementations must implement the same interface
- Use
AuthErrorsealed class for all auth-related errors - Return
AuthResult<T>for operations that can fail - Map platform-specific errors to
AuthErrortypes - Never throw exceptions for expected auth failures
The library expects OAuth tokens to be obtained externally:
// ✅ Correct: Repository accepts tokens as strings
suspend fun signInWithGoogle(idToken: String): AuthResult<AuthUser>
// ❌ Wrong: Don't handle token acquisition in repository
suspend fun signInWithGoogle(activity: Activity): AuthResult<AuthUser>Rationale: Token acquisition requires platform-specific UI flows that vary by implementation.
Use StateFlow for reactive auth state:
// ✅ Correct: Expose auth state as StateFlow
interface AuthBackend {
val authState: StateFlow<AuthUser?>
}
// ✅ Correct: Collect in UI
val authState by authRepository.authState.collectAsState()
// ❌ Wrong: Don't poll for auth state
suspend fun getCurrentUser(): AuthUser?Validation belongs in AuthRepository, not AuthBackend:
// ✅ Correct: AuthRepository validates before delegating
suspend fun signInWithEmail(email: String, password: String): AuthResult<AuthUser> {
if (email.isBlank()) return AuthResult.Failure(AuthError.InvalidCredential)
if (password.length < 6) return AuthResult.Failure(AuthError.WeakPassword)
return backend.signInWithEmail(email, password)
}
// ❌ Wrong: Don't validate in AuthBackend
// Backend assumes inputs are pre-validatedPlatform implementations must map native errors to AuthError:
// ✅ Correct: Map Firebase errors to AuthError
catch (e: FirebaseAuthException) {
val error = when (e.errorCode) {
"ERROR_USER_NOT_FOUND" -> AuthError.UserNotFound
"ERROR_WRONG_PASSWORD" -> AuthError.InvalidCredential
"ERROR_EMAIL_ALREADY_IN_USE" -> AuthError.EmailAlreadyInUse
else -> AuthError.Unknown(e.message)
}
AuthResult.Failure(error)
}- Write test first that defines expected behavior
- Run test and verify it fails
- Implement minimum code to make test pass
- Refactor while keeping tests green
- Never commit code without passing tests
// ✅ Correct: Use FakeAuthBackend for testing
class AuthRepositoryTest {
private lateinit var fakeBackend: FakeAuthBackend
private lateinit var repository: AuthRepository
@BeforeTest
fun setup() {
fakeBackend = FakeAuthBackend()
repository = AuthRepository(fakeBackend)
}
@Test
fun `signInWithEmail should return success when valid credentials`() = runTest {
// Arrange
val email = "test@example.com"
val password = "password123"
fakeBackend.setAuthResult(AuthResult.Success(testUser))
// Act
val result = repository.signInWithEmail(email, password)
// Assert
assertTrue(result is AuthResult.Success)
}
}- All public methods in
AuthRepositorymust have tests - Both success and failure paths must be tested
- Edge cases (empty strings, null values, etc.) must be covered
- Error mapping must be verified
// ✅ Correct: Use expect/actual for platform auth backend
val appModule = module {
single<AuthBackend> { platformAuthBackend() }
single { AuthRepository(get()) }
factory { AuthViewModel(get()) }
}
// Platform-specific files
// androidMain
actual fun platformAuthBackend(): AuthBackend = AndroidFirebaseAuthBackend()
// iosMain
actual fun platformAuthBackend(): AuthBackend = IosFirebaseAuthBackend()- Use
singlefor stateful services (AuthBackend, AuthRepository) - Use
factoryfor ViewModels (new instance per injection) - Never create platform implementations directly in common code
- Always inject dependencies through constructor
// ✅ Correct: Set activity reference in onCreate
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityHolder.current = this
// ...
}
override fun onDestroy() {
super.onDestroy()
ActivityHolder.current = null
}
}// ✅ Correct: Use Firebase Auth directly
class AndroidFirebaseAuthBackend : AuthBackend {
private val auth = Firebase.auth
override suspend fun signInWithEmail(email: String, password: String) =
suspendCoroutine { continuation ->
auth.signInWithEmailAndPassword(email, password)
.addOnSuccessListener { /* ... */ }
.addOnFailureListener { /* ... */ }
}
}// ✅ Correct: Use notification bridge for iOS
class IosFirebaseAuthBackend : AuthBackend {
private val requestId = UUID.randomUUID().toString()
override suspend fun signInWithEmail(email: String, password: String) =
suspendCoroutine { continuation ->
// Post request notification
NSNotificationCenter.defaultCenter.postNotificationName(
name = "AuthRequest",
`object` = null,
userInfo = mapOf(
"requestId" to requestId,
"action" to "signInWithEmail",
"email" to email,
"password" to password
)
)
// Listen for response
observeAuthResponse(requestId, continuation)
}
}- Classes: PascalCase (e.g.,
AuthRepository,AuthBackend) - Functions: camelCase (e.g.,
signInWithEmail,resetPassword) - Properties: camelCase (e.g.,
authState,currentUser) - Constants: UPPER_SNAKE_CASE (e.g.,
PROVIDER_GOOGLE,MIN_PASSWORD_LENGTH)
// ✅ Correct: Use sealed classes for fixed hierarchies
sealed class AuthError {
data object InvalidCredential : AuthError()
data object EmailAlreadyInUse : AuthError()
data class Unknown(val message: String?) : AuthError()
}
// ✅ Correct: Use data classes for models
data class AuthUser(
val uid: String,
val email: String?,
val displayName: String?,
val photoUrl: String?,
val isEmailVerified: Boolean,
val isAnonymous: Boolean,
val providers: List<String>
)
// ✅ Correct: Use suspend functions for async operations
suspend fun signInWithEmail(email: String, password: String): AuthResult<AuthUser>
// ❌ Wrong: Don't use callbacks
fun signInWithEmail(email: String, password: String, callback: (AuthResult<AuthUser>) -> Unit)// ✅ Correct: Document public APIs
/**
* Signs in a user with email and password.
*
* @param email The user's email address (must be valid format)
* @param password The user's password (minimum 6 characters)
* @return AuthResult.Success with AuthUser or AuthResult.Failure with AuthError
*/
suspend fun signInWithEmail(email: String, password: String): AuthResult<AuthUser>// ❌ Wrong: Exposing Android types in common code
fun signInWithGoogle(context: Context): AuthResult<AuthUser>
// ✅ Correct: Keep platform types in platform code
fun signInWithGoogle(idToken: String): AuthResult<AuthUser>// ❌ Wrong: Throwing exceptions for auth failures
suspend fun signInWithEmail(email: String, password: String): AuthUser {
throw AuthException("Invalid credentials")
}
// ✅ Correct: Return AuthResult
suspend fun signInWithEmail(email: String, password: String): AuthResult<AuthUser> {
return AuthResult.Failure(AuthError.InvalidCredential)
}// ❌ Wrong: Blocking main thread
fun signInWithEmail(email: String, password: String): AuthUser {
return runBlocking { backend.signInWithEmail(email, password) }
}
// ✅ Correct: Use suspend functions
suspend fun signInWithEmail(email: String, password: String): AuthResult<AuthUser> {
return backend.signInWithEmail(email, password)
}// ❌ Wrong: Polling current user
fun isUserLoggedIn(): Boolean {
return getCurrentUser() != null
}
// ✅ Correct: Observe auth state
val authState: StateFlow<AuthUser?>-
Re-authentication Required: Some operations (email change, password change, account deletion) may fail with
AuthError.RequiresRecentLogin. Handle this by prompting user to re-authenticate. -
Account Linking: When linking accounts, ensure providers are not already linked. Use
AuthUser.providersto check existing providers. -
Email Verification: After sign-up, always prompt users to verify email. Some operations may require verified email.
-
Anonymous Account Conversion: When converting anonymous accounts, be aware that anonymous account will be deleted if conversion fails.
-
Provider Consistency: Use correct provider IDs:
- Google:
"google.com" - Apple:
"apple.com" - Facebook:
"facebook.com" - Email/Password:
"password"
- Google:
-
StateFlow over Callbacks: Use StateFlow for auth state to avoid memory leaks from unmanaged callbacks
-
Suspend Functions: Use suspend functions instead of blocking calls for better coroutine integration
-
Platform-Specific Optimization:
- Android: Reuse Firebase Auth instance
- iOS: Minimize notification overhead by batching requests when possible
-
Testing Performance: Keep tests fast by using FakeAuthBackend instead of real Firebase
- Never log sensitive data (passwords, tokens, email addresses in production)
- Validate all inputs in AuthRepository before passing to backend
- Handle token expiration gracefully
- Clear auth state on sign-out
- Use secure storage for refresh tokens (if implementing token management)
- Implement rate limiting for password reset and other sensitive operations
- Follow Firebase security best practices for configuration files