Phase 3A: Shared Authentication Foundation
Date: 2025-11-19 Status: Complete Week: 28
Overview
Phase 3A establishes the platform-agnostic authentication foundation for the Archery Apprentice iOS development. This phase creates shared authentication logic that works identically on both Android and iOS through Kotlin Multiplatform (KMP).
What Phase 3A Delivers:
- Domain models for authentication state and user data
- Repository pattern with Firebase Auth implementation
- Anonymous sign-in support (foundation for Phase 3B/3C)
- Cross-platform authentication using GitLive Firebase KMP SDK
- 100% shared authentication code (zero platform-specific auth logic)
Future Phases:
- Phase 3B: Google Sign-In on iOS
- Phase 3C: Apple Sign-In
- Phase 3D: Sign-out and account management
Architecture
High-Level Design
Phase 3A follows a clean architecture pattern with clear separation between domain, data, and platform layers:
┌─────────────────────────────────────────────────────┐
│ Platform Layer │
│ (Android Activity, iOS SwiftUI) │
│ │
│ Android: Composables observe AuthState │
│ iOS: SwiftUI views observe AuthState │
└─────────────────┬───────────────────────────────────┘
│
│ Flow<AuthState>
▼
┌─────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (Shared Presenters) │
│ │
│ AuthPresenter: Manages auth UI state │
│ Observes repository, emits UI-ready states │
└─────────────────┬───────────────────────────────────┘
│
│ Repository Interface
▼
┌─────────────────────────────────────────────────────┐
│ Domain Layer │
│ (shared:domain module) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Models: │ │
│ │ - AuthUser (domain user model) │ │
│ │ - AuthState (sealed class) │ │
│ │ - AuthResult (result wrapper) │ │
│ │ - AuthException (error types) │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Contracts: │ │
│ │ - AuthRepository (interface) │ │
│ └──────────────────────────────────────┘ │
└─────────────────┬───────────────────────────────────┘
│
│ Interface implementation
▼
┌─────────────────────────────────────────────────────┐
│ Data Layer │
│ (shared:data module) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ FirebaseAuthRepository │ │
│ │ (GitLive KMP SDK) │ │
│ │ │ │
│ │ - signInAnonymously() │ │
│ │ - observeAuthState() │ │
│ │ - getCurrentUser() │ │
│ │ - signOut() │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ AuthMappers │ │
│ │ (Firebase → Domain conversion) │ │
│ │ │ │
│ │ Firebase.User → AuthUser │ │
│ └──────────────────────────────────────┘ │
└─────────────────┬───────────────────────────────────┘
│
│ GitLive Firebase KMP
▼
┌─────────────────────────────────────────────────────┐
│ Firebase (Platform SDKs) │
│ │
│ Android: Firebase Android SDK │
│ iOS: Firebase iOS SDK (CocoaPods) │
│ │
│ Initialized per platform: │
│ - Android: Automatic (google-services.json) │
│ - iOS: Manual (FirebaseApp.configure()) │
└─────────────────────────────────────────────────────┘
Key Design Decisions
1. GitLive Firebase KMP SDK
Decision: Use GitLive Firebase KMP SDK for cross-platform Firebase access.
Rationale:
- Enables 100% shared authentication code
- Maintains access to full Firebase Auth feature set
- Wraps native Firebase SDKs (not a reimplementation)
- Proven in production (already used for Firestore in project)
Trade-offs:
- API differences from native Firebase SDKs (minor)
- Additional dependency (GitLive wrapper)
- Requires native Firebase initialization on each platform
Outcome: ✅ Successful - Already validated with Firestore integration in Week 28
2. Sealed Classes for State Management
Decision: Use Kotlin sealed classes for AuthState and AuthResult hierarchies.
Pattern:
sealed class AuthState {
data object Loading : AuthState()
data class Authenticated(val user: AuthUser) : AuthState()
data object Unauthenticated : AuthState()
data class Error(val exception: AuthException) : AuthState()
}
sealed class AuthResult {
data class Success(val user: AuthUser) : AuthResult()
data class Error(val exception: AuthException) : AuthResult()
}Benefits:
- Type-safe state representation
- Exhaustive
whenexpressions (compiler-enforced) - Clear API for consumers
- Easy to mock in tests
Outcome: Clean, self-documenting state management with compile-time safety
3. Repository Pattern
Decision: Abstract Firebase implementation behind AuthRepository interface in domain layer.
Pattern:
// Domain layer (interface)
interface AuthRepository {
fun observeAuthState(): Flow<AuthState>
suspend fun signInAnonymously(): AuthResult
suspend fun signOut(): AuthResult
}
// Data layer (implementation)
class FirebaseAuthRepository(
private val firebaseAuth: FirebaseAuth // GitLive SDK
) : AuthRepository {
// Implementation using GitLive Firebase KMP
}Benefits:
- Domain layer independent of Firebase
- Easy to mock for testing
- Future-proof (can swap implementations)
- Follows KMP best practices
Outcome: Clean separation of concerns, testable domain logic
4. Expect/Actual Pattern for Firebase Initialization
Decision: Use Kotlin’s expect/actual mechanism for platform-specific Firebase setup.
Pattern:
// commonMain (expect)
expect object FirebaseInitializer {
fun isInitialized(): Boolean
}
// androidMain (actual)
actual object FirebaseInitializer {
actual fun isInitialized(): Boolean {
return FirebaseApp.getApps().isNotEmpty()
}
}
// iosMain (actual)
actual object FirebaseInitializer {
actual fun isInitialized(): Boolean {
return FirebaseApp.app() != null
}
}Benefits:
- Platform-specific initialization logic encapsulated
- Shared code can verify Firebase readiness
- Type-safe platform abstractions
Outcome: Clean, maintainable platform initialization
Domain Layer Implementation
AuthUser Model
File: shared/domain/src/commonMain/kotlin/com/archeryapprentice/domain/models/auth/AuthUser.kt
/**
* Platform-agnostic user model.
* Represents an authenticated user across Android and iOS.
*/
data class AuthUser(
val uid: String,
val email: String?,
val displayName: String?,
val photoUrl: String?,
val isAnonymous: Boolean,
val emailVerified: Boolean
)Design notes:
- Minimal data class (no Firebase types)
- Nullable fields match Firebase Auth API
isAnonymousdistinguishes anonymous from authenticated users
AuthState Sealed Class
File: shared/domain/src/commonMain/kotlin/com/archeryapprentice/domain/models/auth/AuthState.kt
/**
* Authentication state hierarchy.
* Represents all possible authentication states in the app.
*/
sealed class AuthState {
/**
* Initial state or auth check in progress.
*/
data object Loading : AuthState()
/**
* User is authenticated (anonymous or with credentials).
*/
data class Authenticated(val user: AuthUser) : AuthState()
/**
* No user signed in.
*/
data object Unauthenticated : AuthState()
/**
* Authentication error occurred.
*/
data class Error(val exception: AuthException) : AuthState()
}Usage in UI:
// Android Composable
@Composable
fun AuthScreen(authState: AuthState) {
when (authState) {
is AuthState.Loading -> LoadingIndicator()
is AuthState.Authenticated -> HomeScreen(authState.user)
is AuthState.Unauthenticated -> SignInScreen()
is AuthState.Error -> ErrorScreen(authState.exception)
}
}// iOS SwiftUI
struct AuthView: View {
@State var authState: AuthState
var body: some View {
switch authState {
case .loading:
ProgressView()
case .authenticated(let user):
HomeView(user: user)
case .unauthenticated:
SignInView()
case .error(let exception):
ErrorView(exception: exception)
}
}
}AuthRepository Interface
File: shared/domain/src/commonMain/kotlin/com/archeryapprentice/domain/repositories/AuthRepository.kt
/**
* Authentication repository contract.
* Defines operations for user authentication.
*/
interface AuthRepository {
/**
* Observe authentication state changes.
* Emits whenever user signs in, signs out, or token refreshes.
*/
fun observeAuthState(): Flow<AuthState>
/**
* Sign in anonymously.
* Used for temporary access before full authentication.
*/
suspend fun signInAnonymously(): AuthResult
/**
* Get current authenticated user.
* Returns null if no user signed in.
*/
suspend fun getCurrentUser(): AuthUser?
/**
* Sign out current user.
*/
suspend fun signOut(): AuthResult
}Data Layer Implementation
FirebaseAuthRepository
File: shared/data/src/commonMain/kotlin/com/archeryapprentice/data/repositories/FirebaseAuthRepository.kt
/**
* Firebase authentication implementation using GitLive KMP SDK.
*/
class FirebaseAuthRepository(
private val firebaseAuth: FirebaseAuth // GitLive SDK
) : AuthRepository {
override fun observeAuthState(): Flow<AuthState> {
return firebaseAuth.authStateChanged
.map { firebaseUser ->
when {
firebaseUser == null -> AuthState.Unauthenticated
else -> AuthState.Authenticated(firebaseUser.toAuthUser())
}
}
.catch { exception ->
emit(AuthState.Error(AuthException.Unknown(exception)))
}
}
override suspend fun signInAnonymously(): AuthResult {
return try {
val authResult = firebaseAuth.signInAnonymously()
val user = authResult.user
?: return AuthResult.Error(AuthException.SignInFailed("No user returned"))
AuthResult.Success(user.toAuthUser())
} catch (e: Exception) {
AuthResult.Error(AuthException.SignInFailed(e.message ?: "Unknown error"))
}
}
override suspend fun getCurrentUser(): AuthUser? {
return firebaseAuth.currentUser?.toAuthUser()
}
override suspend fun signOut(): AuthResult {
return try {
firebaseAuth.signOut()
AuthResult.Success(null) // No user after sign out
} catch (e: Exception) {
AuthResult.Error(AuthException.SignOutFailed(e.message ?: "Unknown error"))
}
}
}Auth Mappers
File: shared/data/src/commonMain/kotlin/com/archeryapprentice/data/mappers/AuthMappers.kt
/**
* Extension function to convert Firebase User to domain AuthUser.
*/
fun FirebaseUser.toAuthUser(): AuthUser {
return AuthUser(
uid = uid,
email = email,
displayName = displayName,
photoUrl = photoURL,
isAnonymous = isAnonymous,
emailVerified = isEmailVerified
)
}Design notes:
- Simple mapping function (no complex logic)
- Handles nullable fields from Firebase
- Single responsibility (Firebase → Domain conversion)
Testing Strategy
Unit Tests
Domain layer tests:
- ✅ AuthState sealed class hierarchy (exhaustiveness)
- ✅ AuthUser data class validation
- ✅ AuthException creation and messages
Data layer tests:
- ✅ FirebaseAuthRepository with mocked GitLive SDK
- ✅ Auth mapper conversions (Firebase → Domain)
- ✅ Error handling (network failures, invalid credentials)
- ✅ State flow emissions (sign in, sign out, token refresh)
Test utilities:
MockAuthRepository- In-memory auth for UI testsTestAuthDataBuilder- Test data creation helpers
Integration Tests
Planned (Phase 3B):
- Firebase Emulator integration tests
- End-to-end authentication flows
- Multi-platform auth verification (Android + iOS)
Code Examples
Observing Auth State (Shared Presenter)
// Shared presenter (presentation layer)
class AuthPresenter(
private val authRepository: AuthRepository,
presenterScope: CoroutineScope
) {
private val _authState = MutableStateFlow<AuthState>(AuthState.Loading)
val authState: StateFlow<AuthState> = _authState.asStateFlow()
init {
presenterScope.launch {
authRepository.observeAuthState()
.collect { state ->
_authState.value = state
}
}
}
fun signInAnonymously() {
presenterScope.launch {
_authState.value = AuthState.Loading
when (val result = authRepository.signInAnonymously()) {
is AuthResult.Success -> {
// State will update via observeAuthState()
}
is AuthResult.Error -> {
_authState.value = AuthState.Error(result.exception)
}
}
}
}
}Android Composable
@Composable
fun SignInScreen(authPresenter: AuthPresenter) {
val authState by authPresenter.authState.collectAsState()
when (authState) {
is AuthState.Loading -> {
CircularProgressIndicator()
}
is AuthState.Unauthenticated -> {
Button(onClick = { authPresenter.signInAnonymously() }) {
Text("Sign In Anonymously")
}
}
is AuthState.Authenticated -> {
// Navigate to home screen
}
is AuthState.Error -> {
Text("Error: ${(authState as AuthState.Error).exception.message}")
}
}
}iOS SwiftUI View
import SwiftUI
import Shared // KMP framework
struct SignInView: View {
@StateObject private var authPresenter: AuthPresenter
var body: some View {
VStack {
// Observe Kotlin Flow as AsyncSequence
switch authPresenter.authState.value {
case is AuthStateLoading:
ProgressView()
case is AuthStateUnauthenticated:
Button("Sign In Anonymously") {
authPresenter.signInAnonymously()
}
case let authenticated as AuthStateAuthenticated:
Text("Signed in as: \(authenticated.user.displayName ?? "User")")
case let error as AuthStateError:
Text("Error: \(error.exception.message)")
}
}
}
}Lessons Learned
Agent Implementation Experience
Agent 1 (Domain Models):
- ✅ Sealed classes provide excellent type safety
- ✅ Minimal data classes reduce coupling
- ⚠️ AuthException hierarchy needs careful design (avoid over-engineering)
Agent 2 (Data Layer):
- ✅ GitLive SDK API is intuitive and well-documented
- ✅ Mapping Firebase → Domain is straightforward
- ⚠️ Error handling requires wrapping Firebase exceptions
- ⚠️
authStateChangedflow is hot (starts immediately)
Agent 3 (Testing):
- ✅ Repository interface makes mocking trivial
- ✅ Sealed classes enable exhaustive test coverage
- ⚠️ GitLive SDK requires mockk (mockito doesn’t support KMP well)
Common Pitfalls
1. Firebase initialization timing
- ❌ Problem: Accessing Firebase before initialization causes crash
- ✅ Solution: Verify
FirebaseInitializer.isInitialized()before repository usage
2. Flow collection lifecycle
- ❌ Problem:
authStateChangedcontinues emitting after sign-out - ✅ Solution: Cancel collection when presenter is destroyed (use
presenterScope)
3. Anonymous vs authenticated users
- ❌ Problem: Treating anonymous users as “not signed in”
- ✅ Solution: Use
isAnonymousfield to distinguish user types
Related Documentation
Main Repository Files
Phase 3A implementations:
shared/domain/models/auth/- Domain models (AuthUser, AuthState, etc.)shared/data/repositories/FirebaseAuthRepository.kt- Firebase implementationshared/data/mappers/AuthMappers.kt- Firebase → Domain mappersdocs/FIREBASE_AUTH_SETUP.md- Firebase configuration guide
Existing abstractions:
shared/domain/platform/AuthProvider.kt- Auth status interface (predates Phase 3A)
Vault Documentation
- ios-development-roadmap - Full iOS roadmap (Phases 1-10)
- ios-firebase-setup - Firebase setup guide (Week 28)
- kmp-architecture - KMP architecture overview
External Resources
Next Steps
Phase 3B: Google Sign-In on iOS
Deliverables:
- Google Sign-In implementation (iOS + Android)
- OAuth consent screen configuration
signInWithGoogle()method inAuthRepository- Google Sign-In button UI components
Estimated: 4-6 hours
Phase 3C: Apple Sign-In
Deliverables:
- Apple Sign-In implementation (iOS only)
- Apple Developer configuration
signInWithApple()method inAuthRepository- Apple Sign-In button (iOS native)
Estimated: 4-6 hours
Phase 3D: Sign-Out and Account Management
Deliverables:
- Account settings screen
- Sign-out functionality
- Account deletion (with Firebase cleanup)
- Profile management
Estimated: 6-8 hours
Document Version: 1.0 Created: 2025-11-19 Last Updated: 2025-11-19 Maintainer: Agent D (Documentation)