From 9b52e772c39b81bb7dc54da9e7944f728e35c262 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Fri, 14 Aug 2026 13:32:21 -0600 Subject: [PATCH 01/14] UPGRADE | #37 Build SetUp for Data layer and DI --- .gitignore | 1 + app/build.gradle.kts | 20 +++++++++++++------- app/src/main/AndroidManifest.xml | 1 + build.gradle.kts | 2 ++ core/data/build.gradle.kts | 19 +++++++++++++++++++ core/domain/build.gradle.kts | 2 ++ core/ui/build.gradle.kts | 5 +++++ features/auth/build.gradle.kts | 5 +++++ features/home/build.gradle.kts | 5 +++++ gradle.properties | 1 + gradle/libs.versions.toml | 19 +++++++++++++++++-- 11 files changed, 71 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 8773f8b..1fc891c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ replay_pid* .idea/ .gradle/ build/ +/app/google-services.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c321b8..5a73c28 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.ksp) + alias(libs.plugins.googleServices) } android { @@ -37,13 +39,6 @@ android { } } -// Add the new DSL here -kotlin { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - } -} - dependencies { implementation(project(path = ":core:domain")) implementation(project(path = ":core:data")) @@ -62,9 +57,20 @@ dependencies { implementation(libs.androidx.material3) implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.navigation.compose) + + // Dagger + implementation(libs.dagger) + ksp(libs.dagger.compiler) + + // Firebase + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.auth) + implementation(libs.firebase.analytics) + implementation(libs.gson) implementation(libs.material) testImplementation(libs.junit) + testImplementation(libs.mockk) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 509af97..ef03309 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> Date: Fri, 14 Aug 2026 13:55:51 -0600 Subject: [PATCH 02/14] NEW | #38 Dagger implementation - Scopes. - Modules. - Components. --- .../java/com/rho/studio/ui/di/AppComponent.kt | 45 ++++++++++ .../com/rho/studio/ui/di/ComponentManager.kt | 83 ++++++++---------- .../com/rho/studio/ui/di/UserComponent.kt | 40 +++++++++ .../studio/ui/core/data/di/CoreComponent.kt | 38 ++++++++ .../rho/studio/ui/core/data/di/CoreModule.kt | 87 +++++++++++++++++++ .../ui/core/ui/di/DaggerViewModelFactory.kt | 47 ++++++++++ .../com/rho/studio/ui/core/ui/di/Scopes.kt | 31 +++++++ .../com/rho/studio/ui/core/ui/di/UIModule.kt | 34 ++++++++ .../rho/studio/ui/core/ui/di/ViewModelKey.kt | 31 +++++++ .../studio/ui/features/auth/di/AuthModule.kt | 35 ++++++++ 10 files changed, 424 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/rho/studio/ui/di/AppComponent.kt rename core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt => app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt (51%) create mode 100644 app/src/main/java/com/rho/studio/ui/di/UserComponent.kt create mode 100644 core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreComponent.kt create mode 100644 core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreModule.kt create mode 100644 core/ui/src/main/java/com/rho/studio/ui/core/ui/di/DaggerViewModelFactory.kt create mode 100644 core/ui/src/main/java/com/rho/studio/ui/core/ui/di/Scopes.kt create mode 100644 core/ui/src/main/java/com/rho/studio/ui/core/ui/di/UIModule.kt create mode 100644 core/ui/src/main/java/com/rho/studio/ui/core/ui/di/ViewModelKey.kt create mode 100644 features/auth/src/main/java/com/rho/studio/ui/features/auth/di/AuthModule.kt diff --git a/app/src/main/java/com/rho/studio/ui/di/AppComponent.kt b/app/src/main/java/com/rho/studio/ui/di/AppComponent.kt new file mode 100644 index 0000000..b1eb9ef --- /dev/null +++ b/app/src/main/java/com/rho/studio/ui/di/AppComponent.kt @@ -0,0 +1,45 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: AppComponent.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-14 + * ============================================================================================== + * Description: Root Dagger component for the application. + * Responsible for bridging the core data layers with the UI layer and + * managing the application-wide dependency graph. + * ============================================================================================== + */ +package com.rho.studio.ui.di + +import com.rho.studio.ui.MainActivity +import com.rho.studio.ui.core.data.di.CoreComponent +import com.rho.studio.ui.core.ui.di.DaggerViewModelFactory +import com.rho.studio.ui.features.auth.di.AuthModule +import dagger.Component +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class AppScope + +@AppScope +@Component( + dependencies = [CoreComponent::class], + modules = [AuthModule::class, com.rho.studio.ui.core.ui.di.UIModule::class] +) +interface AppComponent { + fun inject(activity: MainActivity) + + // Exposed for UserComponent + fun coreComponent(): CoreComponent + + fun viewModelFactory(): DaggerViewModelFactory +} diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt b/app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt similarity index 51% rename from core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt rename to app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt index e774881..5ffa139 100644 --- a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt +++ b/app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt @@ -7,65 +7,54 @@ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ * * ============================================================================================== - * File: SessionRepository.kt + * File: ComponentManager.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-04 + * Date: 2026-08-14 * ============================================================================================== - * Description: Repository for persisting session data. + * Description: Centralized manager for the Dagger component hierarchy. + * Handles the lifecycle of global (App) and scoped (User) components. + * Enables session-based dependency injection by providing mechanisms to + * initialize and tear down the UserComponent upon login/logout. * ============================================================================================== */ -package com.rho.studio.ui.core.data.repository +package com.rho.studio.ui.di import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import com.google.gson.Gson -import com.rho.studio.ui.core.domain.model.User - -/** - * Interface defining the persistence operations for user sessions. - */ -interface SessionRepository { - suspend fun saveUser(user: User) - suspend fun getUser(): User? - suspend fun clearSession() -} - -/** - * Implementation of [SessionRepository] using SharedPreferences. - */ -class SessionRepositoryImpl(context: Context) : SessionRepository { - - private val preferences: SharedPreferences = - context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - - private val gson = Gson() - - companion object { - private const val PREFS_NAME = "session_prefs" - private const val PREF_KEY_USER = "pref_current_user" +import com.rho.studio.ui.core.data.di.CoreComponent +import com.rho.studio.ui.core.data.di.CoreModule +import com.rho.studio.ui.core.data.di.DaggerCoreComponent + +object ComponentManager { + + private lateinit var coreComponent: CoreComponent + private lateinit var appComponent: AppComponent + private var userComponent: UserComponent? = null + + fun init(context: Context) { + CoreModule.init(context) + coreComponent = DaggerCoreComponent.builder() + .build() + + appComponent = DaggerAppComponent.builder() + .coreComponent(coreComponent) + .build() } - override suspend fun saveUser(user: User) { - val userJson = gson.toJson(user) - preferences.edit { - putString(PREF_KEY_USER, userJson) - } - } + fun getAppComponent(): AppComponent = appComponent - override suspend fun getUser(): User? { - val userJson = preferences.getString(PREF_KEY_USER, null) ?: return null - return try { - gson.fromJson(userJson, User::class.java) - } catch (e: Exception) { - null + fun createUserComponent(): UserComponent { + if (userComponent == null) { + userComponent = DaggerUserComponent.builder() + .coreComponent(coreComponent) + .build() } + return userComponent!! } - override suspend fun clearSession() { - preferences.edit { - remove(PREF_KEY_USER) - } + fun destroyUserComponent() { + userComponent = null } + + fun getUserComponent(): UserComponent? = userComponent } diff --git a/app/src/main/java/com/rho/studio/ui/di/UserComponent.kt b/app/src/main/java/com/rho/studio/ui/di/UserComponent.kt new file mode 100644 index 0000000..aab6d48 --- /dev/null +++ b/app/src/main/java/com/rho/studio/ui/di/UserComponent.kt @@ -0,0 +1,40 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: UserComponent.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Dagger component defining the scope for authenticated user sessions. + * + * This component acts as the Single Source of Truth (SSOT) for the user's session + * lifecycle, managing the injection of dependencies that require a valid user + * context. It bridges core data layers with feature-specific modules, ensuring + * that sensitive user data is scoped correctly and cleared upon logout. + * + * Scope: @UserScope + * ============================================================================================== + */ +package com.rho.studio.ui.di + +import com.rho.studio.ui.core.data.di.CoreComponent +import com.rho.studio.ui.core.ui.di.DaggerViewModelFactory +import com.rho.studio.ui.core.ui.di.UserScope +import com.rho.studio.ui.features.home.di.HomeModule +import dagger.Component + +@UserScope +@Component( + dependencies = [CoreComponent::class], + modules = [HomeModule::class, com.rho.studio.ui.core.ui.di.UIModule::class] +) +interface UserComponent { + fun viewModelFactory(): DaggerViewModelFactory +} diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreComponent.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreComponent.kt new file mode 100644 index 0000000..9b2014a --- /dev/null +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreComponent.kt @@ -0,0 +1,38 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: CoreComponent.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Dagger component responsible for providing core infrastructure dependencies, + * including session management, authentication repositories, and application context. + * ============================================================================================== + */ +package com.rho.studio.ui.core.data.di + +import android.content.Context +import com.rho.studio.ui.core.data.manager.SessionManager +import com.rho.studio.ui.core.domain.repository.SessionRepository +import com.rho.studio.ui.core.domain.repository.AuthRepository +import com.rho.studio.ui.core.domain.usecase.SessionManagerInterface +import dagger.Component +import javax.inject.Singleton + +@Singleton +@Component(modules = [CoreModule::class]) +interface CoreComponent { + + fun context(): Context + fun sessionRepository(): SessionRepository + fun sessionManager(): SessionManager + fun sessionManagerInterface(): SessionManagerInterface + fun authRepository(): AuthRepository +} diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreModule.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreModule.kt new file mode 100644 index 0000000..f4a5ae7 --- /dev/null +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreModule.kt @@ -0,0 +1,87 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: CoreModule.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-14 + * ============================================================================================== + * Description: Dagger module responsible for providing core infrastructure dependencies. + * It centralizes the injection of data sources, repositories, and cross-cutting + * concerns like threading and analytics. + * + * Responsibilities: + * - Binding implementations to domain-level Repository interfaces (Auth and Session). + * - Providing Singleton instances of Firebase services (Auth, Analytics). + * - Managing global application context for core-level dependencies. + * - Defining standard CoroutineDispatchers for background operations. + * ============================================================================================== + */ +package com.rho.studio.ui.core.data.di + +import android.Manifest +import android.content.Context +import androidx.annotation.RequiresPermission +import com.rho.studio.ui.core.data.manager.SessionManager +import com.rho.studio.ui.core.data.repository.SessionRepositoryImpl +import com.rho.studio.ui.core.data.repository.AuthRepositoryImpl +import com.rho.studio.ui.core.domain.repository.SessionRepository +import com.rho.studio.ui.core.domain.repository.AuthRepository +import com.rho.studio.ui.core.domain.usecase.SessionManagerInterface +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.analytics.FirebaseAnalytics +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import dagger.Binds +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +abstract class CoreModule { + + @Binds + @Singleton + abstract fun bindSessionRepository(impl: SessionRepositoryImpl): SessionRepository + + @Binds + @Singleton + abstract fun bindAuthRepository(impl: AuthRepositoryImpl): AuthRepository + + @Binds + @Singleton + abstract fun bindSessionManagerInterface(impl: SessionManager): SessionManagerInterface + + companion object { + private var appContext: Context? = null + + fun init(context: Context) { + appContext = context.applicationContext + } + + @Provides + @Singleton + fun provideContext(): Context { + return appContext ?: throw IllegalStateException("CoreModule must be initialized with init(context)") + } + + @Provides + @Singleton + fun provideFirebaseAuth(): FirebaseAuth = FirebaseAuth.getInstance() + + @RequiresPermission(allOf = [Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.WAKE_LOCK]) + @Provides + @Singleton + fun provideFirebaseAnalytics(context: Context): FirebaseAnalytics = FirebaseAnalytics.getInstance(context) + + @Provides + @Singleton + fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO + } +} diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/DaggerViewModelFactory.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/DaggerViewModelFactory.kt new file mode 100644 index 0000000..cb4df94 --- /dev/null +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/DaggerViewModelFactory.kt @@ -0,0 +1,47 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: DaggerViewModelFactory.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: A custom ViewModelProvider.Factory that facilitates Dagger 2 multibinding. + * It acts as a bridge between Dagger's dependency injection container and + * Architecture Components' ViewModelStore, enabling constructor injection + * into ViewModels. + * ============================================================================================== + */ +package com.rho.studio.ui.core.ui.di + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider + +/** + * Standard ViewModelFactory for Dagger multibinding. + */ +class DaggerViewModelFactory @Inject constructor( + private val creators: Map, @JvmSuppressWildcards Provider> +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + val creator = creators[modelClass] ?: creators.entries.firstOrNull { + modelClass.isAssignableFrom(it.key) + }?.value ?: throw IllegalArgumentException("Unknown model class $modelClass") + + try { + @Suppress("UNCHECKED_CAST") + return creator.get() as T + } catch (e: Exception) { + throw RuntimeException(e) + } + } +} diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/Scopes.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/Scopes.kt new file mode 100644 index 0000000..8f9efe7 --- /dev/null +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/Scopes.kt @@ -0,0 +1,31 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: Scopes.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Defines custom Dagger scopes and qualifiers for dependency injection. + * Provides lifecycle management for components tied to specific app states, + * ensuring proper resource allocation and state persistence (e.g., User session) + * across the application architecture. + * ============================================================================================== + */ +package com.rho.studio.ui.core.ui.di + +import javax.inject.Scope +import javax.inject.Qualifier + +/** + * Scope for dependencies that should live as long as the user is authenticated. + */ +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class UserScope diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/UIModule.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/UIModule.kt new file mode 100644 index 0000000..c70f610 --- /dev/null +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/UIModule.kt @@ -0,0 +1,34 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: UIModule.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Dagger module for providing and binding UI-related dependencies. + * Manages ViewModel multi-bindings for the UI layer components. + * ============================================================================================== + */ +package com.rho.studio.ui.core.ui.di + +import androidx.lifecycle.ViewModel +import com.rho.studio.ui.core.ui.common.HeaderViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap + +@Module +abstract class UIModule { + + @Binds + @IntoMap + @ViewModelKey(HeaderViewModel::class) + abstract fun bindHeaderViewModel(viewModel: HeaderViewModel): ViewModel +} diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/ViewModelKey.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/ViewModelKey.kt new file mode 100644 index 0000000..cd9079c --- /dev/null +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/ViewModelKey.kt @@ -0,0 +1,31 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: ViewModelKey.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Custom annotation used for Dagger ViewModel multibinding. + * Maps ViewModel classes to their respective providers within a Dagger Map. + * ============================================================================================== + */ +package com.rho.studio.ui.core.ui.di + +import androidx.lifecycle.ViewModel +import dagger.MapKey +import kotlin.reflect.KClass + +/** + * MapKey for Dagger ViewModel multibinding. + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class ViewModelKey(val value: KClass) diff --git a/features/auth/src/main/java/com/rho/studio/ui/features/auth/di/AuthModule.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/di/AuthModule.kt new file mode 100644 index 0000000..d3a4c48 --- /dev/null +++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/di/AuthModule.kt @@ -0,0 +1,35 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: AuthModule.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Dagger module for the authentication feature. + * Provides and binds dependencies related to auth UI components and ViewModels. + * ============================================================================================== + */ +package com.rho.studio.ui.features.auth.di + +import androidx.lifecycle.ViewModel +import com.rho.studio.ui.core.ui.di.ViewModelKey +import com.rho.studio.ui.features.auth.LoginViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap + +@Module +abstract class AuthModule { + + @Binds + @IntoMap + @ViewModelKey(LoginViewModel::class) + abstract fun bindLoginViewModel(viewModel: LoginViewModel): ViewModel +} From 98cb7a6518c0b85f0abf4f6bf6dbcf67c8c5a4b0 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Fri, 14 Aug 2026 13:58:55 -0600 Subject: [PATCH 03/14] NEW | #38 Home Module --- .../studio/ui/features/home/di/HomeModule.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 features/home/src/main/java/com/rho/studio/ui/features/home/di/HomeModule.kt diff --git a/features/home/src/main/java/com/rho/studio/ui/features/home/di/HomeModule.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/di/HomeModule.kt new file mode 100644 index 0000000..8df6e3d --- /dev/null +++ b/features/home/src/main/java/com/rho/studio/ui/features/home/di/HomeModule.kt @@ -0,0 +1,35 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: HomeModule.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-14 + * ============================================================================================== + * Description: Dagger module responsible for defining dependency injections for the Home feature. + * Provides bindings for the HomeViewModel and other home-scoped dependencies. + * ============================================================================================== + */ +package com.rho.studio.ui.features.home.di + +import androidx.lifecycle.ViewModel +import com.rho.studio.ui.core.ui.di.ViewModelKey +import com.rho.studio.ui.features.home.HomeViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap + +@Module +abstract class HomeModule { + + @Binds + @IntoMap + @ViewModelKey(HomeViewModel::class) + abstract fun bindHomeViewModel(viewModel: HomeViewModel): ViewModel +} From 00e68a4cfeb6b65dd63a7e0ceda2fc644a4250bf Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Fri, 14 Aug 2026 14:08:40 -0600 Subject: [PATCH 04/14] NEW | #39 Data Layer definition - Remote data sources (firebase). - Repository implementations. --- .../ui/core/data/manager/SessionManager.kt | 122 ++++++------------ .../data/remote/AnalyticsRemoteDataSource.kt | 43 ++++++ .../data/remote/FirebaseRemoteDataSource.kt | 57 ++++++++ .../data/repository/AuthRepositoryImpl.kt | 37 ++---- .../data/repository/SessionRepositoryImpl.kt | 71 ++++++++++ .../domain/repository/SessionRepository.kt | 29 +++++ 6 files changed, 249 insertions(+), 110 deletions(-) create mode 100644 core/data/src/main/java/com/rho/studio/ui/core/data/remote/AnalyticsRemoteDataSource.kt create mode 100644 core/data/src/main/java/com/rho/studio/ui/core/data/remote/FirebaseRemoteDataSource.kt create mode 100644 core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepositoryImpl.kt create mode 100644 core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/SessionRepository.kt diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt index bfdde4c..46f7551 100644 --- a/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt @@ -10,110 +10,57 @@ * File: SessionManager.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-04 + * Date: 2026-08-12 * ============================================================================================== - * Description: Singleton orchestrator for authentication state and session lifecycle. - * Delegates persistence to a SessionRepository.kt and exposes reactive state - * via Kotlin Coroutines StateFlow for lifecycle-aware UI updates. + * Description: Orchestrator for authentication state and session lifecycle. + * Acts as the Single Source of Truth (SSOT) for the user's session. * ============================================================================================== */ package com.rho.studio.ui.core.data.manager -import android.content.Context +import com.rho.studio.ui.core.domain.model.SessionState import com.rho.studio.ui.core.domain.model.User -import com.rho.studio.ui.core.data.repository.SessionRepository -import com.rho.studio.ui.core.data.repository.SessionRepositoryImpl +import com.rho.studio.ui.core.domain.repository.SessionRepository import com.rho.studio.ui.core.domain.usecase.SessionManagerInterface import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import javax.inject.Singleton /** * # SessionManager * Implementation of SessionManagerInterface and SSOT for session state. */ -class SessionManager private constructor() : SessionManagerInterface { +@Singleton +class SessionManager @Inject constructor( + private val repository: SessionRepository, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : SessionManagerInterface { - companion object { + private val sessionScope = CoroutineScope(ioDispatcher + SupervisorJob()) - /** ensure that changes to the instance variable in a SessionManager are - * immediately visible to all threads, preventing issues caused - * by thread-local caching.*/ - @Volatile - private var instance: SessionManager? = null + private val _sessionState = MutableStateFlow(SessionState.Uninitialized) + val sessionState: StateFlow = _sessionState.asStateFlow() - /** Thread-safe singleton instance getter.*/ - fun getInstance(): SessionManager { - return instance ?: synchronized(this) { - instance ?: SessionManager().also { instance = it } - } - } - - /** - * Initialize the session manager with application context. - * Should be called in Application.onCreate() or MainActivity.onCreate(). - */ - fun init(context: Context) { - getInstance().initialize(SessionRepositoryImpl(context)) - } - - fun init(repository: SessionRepository) { - getInstance().initialize(repository) - } - } - - // ==================== PROPERTIES ==================== - - /**Instead of blocking the UI thread when saving - * to disk or simulating a network call, - * it uses a dedicated sessionScope*/ - private val sessionScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private lateinit var repository: SessionRepository - private var isInitialized = false - - /** # Private MutableStateFlow - * Only the SessionManager can modify the state*/ - private val _isAuthenticated = MutableStateFlow(false) - private val _currentUser = MutableStateFlow(null) private val _isLoading = MutableStateFlow(false) - private val _error = MutableStateFlow(null) - private val _isSessionChecked = MutableStateFlow(false) - - /** # PUBLIC StateFlow - * The rest of the app (UI/ViewModels) can only observe the state.*/ - val isAuthenticated: StateFlow = _isAuthenticated.asStateFlow() - val currentUser: StateFlow = _currentUser.asStateFlow() val isLoading: StateFlow = _isLoading.asStateFlow() - val error: StateFlow = _error.asStateFlow() - val isSessionChecked: StateFlow = _isSessionChecked.asStateFlow() - // ==================== INITIALIZATION ==================== - private fun initialize(repository: SessionRepository) { - if (isInitialized) return + private val _error = MutableStateFlow(null) + val error: StateFlow = _error.asStateFlow() - this.repository = repository - isInitialized = true + init { loadSavedSession() } - private fun checkInitialized() { - if (!isInitialized) { - throw IllegalStateException("SessionManager must be initialized with init(context) before use.") - } - } - override fun updateSession(user: User) { - checkInitialized() - _currentUser.value = user - _isAuthenticated.value = true + _sessionState.value = SessionState.Authenticated(user) saveUserSession(user) } override fun clearSession() { - checkInitialized() - _currentUser.value = null - _isAuthenticated.value = false + _sessionState.value = SessionState.Guest clearUserSession() } @@ -124,20 +71,18 @@ class SessionManager private constructor() : SessionManagerInterface { } private fun loadSavedSession() { + _sessionState.value = SessionState.Checking sessionScope.launch { try { - val user = repository.getUser() // persistence + val user = repository.getUser() if (user != null) { - _currentUser.value = user - _isAuthenticated.value = true + _sessionState.value = SessionState.Authenticated(user) } else { - _isAuthenticated.value = false + _sessionState.value = SessionState.Guest } } catch (e: Exception) { - _isAuthenticated.value = false + _sessionState.value = SessionState.Guest clearUserSession() - } finally { - _isSessionChecked.value = true } } } @@ -147,6 +92,7 @@ class SessionManager private constructor() : SessionManagerInterface { try { repository.saveUser(user) } catch (e: Exception) { + // Log error } } } @@ -156,12 +102,20 @@ class SessionManager private constructor() : SessionManagerInterface { try { repository.clearSession() } catch (e: Exception) { + // Log error } } } - fun isAuthenticatedSync(): Boolean = _isAuthenticated.value - fun getCurrentUserSync(): User? = _currentUser.value - fun clearError() { _error.value = null } - fun cleanup() { sessionScope.cancel() } + fun isAuthenticated(): Boolean = _sessionState.value is SessionState.Authenticated + fun getCurrentUser(): User? = (_sessionState.value as? SessionState.Authenticated)?.user + fun isSessionChecked(): Boolean = _sessionState.value !is SessionState.Uninitialized && _sessionState.value !is SessionState.Checking + + fun clearError() { + _error.value = null + } + + fun cleanup() { + sessionScope.cancel() + } } diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/remote/AnalyticsRemoteDataSource.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/AnalyticsRemoteDataSource.kt new file mode 100644 index 0000000..a4d0659 --- /dev/null +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/AnalyticsRemoteDataSource.kt @@ -0,0 +1,43 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: AnalyticsRemoteDataSource.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-13 + * ============================================================================================== + * Description: Firebase analytics data layer for decoupled event logging. + * ============================================================================================== + */ +package com.rho.studio.ui.core.data.remote + +import android.os.Bundle +import android.util.Log +import com.google.firebase.analytics.FirebaseAnalytics +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Wrapper around FirebaseAnalytics for decoupled event logging. + */ +@Singleton +class AnalyticsRemoteDataSource @Inject constructor( + private val firebaseAnalytics: FirebaseAnalytics +) { + fun logLogin(userId: String) { + Log.d("RHO_TELEMETRY", "Logging login event for user: $userId") + // Set the user identity for all future events in the session + firebaseAnalytics.setUserId(userId) + + val bundle = Bundle().apply { + putString(FirebaseAnalytics.Param.METHOD, "email_password") + } + firebaseAnalytics.logEvent(FirebaseAnalytics.Event.LOGIN, bundle) + } +} diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/remote/FirebaseRemoteDataSource.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/FirebaseRemoteDataSource.kt new file mode 100644 index 0000000..5ddcc4d --- /dev/null +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/FirebaseRemoteDataSource.kt @@ -0,0 +1,57 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: FirebaseRemoteDataSource.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-13 + * ============================================================================================== + * Description: Firebase remote data source. + * ============================================================================================== + */ +package com.rho.studio.ui.core.data.remote + +import com.google.firebase.auth.FirebaseAuth +import com.rho.studio.ui.core.domain.model.Credentials +import com.rho.studio.ui.core.domain.model.User +import kotlinx.coroutines.tasks.await +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Thread-safe wrapper around FirebaseAuth SDK. + */ +@Singleton +class FirebaseRemoteDataSource @Inject constructor( + private val firebaseAuth: FirebaseAuth, + private val analyticsDataSource: AnalyticsRemoteDataSource +) { + suspend fun login(credentials: Credentials): User { + val result = firebaseAuth.signInWithEmailAndPassword( + credentials.email, + credentials.password + ).await() + + val firebaseUser = result.user ?: throw Exception("Firebase user is null") + + analyticsDataSource.logLogin(firebaseUser.uid) + + return User( + id = firebaseUser.uid, + email = firebaseUser.email ?: credentials.email, + name = firebaseUser.displayName ?: extractNameFromEmail(firebaseUser.email ?: credentials.email) + ) + } + + private fun extractNameFromEmail(email: String): String { + return email.substringBefore("@") + .split(".", "_", "-") + .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + } +} diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt index 3d891da..2df6248 100644 --- a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt @@ -10,9 +10,9 @@ * File: AuthRepositoryImpl.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-04 + * Date: 2026-08-14 * ============================================================================================== - * Description: Repository for persisting session data. Current as a mock. + * Description: Repository for persisting session data. * ============================================================================================== */ package com.rho.studio.ui.core.data.repository @@ -20,33 +20,18 @@ package com.rho.studio.ui.core.data.repository import com.rho.studio.ui.core.domain.model.Credentials import com.rho.studio.ui.core.domain.model.User import com.rho.studio.ui.core.domain.repository.AuthRepository -import kotlinx.coroutines.delay +import com.rho.studio.ui.core.data.remote.FirebaseRemoteDataSource +import javax.inject.Inject +import javax.inject.Singleton /** - * Mock implementation of [AuthRepository] for development purposes. - * This will be replaced by Firebase Auth in the future. + * Production implementation of [AuthRepository] using Firebase Authentication. */ -class AuthRepositoryImpl : AuthRepository { +@Singleton +class AuthRepositoryImpl @Inject constructor( + private val firebaseDataSource: FirebaseRemoteDataSource +) : AuthRepository { override suspend fun login(credentials: Credentials): User { - // Simulated network delay - delay(1500) - - // Mocking authentication check - if (credentials.email == "error@rho.studio") { - throw RuntimeException("Network error") - } - - // Mock User creation - return User( - id = "user_${System.currentTimeMillis()}", - email = credentials.email, - name = extractNameFromEmail(credentials.email) - ) - } - - private fun extractNameFromEmail(email: String): String { - return email.substringBefore("@") - .split(".", "_", "-") - .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + return firebaseDataSource.login(credentials) } } diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepositoryImpl.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepositoryImpl.kt new file mode 100644 index 0000000..aebcc20 --- /dev/null +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepositoryImpl.kt @@ -0,0 +1,71 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: SessionRepositoryImpl.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Implementation of SessionRepository using Jetpack DataStore. + * ============================================================================================== + */ +package com.rho.studio.ui.core.data.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.google.gson.Gson +import com.rho.studio.ui.core.domain.model.User +import com.rho.studio.ui.core.domain.repository.SessionRepository +import kotlinx.coroutines.flow.first +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.dataStore: DataStore by preferencesDataStore(name = "session_prefs") + +/** + * Implementation of [SessionRepository] using Preferences DataStore. + */ +@Singleton +class SessionRepositoryImpl @Inject constructor( + private val context: Context +) : SessionRepository { + + private val gson = Gson() + + companion object { + private val PREF_KEY_USER = stringPreferencesKey("pref_current_user") + } + + override suspend fun saveUser(user: User) { + val userJson = gson.toJson(user) + context.dataStore.edit { preferences -> + preferences[PREF_KEY_USER] = userJson + } + } + + override suspend fun getUser(): User? { + val preferences = context.dataStore.data.first() + val userJson = preferences[PREF_KEY_USER] ?: return null + return try { + gson.fromJson(userJson, User::class.java) + } catch (e: Exception) { + null + } + } + + override suspend fun clearSession() { + context.dataStore.edit { preferences -> + preferences.remove(PREF_KEY_USER) + } + } +} diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/SessionRepository.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/SessionRepository.kt new file mode 100644 index 0000000..2ebc3da --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/SessionRepository.kt @@ -0,0 +1,29 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: SessionRepository.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-12 + * ============================================================================================== + * Description: Contract for session persistence operations. + * ============================================================================================== + */ +package com.rho.studio.ui.core.domain.repository + +import com.rho.studio.ui.core.domain.model.User + +/** + * Interface defining the persistence operations for user sessions. + */ +interface SessionRepository { + suspend fun saveUser(user: User) + suspend fun getUser(): User? + suspend fun clearSession() +} From e418a8263b42f629d10e3e9cd42bd415765985e5 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Mon, 17 Aug 2026 18:21:45 -0600 Subject: [PATCH 05/14] CHANGE | #38 AppScope: MainActivity.kt and DI - RhoStudioUIApp.kt : Init firebase and Dagger ComponentManager. - MainActivity.kt : DI configuration and session based navigation. --- .../java/com/rho/studio/ui/MainActivity.kt | 95 ++++++++++++++----- .../java/com/rho/studio/ui/RhoStudioUIApp.kt | 33 +++++++ .../ui/core/domain/model/Credentials.kt | 30 ++---- 3 files changed, 108 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt diff --git a/app/src/main/java/com/rho/studio/ui/MainActivity.kt b/app/src/main/java/com/rho/studio/ui/MainActivity.kt index cda9f57..0820aaf 100644 --- a/app/src/main/java/com/rho/studio/ui/MainActivity.kt +++ b/app/src/main/java/com/rho/studio/ui/MainActivity.kt @@ -10,17 +10,17 @@ * File: MainActivity.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-04 + * Date: 2026-08-17 * ========================================================================== * Description: * The primary entry point for the RHO Studio application, migrated to * pure Jetpack Compose. * Screen Assembly: Built LoginScreen and HomeScreen to unify the components. * Main Entry Point: Migrated MainActivity to ComponentActivity. - * Compose Navigation: Implemented a NavHost in MainActivity to handle routing - * based on SessionManager state, replacing nav_graph.xml. - * State Management: Switched state observation from LiveData to StateFlow - * using collectAsState() for better compatibility with Compose. + * Navigation: Implemented NavHost for routing based on SessionManager state. + * Dagger Scoping: Orchestrates UserComponent lifecycle, ensuring data cleanup + * on logout via ComponentManager. + * State Management: Uses StateFlow with collectAsState() for Compose compatibility. * ========================================================================== */ package com.rho.studio.ui @@ -38,41 +38,42 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.rho.studio.ui.core.data.manager.SessionManager +import com.rho.studio.ui.core.domain.model.SessionState +import com.rho.studio.ui.core.ui.common.HeaderViewModel import com.rho.studio.ui.features.auth.LoginScreen import com.rho.studio.ui.features.auth.LoginViewModel import com.rho.studio.ui.features.home.HomeScreen import com.rho.studio.ui.features.home.HomeViewModel import com.rho.studio.ui.core.ui.theme.UITheme +import com.rho.studio.ui.di.ComponentManager import kotlinx.coroutines.launch +import javax.inject.Inject class MainActivity : ComponentActivity() { - private lateinit var sessionManager: SessionManager - private lateinit var loginViewModel: LoginViewModel - private lateinit var homeViewModel: HomeViewModel - + /** + * # Dependency Injection Integration + * Field injection of the SessionManager SSOT. This removes manual singleton access + * and ensures the class is provisioned by the Dagger CoreComponent. + */ + @Inject + lateinit var sessionManager: SessionManager + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initializeManagers() - setContent { - UITheme { - MainContent() - } - } - } - - private fun initializeManagers() { - SessionManager.init(applicationContext) - sessionManager = SessionManager.getInstance() - loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] - homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java] + // Bootstrapping: Connects the Activity to the Dagger dependency graph. + ComponentManager.getAppComponent().inject(this) + /** + * # Reactive Error Feedback + * Globally collects infrastructure errors and displays them as Toasts, + * regardless of the current navigation destination. + */ lifecycleScope.launch { sessionManager.error.collect { error -> error?.let { @@ -81,20 +82,42 @@ class MainActivity : ComponentActivity() { } } } + + setContent { + UITheme { + MainContent() + } + } } @Composable private fun MainContent() { val navController = rememberNavController() - val isSessionChecked by sessionManager.isSessionChecked.collectAsState() - val isAuthenticated by sessionManager.isAuthenticated.collectAsState() + + /** + * # Sealed State Management + * Uses a sealed class (SessionState) instead of simple booleans to prevent + * illegal UI states and ensure the UI is always a reflection of the session truth. + */ + val sessionState by sessionManager.sessionState.collectAsState() val isLoading by sessionManager.isLoading.collectAsState() + val isSessionChecked = sessionState !is SessionState.Uninitialized && sessionState !is SessionState.Checking + val isAuthenticated = sessionState is SessionState.Authenticated + if (!isSessionChecked) { LoadingScreen() return } + /** + * # Reactive ViewModel Provisioning + * Utilizes Compose-native viewModel() pattern to avoid race conditions. + * LoginViewModel is sourced from the persistent AppScope. + */ + val appFactory = ComponentManager.getAppComponent().viewModelFactory() + val loginViewModel: LoginViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = appFactory) + LaunchedEffect(isAuthenticated) { if (isAuthenticated) { navController.navigate("home") { @@ -102,6 +125,14 @@ class MainActivity : ComponentActivity() { } } else { loginViewModel.resetForm() + + /** + * # Secure Session Isolation + * Atomically destroys the authenticated dependency graph on logout, + * ensuring PII (Personally Identifiable Information) is binary-purged from memory. + */ + ComponentManager.destroyUserComponent() + navController.navigate("login") { popUpTo("home") { inclusive = true } } @@ -117,7 +148,19 @@ class MainActivity : ComponentActivity() { LoginScreen(viewModel = loginViewModel) } composable("home") { - HomeScreen(homeViewModel = homeViewModel) + /** + * # Tiered DI Scoping + * Home and Header ViewModels are provided by the dynamic UserComponent, + * which only exists while the user is actively authenticated. + */ + val userFactory = ComponentManager.createUserComponent().viewModelFactory() + val homeViewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = userFactory) + val headerViewModel: HeaderViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = userFactory) + + HomeScreen( + homeViewModel = homeViewModel, + headerViewModel = headerViewModel + ) } } diff --git a/app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt b/app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt new file mode 100644 index 0000000..3177fba --- /dev/null +++ b/app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt @@ -0,0 +1,33 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ========================================================================== + * File: RhoStudioUIApp.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-17 + * ========================================================================== + * Description: + * Main Application class for RhoStudio UI. Responsible for initializing + * core services including Firebase and the dependency injection framework. + * ========================================================================== + */ +package com.rho.studio.ui + +import android.app.Application +import com.google.firebase.FirebaseApp +import com.rho.studio.ui.di.ComponentManager + +class RhoStudioUIApp : Application() { + + override fun onCreate() { + super.onCreate() + FirebaseApp.initializeApp(this) + ComponentManager.init(this) + } +} diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt index b7e81a2..bee33f1 100644 --- a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt @@ -10,36 +10,18 @@ * File: Credentials.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-05 + * Date: 2026-08-12 * ============================================================================================== * Description: core:domain - * Represents the user's authentication data and provides validation logic. + * Represents the user's authentication data as standard String values. * ============================================================================================== */ package com.rho.studio.ui.core.domain.model /** - * Represents the user's authentication data and provides validation logic. + * Represents the user's authentication data. */ data class Credentials( - var email: String = "", - var password: String = "" -) { - companion object { - private val EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex() - } - - val isEmailValid: Boolean - get() = email.isNotBlank() && email.matches(EMAIL_REGEX) - - val isPasswordValid: Boolean - get() = password.length >= 6 - - val isValid: Boolean - get() = isEmailValid && isPasswordValid - - fun clear() { - email = "" - password = "" - } -} + val email: String, + val password: String +) From 9286213635b796bc12872bb8c06176a5eb3b281c Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 13:19:43 -0600 Subject: [PATCH 06/14] CHANGE | #38 LoginUseCase.kt DI setup --- .../studio/ui/core/domain/usecase/LoginUseCase.kt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt index bb2af5e..dbc35af 100644 --- a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt @@ -10,7 +10,7 @@ * File: LoginUseCase.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-06 + * Date: 2026-08-18 * ============================================================================================== * Description: Orchestrates the authentication process by validating user credentials, * interacting with the AuthRepository to verify identity, and updating the @@ -22,23 +22,20 @@ package com.rho.studio.ui.core.domain.usecase import com.rho.studio.ui.core.domain.model.Credentials import com.rho.studio.ui.core.domain.model.User import com.rho.studio.ui.core.domain.repository.AuthRepository +import javax.inject.Inject /*** Encapsulates the login business transaction.*/ -class LoginUseCase( +class LoginUseCase @Inject constructor( private val authRepository: AuthRepository, private val sessionManager: SessionManagerInterface ) : BaseUseCase() { override suspend fun execute(parameters: Credentials): User { - // 1. Validate input using the Credentials domain model. - if (!parameters.isValid) { - throw IllegalArgumentException("Invalid credentials") - } - - // 2. Authentication performed by Repository + // 1. Authentication performed by Repository + // Note: parameters (Credentials) are already validated via Value Objects val user = authRepository.login(parameters) - // 3. Update global session state + // 2. Update global session state sessionManager.updateSession(user) return user From 2b3cc425510241c66226f6dcba239e4c7ecf8ae5 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 13:21:20 -0600 Subject: [PATCH 07/14] CHANGE | #38 LogoutUseCase.kt DI setup --- .../com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt index 0005cf1..518705e 100644 --- a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt @@ -10,7 +10,7 @@ * File: LogoutUseCase.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-06 + * Date: 2026-08-18 * ============================================================================================== * Description: Handles the termination of the user session by clearing authentication tokens, * resetting global application state, and ensuring secure cleanup of @@ -19,8 +19,10 @@ */ package com.rho.studio.ui.core.domain.usecase +import javax.inject.Inject + /*** Encapsulates the logout business transaction.*/ -class LogoutUseCase( +class LogoutUseCase @Inject constructor( private val sessionManager: SessionManagerInterface ) : BaseUseCase() { From e6581ff14d15dae22001a9f3ee499717bbb4f7f4 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 17:25:31 -0600 Subject: [PATCH 08/14] CHANGE | #38 LoginViewModel.kt DI setup --- .../studio/ui/features/auth/LoginViewModel.kt | 115 ++++++------------ 1 file changed, 38 insertions(+), 77 deletions(-) diff --git a/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt index 4995dc4..82d77c6 100644 --- a/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt +++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt @@ -10,46 +10,11 @@ * File: LoginViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-06 + * Date: 2026-08-18 * ============================================================================ * Description: * The LoginViewModel manages the state and business logic for the - * Authentication screen, acting as the UI Layer in a pure - * Jetpack Compose environment. - * It leverages the Rho Studio BaseViewModel architecture to handle - * user input validation, asynchronous login requests via LoginUseCase, - * and reactive UI states using both Compose State and StateFlow. - * - * •Extends: com.rho.studio.ui.core.ui.base.BaseViewModel - * •Dependencies:s - * •LoginUseCase: Orchestrates the login flow through the Repository. - * •Credentials: A data model encapsulating email and password logic. - * - * Core Logic Flows - * State-Driven Input - * •email / password: Uses Compose `mutableStateOf` to provide - * immediate, observable reactivity for the UI layer. - * Real-time & Debounced Validation - * •onEmailChanged() / onPasswordChanged(): - * Triggered on every keystroke, updating the state immediately. - * •300ms Debounce: Logic moved from Fragments to the ViewModel, - * ensuring validation is only performed after the user pauses typing. - * Authentication Process - * •Trigger: onLoginClick() performs final validation and guards - * against concurrent attempts using the base loading state. - * •Execution: performLogin() utilizes launchWithLoading() to - * automatically manage the UI loading state and error trapping. - * •UseCase: Executes loginUseCase(credentials) within a managed coroutine. - * •Result Handling: - * •Success: Sets success toast; navigation is handled via SessionManager state. - * •Failure: Customizes error messages via the handleError() hook. - * Layering & Architecture - * •Job Management: - * Relies on BaseViewModel's automated job tracking and cleanup - * to prevent memory leaks without manual cancellation logic. - * •State Reset: - * resetForm() provides a clean, secure slate for the UI by - * clearing Compose states and the underlying model. + * Authentication screen, utilizing reactive validation and Firebase. * ============================================================================ */ package com.rho.studio.ui.features.auth @@ -62,92 +27,89 @@ import com.rho.studio.ui.core.ui.base.BaseViewModel import com.rho.studio.ui.core.domain.model.Credentials import com.rho.studio.ui.core.domain.model.Result import com.rho.studio.ui.core.domain.usecase.LoginUseCase -import com.rho.studio.ui.core.data.manager.SessionManager -import com.rho.studio.ui.core.data.repository.AuthRepositoryImpl import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds -class LoginViewModel : BaseViewModel() { - // ==================== DEPENDENCIES ==================== - private val loginUseCase = LoginUseCase(AuthRepositoryImpl(), SessionManager.getInstance()) +class LoginViewModel @Inject constructor( + private val loginUseCase: LoginUseCase +) : BaseViewModel() { + private var loginJob: Job? = null - private var emailDebounceJob: Job? = null - private var passwordDebounceJob: Job? = null + private var validationJob: Job? = null + // ==================== FORM STATE ==================== var email by mutableStateOf("") private set var password by mutableStateOf("") private set - val credentials = Credentials() + // ==================== UI STATE ==================== private val _emailError = MutableStateFlow(null) val emailError: StateFlow = _emailError.asStateFlow() + private val _passwordError = MutableStateFlow(null) val passwordError: StateFlow = _passwordError.asStateFlow() + private val _isFormValid = MutableStateFlow(false) val isFormValid: StateFlow = _isFormValid.asStateFlow() - // ==================== FORM VALIDATION ==================== + companion object { + private val EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex() + } + + // ==================== ACTIONS ==================== fun onEmailChanged(email: String) { this.email = email - credentials.email = email - - emailDebounceJob?.cancel() - emailDebounceJob = viewModelScope.launch { - delay(300.milliseconds) - validateEmail() - validateForm() - } + triggerValidation() } fun onPasswordChanged(password: String) { this.password = password - credentials.password = password - - passwordDebounceJob?.cancel() - passwordDebounceJob = viewModelScope.launch { + triggerValidation() + } + + private fun triggerValidation() { + validationJob?.cancel() + validationJob = viewModelScope.launch { delay(300.milliseconds) - validatePassword() - validateForm() + validate() } } - private fun validateEmail() { + private fun validate(): Boolean { + val emailValid = email.isNotBlank() && email.matches(EMAIL_REGEX) + _emailError.value = when { - credentials.email.isBlank() -> "Email is required" - !credentials.isEmailValid -> "Please enter a valid email address" + email.isBlank() -> "Email is required" + !emailValid -> "Please enter a valid email address" else -> null } - } - private fun validatePassword() { _passwordError.value = when { - credentials.password.isBlank() -> "Password is required" - !credentials.isPasswordValid -> "Password must be at least 6 characters" + password.isBlank() -> "Password is required" else -> null } - } - private fun validateForm() { - _isFormValid.value = credentials.isValid + val isValid = emailValid //&& passwordValid + _isFormValid.value = isValid + return isValid } fun onLoginClick() { if (isLoading.value) return - if (!credentials.isValid) { - validateEmail() - validatePassword() - return + + if (validate()) { + performLogin(Credentials(email, password)) } - performLogin() } - private fun performLogin() { + private fun performLogin(credentials: Credentials) { loginJob = launchWithLoading( block = { when (val result = loginUseCase(credentials)) { @@ -167,7 +129,6 @@ class LoginViewModel : BaseViewModel() { fun resetForm() { email = "" password = "" - credentials.clear() _emailError.value = null _passwordError.value = null _isFormValid.value = false From d67e147a3067fb37a6e6a5f0a79a195525d66f78 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 17:47:28 -0600 Subject: [PATCH 09/14] NEW | #40 SessionState.kt Auth feature - Define possible states of a user session. --- .../ui/core/domain/model/SessionState.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/SessionState.kt diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/SessionState.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/SessionState.kt new file mode 100644 index 0000000..ebc99bb --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/SessionState.kt @@ -0,0 +1,31 @@ +/** + * Rho Studio® + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ========================================================================== + * File: SessionState.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-18 + * ========================================================================== + * Description: + * Defines the states of a user session, + * to manage authentication and guest access. + * ========================================================================== + */ +package com.rho.studio.ui.core.domain.model + +/** + * Sealed class representing the possible states of a user session. + */ +sealed class SessionState { + object Uninitialized : SessionState() + object Checking : SessionState() + data class Authenticated(val user: User) : SessionState() + object Guest : SessionState() +} From b6b68998fc8219b728d670fbcbdafa178498a98a Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 18:04:26 -0600 Subject: [PATCH 10/14] CHANGE | #38 HeaderViewModel DI SetUp Signed-off-by: Alexis Tercero --- .../ui/core/ui/common/HeaderViewModel.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt index 2aa66e7..a8020c9 100644 --- a/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt @@ -1,5 +1,6 @@ /** - * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * Rho Studio® + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗® * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ @@ -10,7 +11,7 @@ * File: HeaderViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-06 + * Date: 2026-08-18 * ========================================================================== * Description: * ViewModel for the reusable PageHeaderFragment. @@ -24,13 +25,27 @@ package com.rho.studio.ui.core.ui.common import com.rho.studio.ui.core.ui.base.BaseViewModel import com.rho.studio.ui.core.data.manager.SessionManager import com.rho.studio.ui.core.domain.model.User +import com.rho.studio.ui.core.domain.model.SessionState +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject -class HeaderViewModel : BaseViewModel() { - private val sessionManager = SessionManager.getInstance() - val currentUser: StateFlow = sessionManager.currentUser +class HeaderViewModel @Inject constructor( + private val sessionManager: SessionManager +) : BaseViewModel() { + private val _currentUser = MutableStateFlow(null) + val currentUser: StateFlow = _currentUser.asStateFlow() + + init { + viewModelScope.launch { + sessionManager.sessionState.collect { state -> + _currentUser.value = (state as? SessionState.Authenticated)?.user + } + } + } private val _title = MutableStateFlow("") val title: StateFlow = _title.asStateFlow() fun setTitle(newTitle: String) { _title.value = newTitle } From c27c140d99435b6adfa9bb9d388dc799392ddbf3 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 18:05:36 -0600 Subject: [PATCH 11/14] CHANGE | #38 HomeViewModel DI SetUp Signed-off-by: Alexis Tercero --- .../rho/studio/ui/features/home/HomeViewModel.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt index 7d3e6e0..f0e16b6 100644 --- a/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt +++ b/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt @@ -10,7 +10,7 @@ * File: HomeViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-06 + * Date: 2026-08-18 * ============================================================================================== * Description: ViewModel for the Home feature, managing feature state, session data, * and providing access to available service modules. @@ -27,13 +27,14 @@ import com.rho.studio.ui.core.domain.usecase.LogoutUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject -class HomeViewModel : BaseViewModel() { +class HomeViewModel @Inject constructor( + private val sessionManager: SessionManager, + private val logoutUseCase: LogoutUseCase +) : BaseViewModel() { - private val sessionManager = SessionManager.getInstance() - private val logoutUseCase = LogoutUseCase(sessionManager) - - private val _currentUser = MutableStateFlow(sessionManager.getCurrentUserSync()) + private val _currentUser = MutableStateFlow(sessionManager.getCurrentUser()) val currentUser: StateFlow = _currentUser.asStateFlow() // Parametrized services for the Home experience From 7c0e9541ef2cc39c0279bf37e4f9c68c28909a5b Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 18:16:31 -0600 Subject: [PATCH 12/14] UPDATE | #37 Add google-services to CI/CD Signed-off-by: Alexis Tercero --- .github/workflows/android.yml | 8 +- .../core/data/manager/SessionManagerTest.kt | 86 +++++++++++++++++++ .../core/domain/usecase/LoginUseCaseTest.kt | 60 ++++++------- 3 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 core/data/src/test/java/com/rho/studio/ui/core/data/manager/SessionManagerTest.kt diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f57bac3..e744d44 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -3,7 +3,7 @@ name: Android Debug CI/CD Rho.Studio® on: workflow_dispatch: push: - branches: [ " " ] + branches: [ "37-data-layer-and-dagger-di" ] pull_request: branches: [ "dev" , "pre-release" ] @@ -23,6 +23,12 @@ jobs: distribution: 'temurin' cache: gradle + # Add this step here + - name: Decode Google Services JSON + env: + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + run: echo $GOOGLE_SERVICES_JSON | base64 --decode > app/google-services.json + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/core/data/src/test/java/com/rho/studio/ui/core/data/manager/SessionManagerTest.kt b/core/data/src/test/java/com/rho/studio/ui/core/data/manager/SessionManagerTest.kt new file mode 100644 index 0000000..b1beb90 --- /dev/null +++ b/core/data/src/test/java/com/rho/studio/ui/core/data/manager/SessionManagerTest.kt @@ -0,0 +1,86 @@ +package com.rho.studio.ui.core.data.manager + +import com.rho.studio.ui.core.domain.model.SessionState +import com.rho.studio.ui.core.domain.model.User +import com.rho.studio.ui.core.domain.repository.SessionRepository +import io.mockk.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.* +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SessionManagerTest { + + private val testDispatcher = StandardTestDispatcher() + private lateinit var repository: SessionRepository + private lateinit var sessionManager: SessionManager + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + repository = mockk(relaxed = true) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `init loads saved session - authenticated`() = runTest { + val user = User("1", "test@rho.studio", "Test User") + coEvery { repository.getUser() } returns user + + sessionManager = SessionManager(repository, testDispatcher) + testDispatcher.scheduler.advanceUntilIdle() + + val state = sessionManager.sessionState.value + assertTrue(state is SessionState.Authenticated) + assertEquals(user, (state as SessionState.Authenticated).user) + } + + @Test + fun `init loads saved session - guest`() = runTest { + coEvery { repository.getUser() } returns null + + sessionManager = SessionManager(repository, testDispatcher) + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(sessionManager.sessionState.value is SessionState.Guest) + } + + @Test + fun `updateSession updates state and saves to repository`() = runTest { + coEvery { repository.getUser() } returns null + sessionManager = SessionManager(repository, testDispatcher) + testDispatcher.scheduler.advanceUntilIdle() + + val user = User("2", "new@rho.studio", "New User") + sessionManager.updateSession(user) + testDispatcher.scheduler.advanceUntilIdle() + + val state = sessionManager.sessionState.value + assertTrue(state is SessionState.Authenticated) + assertEquals(user, (state as SessionState.Authenticated).user) + coVerify { repository.saveUser(user) } + } + + @Test + fun `clearSession updates state to Guest and clears repository`() = runTest { + val user = User("1", "test@rho.studio", "Test User") + coEvery { repository.getUser() } returns user + sessionManager = SessionManager(repository, testDispatcher) + testDispatcher.scheduler.advanceUntilIdle() + + sessionManager.clearSession() + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(sessionManager.sessionState.value is SessionState.Guest) + coVerify { repository.clearSession() } + } +} diff --git a/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt b/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt index 5e64b57..5291431 100644 --- a/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt +++ b/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt @@ -10,17 +10,16 @@ * File: LoginUseCaseTest.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-08-06 + * Date: 2026-08-12 * ============================================================================================== - * Description: Check expected behavior of [LoginUseCase] + * Description: Check expected behavior of [LoginUseCase] with simplified credentials. * ============================================================================================== */ package com.rho.studio.ui.core.domain.usecase -import com.rho.studio.ui.core.domain.model.Credentials -import com.rho.studio.ui.core.domain.model.Result -import com.rho.studio.ui.core.domain.model.User +import com.rho.studio.ui.core.domain.model.* import com.rho.studio.ui.core.domain.repository.AuthRepository +import io.mockk.* import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -32,54 +31,47 @@ class LoginUseCaseTest { private lateinit var loginUseCase: LoginUseCase private lateinit var authRepository: AuthRepository private lateinit var sessionManager: SessionManagerInterface - private var lastUpdatedUser: User? = null - private var isSessionCleared = false @Before fun setUp() { - sessionManager = object : SessionManagerInterface { - override fun updateSession(user: User) { - lastUpdatedUser = user - } - override fun clearSession() { - isSessionCleared = true - } - override fun extractNameFromEmail(email: String): String { - return "Test" - } - } - authRepository = object : AuthRepository { - override suspend fun login(credentials: Credentials): User { - if (credentials.email == "error@rho.studio") throw RuntimeException("Network error") - return User("user_123", credentials.email, "Test") - } - } + authRepository = mockk() + sessionManager = mockk(relaxed = true) loginUseCase = LoginUseCase(authRepository, sessionManager) } @Test - fun `login success with valid credentials`() = runBlocking { + fun `login success calls repository and updates session`() = runBlocking { val credentials = Credentials("test@rho.studio", "password123") + val user = User("user_123", "test@rho.studio", "Test User") + + coEvery { authRepository.login(credentials) } returns user + val result = loginUseCase(credentials) + assertTrue(result is Result.Success) - val user = (result as Result.Success).data - assertEquals("test@rho.studio", user.email) - assertEquals("Test", user.name) - assertEquals(user, lastUpdatedUser) + assertEquals(user, (result as Result.Success).data) + coVerify { sessionManager.updateSession(user) } } @Test - fun `login failure with invalid credentials`() = runBlocking { - val credentials = Credentials("invalid-email", "short") + fun `login failure returns error result`() = runBlocking { + val credentials = Credentials("test@rho.studio", "password123") + val exceptionMessage = "Auth failed" + val exception = RuntimeException(exceptionMessage) + + coEvery { authRepository.login(credentials) } throws exception + val result = loginUseCase(credentials) + assertTrue(result is Result.Error) - assertTrue((result as Result.Error).exception is IllegalArgumentException) + assertEquals(exceptionMessage, (result as Result.Error).exception.message) + coVerify(exactly = 0) { sessionManager.updateSession(any()) } } @Test - fun `logout success`() = runBlocking { + fun `logout success calls session clear`() = runBlocking { val logoutUseCase = LogoutUseCase(sessionManager) logoutUseCase(Unit) - assertTrue(isSessionCleared) + coVerify { sessionManager.clearSession() } } } From 193f8d4eead95815c62a0a941fef7481ca20bc30 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 18:24:24 -0600 Subject: [PATCH 13/14] NEW | #37 DaggerGraphTest - DevIteration completed Signed-off-by: Alexis Tercero --- .../com/rho/studio/ui/di/DaggerGraphTest.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt diff --git a/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt b/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt new file mode 100644 index 0000000..8e3b342 --- /dev/null +++ b/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt @@ -0,0 +1,50 @@ +package com.rho.studio.ui.di + +import android.content.Context +import com.google.firebase.auth.FirebaseAuth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +class DaggerGraphTest { + + @Before + fun setUp() { + mockkStatic(FirebaseAuth::class) + every { FirebaseAuth.getInstance() } returns mockk(relaxed = true) + } + + @Test + fun `verify Dagger graph initialization`() { + val mockContext = mockk(relaxed = true) + + // Initialize ComponentManager + ComponentManager.init(mockContext) + + val appComponent = ComponentManager.getAppComponent() + assertNotNull(appComponent) + assertNotNull(appComponent.viewModelFactory()) + + // Verify CoreComponent exposures + val coreComponent = appComponent.coreComponent() + assertNotNull(coreComponent) + assertNotNull(coreComponent.sessionManager()) + assertNotNull(coreComponent.authRepository()) + } + + @Test + fun `verify UserComponent lifecycle`() { + val mockContext = mockk(relaxed = true) + ComponentManager.init(mockContext) + + val userComponent = ComponentManager.createUserComponent() + assertNotNull(userComponent) + assertNotNull(userComponent.viewModelFactory()) + + ComponentManager.destroyUserComponent() + // Note: ComponentManager.getUserComponent() would return null now + } +} From f6648b7fdbf7c573262e313f27ede9b652df6443 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Tue, 18 Aug 2026 18:38:38 -0600 Subject: [PATCH 14/14] FIX | #37 DaggerGraphTest Signed-off-by: Alexis Tercero --- app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt b/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt index 8e3b342..68a6776 100644 --- a/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt +++ b/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt @@ -1,6 +1,7 @@ package com.rho.studio.ui.di import android.content.Context +import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.auth.FirebaseAuth import io.mockk.every import io.mockk.mockk @@ -15,6 +16,9 @@ class DaggerGraphTest { fun setUp() { mockkStatic(FirebaseAuth::class) every { FirebaseAuth.getInstance() } returns mockk(relaxed = true) + + mockkStatic(FirebaseAnalytics::class) + every { FirebaseAnalytics.getInstance(any()) } returns mockk(relaxed = true) } @Test