Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Account Settings - Show account name in TopAppBar
The account settings should display the account name as subtitle in the TopAppBar. This should also be added for the new account settings.


Discussion hints:
nan

Pull request: Add account name to general settings
Resolves #9149

Test command: ./gradlew :feature:account:settings:impl:testDebugUnitTest --tests "net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsViewModelTest" --tests "net.thunderbird.feature.account.settings.impl.domain.usecase.GetAccountNameTest" --tests "net.thunderbird.feature.account.settings.impl.AccountSettingsModuleKtTest"

Fix patch:
diff --git a/feature/account/settings/impl/build.gradle.kts b/feature/account/settings/impl/build.gradle.kts
index e46a95ad25f..85f4ca0dee0 100644
--- a/feature/account/settings/impl/build.gradle.kts
+++ b/feature/account/settings/impl/build.gradle.kts
@@ -18,6 +18,7 @@ dependencies {
     implementation(projects.core.ui.compose.navigation)
     implementation(projects.core.ui.compose.preference)
     implementation(projects.core.ui.legacy.theme2.common)
+    implementation(libs.timber)
 
     testImplementation(projects.core.ui.compose.testing)
 }
diff --git a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt
index 33a3ec5aad0..62bc1498c11 100644
--- a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt
+++ b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt
@@ -4,6 +4,7 @@ import net.thunderbird.feature.account.settings.api.AccountSettingsNavigation
 import net.thunderbird.feature.account.settings.impl.DefaultAccountSettingsNavigation
 import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.ResourceProvider
 import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
+import net.thunderbird.feature.account.settings.impl.domain.usecase.GetAccountName
 import net.thunderbird.feature.account.settings.impl.domain.usecase.GetGeneralPreferences
 import net.thunderbird.feature.account.settings.impl.domain.usecase.UpdateGeneralPreferences
 import net.thunderbird.feature.account.settings.impl.ui.general.GeneralResourceProvider
@@ -21,6 +22,12 @@ val featureAccountSettingsModule = module {
         )
     }
 
+    factory<UseCase.GetAccountName> {
+        GetAccountName(
+            repository = get(),
+        )
+    }
+
     factory<UseCase.GetGeneralPreferences> {
         GetGeneralPreferences(
             repository = get(),
@@ -37,6 +44,7 @@ val featureAccountSettingsModule = module {
     viewModel { params ->
         GeneralSettingsViewModel(
             accountId = params.get(),
+            getAccountName = get(),
             getGeneralPreferences = get(),
             updateGeneralPreferences = get(),
         )
diff --git a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/AccountSettingsDomainContract.kt b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/AccountSettingsDomainContract.kt
index 6384a7abedb..60cb7101fde 100644
--- a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/AccountSettingsDomainContract.kt
+++ b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/AccountSettingsDomainContract.kt
@@ -11,12 +11,17 @@ import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
 import net.thunderbird.feature.account.api.AccountId
 import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
 
+internal typealias AccountNameOutcome = Outcome<String, SettingsError>
 internal typealias AccountSettingsOutcome = Outcome<ImmutableList<Preference>, SettingsError>
 
 internal interface AccountSettingsDomainContract {
 
     interface UseCase {
 
+        fun interface GetAccountName {
+            operator fun invoke(accountId: AccountId): Flow<AccountNameOutcome>
+        }
+
         fun interface GetGeneralPreferences {
             operator fun invoke(accountId: AccountId): Flow<AccountSettingsOutcome>
         }
diff --git a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/GetAccountName.kt b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/GetAccountName.kt
new file mode 100644
index 00000000000..207b80f8291
--- /dev/null
+++ b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/GetAccountName.kt
@@ -0,0 +1,29 @@
+package net.thunderbird.feature.account.settings.impl.domain.usecase
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import net.thunderbird.core.outcome.Outcome
+import net.thunderbird.feature.account.api.AccountId
+import net.thunderbird.feature.account.api.profile.AccountProfileRepository
+import net.thunderbird.feature.account.settings.impl.domain.AccountNameOutcome
+import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract
+import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
+
+internal class GetAccountName(
+    private val repository: AccountProfileRepository,
+) : UseCase.GetAccountName {
+
+    override fun invoke(accountId: AccountId): Flow<AccountNameOutcome> {
+        return repository.getById(accountId).map { profile ->
+            if (profile != null) {
+                Outcome.success(profile.name)
+            } else {
+                Outcome.failure(
+                    AccountSettingsDomainContract.SettingsError.NotFound(
+                        message = "Account profile not found for accountId: ${accountId.value}",
+                    ),
+                )
+            }
+        }
+    }
+}
diff --git a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModel.kt b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModel.kt
index 6819e5dd7d0..061bb8466cc 100644
--- a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModel.kt
+++ b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModel.kt
@@ -6,19 +6,37 @@ import kotlinx.coroutines.launch
 import net.thunderbird.core.outcome.handle
 import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
 import net.thunderbird.feature.account.api.AccountId
+import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
 import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
 import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Effect
 import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Event
 import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.State
+import timber.log.Timber
 
 internal class GeneralSettingsViewModel(
     private val accountId: AccountId,
+    private val getAccountName: UseCase.GetAccountName,
     private val getGeneralPreferences: UseCase.GetGeneralPreferences,
     private val updateGeneralPreferences: UseCase.UpdateGeneralPreferences,
     initialState: State = State(),
 ) : BaseViewModel<State, Event, Effect>(initialState), GeneralSettingsContract.ViewModel {
 
     init {
+        viewModelScope.launch {
+            getAccountName(accountId).collect { outcome ->
+                outcome.handle(
+                    onSuccess = { accountName ->
+                        updateState { state ->
+                            state.copy(
+                                subtitle = accountName,
+                            )
+                        }
+                    },
+                    onFailure = { handleError(it) },
+                )
+            }
+        }
+
         viewModelScope.launch {
             getGeneralPreferences(accountId).collect { outcome ->
                 outcome.handle(
@@ -29,7 +47,7 @@ internal class GeneralSettingsViewModel(
                             )
                         }
                     },
-                    onFailure = {},
+                    onFailure = { handleError(it) },
                 )
             }
         }
@@ -47,4 +65,10 @@ internal class GeneralSettingsViewModel(
             updateGeneralPreferences(accountId, preference)
         }
     }
+
+    private fun handleError(error: SettingsError) {
+        when (error) {
+            is SettingsError.NotFound -> Timber.w(error.message)
+        }
+    }
 }


Test patch:
diff --git a/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/AccountSettingsModuleKtTest.kt b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/AccountSettingsModuleKtTest.kt
index e02d4d9944e..eb4216dd963 100644
--- a/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/AccountSettingsModuleKtTest.kt
+++ b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/AccountSettingsModuleKtTest.kt
@@ -1,39 +1,15 @@
 package net.thunderbird.feature.account.settings.impl
 
 import kotlin.test.Test
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
 import net.thunderbird.feature.account.api.AccountId
-import net.thunderbird.feature.account.api.profile.AccountProfile
-import net.thunderbird.feature.account.api.profile.AccountProfileRepository
 import net.thunderbird.feature.account.settings.featureAccountSettingsModule
 import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract
-import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsViewModel
-import org.junit.runner.RunWith
-import org.koin.android.ext.koin.androidContext
 import org.koin.core.annotation.KoinExperimentalAPI
-import org.koin.core.module.Module
-import org.koin.dsl.module
-import org.koin.test.AutoCloseKoinTest
-import org.koin.test.check.checkKoinModules
 import org.koin.test.verify.verify
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.RuntimeEnvironment
 
-@OptIn(KoinExperimentalAPI::class)
-@RunWith(RobolectricTestRunner::class)
-internal class AccountSettingsModuleKtTest : AutoCloseKoinTest() {
-
-    private val externalModule: Module = module {
-        single<AccountProfileRepository> {
-            object : AccountProfileRepository {
-                override fun getById(accountId: AccountId): Flow<AccountProfile?> = flow { null }
-
-                override suspend fun update(accountProfile: AccountProfile) = Unit
-            }
-        }
-    }
+internal class AccountSettingsModuleKtTest {
 
+    @OptIn(KoinExperimentalAPI::class)
     @Test
     fun `should hava a valid di module`() {
         featureAccountSettingsModule.verify(
@@ -42,12 +18,5 @@ internal class AccountSettingsModuleKtTest : AutoCloseKoinTest() {
                 GeneralSettingsContract.State::class,
             ),
         )
-
-        checkKoinModules(
-            modules = listOf(externalModule, featureAccountSettingsModule),
-            appDeclaration = { androidContext(RuntimeEnvironment.getApplication()) },
-        ) {
-            withParameter<GeneralSettingsViewModel> { AccountId.create() }
-        }
     }
 }
diff --git a/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/GetAccountNameTest.kt b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/GetAccountNameTest.kt
new file mode 100644
index 00000000000..6b065c130b4
--- /dev/null
+++ b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/GetAccountNameTest.kt
@@ -0,0 +1,61 @@
+package net.thunderbird.feature.account.settings.impl.domain.usecase
+
+import app.cash.turbine.test
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+import net.thunderbird.core.outcome.Outcome
+import net.thunderbird.feature.account.api.AccountId
+import net.thunderbird.feature.account.api.profile.AccountProfile
+import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
+import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
+
+class GetAccountNameTest {
+
+    @Test
+    fun `should emit account name when account profile present`() = runTest {
+        // Arrange
+        val accountId = AccountId.create()
+        val accountProfile = AccountProfile(
+            accountId = accountId,
+            name = "Test Account",
+            color = 0xFF0000,
+        )
+        val testSubject = createTestSubject(accountProfile)
+
+        // Act & Assert
+        testSubject(accountId).test {
+            val outcome = awaitItem()
+            assertThat(outcome).isInstanceOf(Outcome.Success::class)
+
+            val success = outcome as Outcome.Success
+            assertThat(success.data).isEqualTo(accountProfile.name)
+        }
+    }
+
+    @Test
+    fun `should emit NotFound when account profile not present`() = runTest {
+        // Arrange
+        val accountId = AccountId.create()
+        val testSubject = createTestSubject()
+
+        // Act & Assert
+        testSubject(accountId).test {
+            val outcome = awaitItem()
+            assertThat(outcome).isInstanceOf(Outcome.Failure::class)
+
+            val failure = outcome as Outcome.Failure
+            assertThat(failure.error).isInstanceOf(SettingsError.NotFound::class)
+        }
+    }
+
+    private fun createTestSubject(
+        accountProfile: AccountProfile? = null,
+    ): UseCase.GetAccountName {
+        return GetAccountName(
+            repository = FakeAccountProfileRepository(accountProfile),
+        )
+    }
+}
diff --git a/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModelTest.kt b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModelTest.kt
index f5dbdad1374..fdcd0540935 100644
--- a/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModelTest.kt
+++ b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralSettingsViewModelTest.kt
@@ -12,6 +12,7 @@ import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.test.StandardTestDispatcher
 import net.thunderbird.core.outcome.Outcome
@@ -27,6 +28,19 @@ class GeneralSettingsViewModelTest {
     @get:Rule
     val mainDispatcherRule = MainDispatcherRule(StandardTestDispatcher())
 
+    @Test
+    fun `should load account name`() = runMviTest {
+        val accountId = AccountId.create()
+        val initialState = State(
+            subtitle = null,
+            preferences = persistentListOf(),
+        )
+
+        generalSettingsRobot(accountId, initialState, persistentListOf()) {
+            verifyAccountNameLoaded()
+        }
+    }
+
     @Test
     fun `should load general settings`() = runMviTest {
         val accountId = AccountId.create()
@@ -101,6 +115,9 @@ private class GeneralSettingsRobot(
     private val viewModel: GeneralSettingsContract.ViewModel by lazy {
         GeneralSettingsViewModel(
             accountId = accountId,
+            getAccountName = {
+                flowOf(Outcome.success("Subtitle"))
+            },
             getGeneralPreferences = {
                 preferencesState.map {
                     println("Loading preferences: $it")
@@ -133,6 +150,14 @@ private class GeneralSettingsRobot(
         )
     }
 
+    suspend fun verifyAccountNameLoaded() {
+        assertThat(turbines.awaitStateItem()).isEqualTo(
+            initialState.copy(
+                subtitle = "Subtitle",
+            ),
+        )
+    }
+
     suspend fun verifyGeneralSettingsLoaded(preferences: ImmutableList<Preference>) {
         assertThat(turbines.awaitStateItem()).isEqualTo(
             initialState.copy(