Coverage Summary for Class: SettingsScreenKt (com.georgeci.moneysurfer.feature.settings)

Class Class, % Method, % Branch, % Line, % Instruction, %
SettingsScreenKt 0% (0/1) 0% (0/31) 0% (0/140) 0% (0/206) 0% (0/2692)


 package com.georgeci.moneysurfer.feature.settings
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.georgeci.moneysurfer.uikit.components.base.SurferToolbar
 import com.georgeci.moneysurfer.uikit.components.settings.SurferNameBlock
 import com.georgeci.moneysurfer.uikit.components.settings.SurferPendingBadge
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsChevron
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsGroup
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsRow
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 import com.georgeci.moneysurfer.uikit.modifier.surferContentContainer
 import com.georgeci.moneysurfer.uikit.modifier.surferSafeInsets
 import com.georgeci.moneysurfer.uikit.modifier.surferTestTagAsId
 import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import com.georgeci.moneysurfer.utils.HandleSideEffect
 import moneysurfer.feature.settings.generated.resources.Res
 import moneysurfer.feature.settings.generated.resources.settings_about
 import moneysurfer.feature.settings.generated.resources.settings_about_supporting_format
 import moneysurfer.feature.settings.generated.resources.settings_appearance_hub_supporting
 import moneysurfer.feature.settings.generated.resources.settings_appearance_hub_supporting_dynamic
 import moneysurfer.feature.settings.generated.resources.settings_appearance_hub_title
 import moneysurfer.feature.settings.generated.resources.settings_backup
 import moneysurfer.feature.settings.generated.resources.settings_backup_supporting
 import moneysurfer.feature.settings.generated.resources.settings_budgets_supporting
 import moneysurfer.feature.settings.generated.resources.settings_budgets_title
 import moneysurfer.feature.settings.generated.resources.settings_categories_supporting
 import moneysurfer.feature.settings.generated.resources.settings_categories_title
 import moneysurfer.feature.settings.generated.resources.settings_change_workspace
 import moneysurfer.feature.settings.generated.resources.settings_change_workspace_supporting
 import moneysurfer.feature.settings.generated.resources.settings_csv_supporting
 import moneysurfer.feature.settings.generated.resources.settings_csv_title
 import moneysurfer.feature.settings.generated.resources.settings_debug_config
 import moneysurfer.feature.settings.generated.resources.settings_debug_config_supporting
 import moneysurfer.feature.settings.generated.resources.settings_delete_account
 import moneysurfer.feature.settings.generated.resources.settings_goals_supporting
 import moneysurfer.feature.settings.generated.resources.settings_goals_title
 import moneysurfer.feature.settings.generated.resources.settings_logout
 import moneysurfer.feature.settings.generated.resources.settings_logout_guest_warning_cancel
 import moneysurfer.feature.settings.generated.resources.settings_logout_guest_warning_confirm
 import moneysurfer.feature.settings.generated.resources.settings_logout_guest_warning_message
 import moneysurfer.feature.settings.generated.resources.settings_logout_guest_warning_title
 import moneysurfer.feature.settings.generated.resources.settings_members
 import moneysurfer.feature.settings.generated.resources.settings_members_count_format
 import moneysurfer.feature.settings.generated.resources.settings_pending_invites
 import moneysurfer.feature.settings.generated.resources.settings_pending_invites_supporting_empty
 import moneysurfer.feature.settings.generated.resources.settings_pending_invites_supporting_format
 import moneysurfer.feature.settings.generated.resources.settings_preferences_hub_supporting
 import moneysurfer.feature.settings.generated.resources.settings_preferences_hub_title
 import moneysurfer.feature.settings.generated.resources.settings_section_data
 import moneysurfer.feature.settings.generated.resources.settings_section_help
 import moneysurfer.feature.settings.generated.resources.settings_section_personalization
 import moneysurfer.feature.settings.generated.resources.settings_section_workspace
 import moneysurfer.feature.settings.generated.resources.settings_sync_hub_supporting
 import moneysurfer.feature.settings.generated.resources.settings_sync_hub_title
 import moneysurfer.feature.settings.generated.resources.settings_title
 import moneysurfer.feature.settings.generated.resources.settings_user_name
 import moneysurfer.feature.settings.generated.resources.settings_version_format
 import org.jetbrains.compose.resources.pluralStringResource
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 
 /**
  * Stable selectors for the Settings screen — see docs/testing/testing-strategy.md.
  *
  * [SyncRow], [LogoutRow] and [DeleteAccountRow] are never composed in the offline build;
  * their absence is what the offline golden Maestro flow asserts via `notVisible`.
  * Everything else — including [GoalsRow], [PreferencesRow] and [BackupRow] — is composed in both
  * variants, and the same flow asserts those positively.
  *
  * [ProfileName] and [MembersRow] are state-gated rather than build-gated: the first needs a
  * profile to show, the second a workspace with members, so assert them only where the flow
  * has put the app in that state.
  *
  * [DebugConfigRow] is the one tag that varies by *build type* rather than by host: it appears
  * whenever a real debug-overrides layer is bound, and never in a release build. No Maestro flow
  * asserts it either way, precisely so the golden path stays independent of build type.
  */
 object SettingsTestTags {
     const val Root = "settings:root"
     const val ProfileName = "settings:profileName"
     const val WorkspaceRow = "settings:workspaceRow"
     const val MembersRow = "settings:membersRow"
     const val CategoriesRow = "settings:categoriesRow"
     const val BudgetsRow = "settings:budgetsRow"
     const val GoalsRow = "settings:goalsRow"
     const val AppearanceRow = "settings:appearanceRow"
     const val PreferencesRow = "settings:preferencesRow"
     const val AboutRow = "settings:aboutRow"
     const val SyncRow = "settings:syncRow"
     const val BackupRow = "settings:backupRow"
     const val CsvRow = "settings:csvRow"
     const val LogoutRow = "settings:logoutRow"
     const val DeleteAccountRow = "settings:deleteAccountRow"
     const val DebugConfigRow = "settings:debugConfigRow"
 }
 
 // The effect dispatch below is one straight-line branch per destination — flat, exhaustive, and
 // checked by the compiler. Its "complexity" is the number of settings rows, not tangled logic.
 @Suppress("CyclomaticComplexMethod")
 @Composable
 fun SettingsScreen(
     onNavigateBack: () -> Unit,
     onNavigateToWorkspaceSelector: () -> Unit,
     onNavigateToIncomingInvites: () -> Unit,
     onNavigateToMembers: (com.georgeci.moneysurfer.domain.primitives.WorkspaceId) -> Unit,
     onNavigateToCategories: () -> Unit,
     onNavigateToBudgets: () -> Unit,
     onNavigateToGoals: () -> Unit,
     onNavigateToAppearance: () -> Unit,
     onNavigateToPreferences: () -> Unit,
     onNavigateToSync: () -> Unit,
     onNavigateToBackup: () -> Unit,
     onNavigateToCsvBackup: () -> Unit,
     onNavigateToAbout: () -> Unit,
     onNavigateToDeleteAccount: () -> Unit,
     onNavigateToDebugConfig: () -> Unit,
     viewModel: SettingsViewModel = koinViewModel(),
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
 
     viewModel.HandleSideEffect { effect ->
         when (effect) {
             SettingsEffect.NavigateBack -> onNavigateBack()
             SettingsEffect.NavigateToWorkspaceSelector -> onNavigateToWorkspaceSelector()
             SettingsEffect.NavigateToIncomingInvites -> onNavigateToIncomingInvites()
             is SettingsEffect.NavigateToMembers -> onNavigateToMembers(effect.workspaceId)
             SettingsEffect.NavigateToCategories -> onNavigateToCategories()
             SettingsEffect.NavigateToBudgets -> onNavigateToBudgets()
             SettingsEffect.NavigateToGoals -> onNavigateToGoals()
             SettingsEffect.NavigateToAppearance -> onNavigateToAppearance()
             SettingsEffect.NavigateToPreferences -> onNavigateToPreferences()
             SettingsEffect.NavigateToSync -> onNavigateToSync()
             SettingsEffect.NavigateToBackup -> onNavigateToBackup()
             SettingsEffect.NavigateToCsvBackup -> onNavigateToCsvBackup()
             SettingsEffect.NavigateToAbout -> onNavigateToAbout()
             SettingsEffect.NavigateToDeleteAccount -> onNavigateToDeleteAccount()
             SettingsEffect.NavigateToDebugConfig -> onNavigateToDebugConfig()
         }
     }
 
     SettingsContent(
         state = state,
         onEvent = viewModel::onEvent,
     )
 }
 
 @Composable
 private fun SettingsContent(
     state: SettingsState,
     onEvent: (SettingsEvent) -> Unit,
 ) {
     Scaffold(
         modifier = Modifier
             .surferSafeInsets()
             .surferTestTagAsId()
             .testTag(SettingsTestTags.Root),
         containerColor = AppTheme.materialColors.surface,
         topBar = {
             SurferToolbar(
                 title = stringResource(Res.string.settings_title),
                 onBack = { onEvent(SettingsEvent.OnBackClick) },
                 colors = TopAppBarDefaults.topAppBarColors(
                     containerColor = AppTheme.materialColors.surface,
                     titleContentColor = AppTheme.materialColors.onSurface,
                 ),
             )
         },
     ) { padding ->
         Column(
             modifier = Modifier
                 .fillMaxSize()
                 .surferContentContainer()
                 .padding(top = padding.calculateTopPadding())
                 .verticalScroll(rememberScrollState()),
         ) {
             if (state.showProfile) {
                 Box(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 12.dp)) {
                     SurferNameBlock(
                         name = stringResource(Res.string.settings_user_name),
                         email = userEmailText(state),
                         trailing = null,
                         nameTestTag = SettingsTestTags.ProfileName,
                     )
                 }
             }
 
             SurferSettingsGroup(title = stringResource(Res.string.settings_section_workspace)) {
                 SurferSettingsRow(
                     icon = SurferIcons.People,
                     title = stringResource(Res.string.settings_change_workspace),
                     supportingText = stringResource(Res.string.settings_change_workspace_supporting),
                     onClick = { onEvent(SettingsEvent.OnChangeWorkspaceClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.WorkspaceRow),
                 )
                 if (state.showWorkspaceMembers && state.currentWorkspaceId != null) {
                     SurferSettingsRow(
                         icon = SurferIcons.Sparkle,
                         title = stringResource(Res.string.settings_members),
                         supportingText = pluralStringResource(
                             Res.plurals.settings_members_count_format,
                             state.activeMemberCount,
                             state.activeMemberCount,
                         ),
                         onClick = { onEvent(SettingsEvent.OnMembersClick) },
                         trailing = { SurferSettingsChevron() },
                         modifier = Modifier.testTag(SettingsTestTags.MembersRow),
                     )
                 }
                 if (state.showPendingInvites) {
                     SurferSettingsRow(
                         icon = SurferIcons.Mail,
                         title = stringResource(Res.string.settings_pending_invites),
                         supporting = if (state.pendingInviteCount > 0) {
                             {
                                 SurferPendingBadge(
                                     text = pluralStringResource(
                                         Res.plurals.settings_pending_invites_supporting_format,
                                         state.pendingInviteCount,
                                         state.pendingInviteCount,
                                     ),
                                 )
                             }
                         } else {
                             null
                         },
                         supportingText = if (state.pendingInviteCount == 0) {
                             stringResource(Res.string.settings_pending_invites_supporting_empty)
                         } else {
                             null
                         },
                         onClick = { onEvent(SettingsEvent.OnIncomingInvitesClick) },
                         trailing = { SurferSettingsChevron() },
                     )
                 }
             }
 
             SurferSettingsGroup(title = stringResource(Res.string.settings_section_personalization)) {
                 SurferSettingsRow(
                     icon = SurferIcons.Category,
                     title = stringResource(Res.string.settings_categories_title),
                     supportingText = stringResource(Res.string.settings_categories_supporting),
                     onClick = { onEvent(SettingsEvent.OnCategoriesClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.CategoriesRow),
                 )
                 SurferSettingsRow(
                     icon = SurferIcons.Savings,
                     title = stringResource(Res.string.settings_budgets_title),
                     supportingText = stringResource(Res.string.settings_budgets_supporting),
                     onClick = { onEvent(SettingsEvent.OnBudgetsClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.BudgetsRow),
                 )
                 SurferSettingsRow(
                     icon = SurferIcons.Flag,
                     title = stringResource(Res.string.settings_goals_title),
                     supportingText = stringResource(Res.string.settings_goals_supporting),
                     onClick = { onEvent(SettingsEvent.OnGoalsClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.GoalsRow),
                 )
                 SurferSettingsRow(
                     icon = SurferIcons.Palette,
                     title = stringResource(Res.string.settings_appearance_hub_title),
                     supportingText = appearanceSupporting(state.isDynamicColorEnabled),
                     onClick = { onEvent(SettingsEvent.OnAppearanceClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.AppearanceRow),
                 )
                 SurferSettingsRow(
                     icon = SurferIcons.Globe,
                     title = stringResource(Res.string.settings_preferences_hub_title),
                     supportingText = stringResource(Res.string.settings_preferences_hub_supporting),
                     onClick = { onEvent(SettingsEvent.OnPreferencesClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.PreferencesRow),
                 )
             }
 
             SurferSettingsGroup(title = stringResource(Res.string.settings_section_data)) {
                 if (state.showSyncSection) {
                     SurferSettingsRow(
                         icon = SurferIcons.Sync,
                         title = stringResource(Res.string.settings_sync_hub_title),
                         supportingText = stringResource(Res.string.settings_sync_hub_supporting),
                         onClick = { onEvent(SettingsEvent.OnSyncClick) },
                         trailing = { SurferSettingsChevron() },
                         modifier = Modifier.testTag(SettingsTestTags.SyncRow),
                     )
                 }
                 SurferSettingsRow(
                     icon = SurferIcons.Archive,
                     title = stringResource(Res.string.settings_backup),
                     supportingText = stringResource(Res.string.settings_backup_supporting),
                     onClick = { onEvent(SettingsEvent.OnBackupClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.BackupRow),
                 )
                 SurferSettingsRow(
                     icon = SurferIcons.Download,
                     title = stringResource(Res.string.settings_csv_title),
                     supportingText = stringResource(Res.string.settings_csv_supporting),
                     onClick = { onEvent(SettingsEvent.OnCsvBackupClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.CsvRow),
                 )
             }
 
             SurferSettingsGroup(title = stringResource(Res.string.settings_section_help)) {
                 SurferSettingsRow(
                     icon = SurferIcons.Info,
                     title = stringResource(Res.string.settings_about),
                     supportingText = stringResource(
                         Res.string.settings_about_supporting_format,
                         state.appVersion,
                     ),
                     onClick = { onEvent(SettingsEvent.OnAboutClick) },
                     trailing = { SurferSettingsChevron() },
                     modifier = Modifier.testTag(SettingsTestTags.AboutRow),
                 )
                 if (state.showDebugConfig) {
                     SurferSettingsRow(
                         icon = SurferIcons.Code,
                         title = stringResource(Res.string.settings_debug_config),
                         supportingText = stringResource(Res.string.settings_debug_config_supporting),
                         onClick = { onEvent(SettingsEvent.OnDebugConfigClick) },
                         trailing = { SurferSettingsChevron() },
                         modifier = Modifier.testTag(SettingsTestTags.DebugConfigRow),
                     )
                 }
             }
 
             if (state.showLogout) {
                 Spacer(Modifier.height(8.dp))
                 Box(modifier = Modifier.padding(horizontal = 16.dp)) {
                     SurferSettingsRow(
                         icon = SurferIcons.Logout,
                         title = stringResource(Res.string.settings_logout),
                         danger = true,
                         onClick = { onEvent(SettingsEvent.OnLogoutClick) },
                         modifier = Modifier.testTag(SettingsTestTags.LogoutRow),
                     )
                 }
             }
 
             if (state.showDeleteAccount) {
                 Spacer(Modifier.height(8.dp))
                 Box(modifier = Modifier.padding(horizontal = 16.dp)) {
                     SurferSettingsRow(
                         icon = SurferIcons.Delete,
                         title = stringResource(Res.string.settings_delete_account),
                         danger = true,
                         onClick = { onEvent(SettingsEvent.OnDeleteAccountClick) },
                         modifier = Modifier.testTag(SettingsTestTags.DeleteAccountRow),
                     )
                 }
             }
 
             Spacer(Modifier.height(AppTheme.spacing.large))
             Text(
                 text = stringResource(Res.string.settings_version_format, state.appVersion),
                 style = AppTheme.typography.bodySmall,
                 color = AppTheme.materialColors.onSurfaceVariant,
                 textAlign = TextAlign.Center,
                 modifier = Modifier
                     .fillMaxWidth()
                     .padding(horizontal = 24.dp, vertical = AppTheme.spacing.small),
             )
             Spacer(Modifier.height(padding.calculateBottomPadding() + AppTheme.spacing.large))
         }
     }
 
     if (state.showGuestLogoutWarning) {
         GuestLogoutWarningDialog(
             onConfirm = { onEvent(SettingsEvent.OnGuestLogoutConfirmed) },
             onDismiss = { onEvent(SettingsEvent.OnGuestLogoutDismissed) },
         )
     }
 }
 
 /** Guest sessions are local-only — warn that logging out wipes the on-device data for good. */
 @Composable
 private fun GuestLogoutWarningDialog(
     onConfirm: () -> Unit,
     onDismiss: () -> Unit,
 ) {
     AlertDialog(
         onDismissRequest = onDismiss,
         icon = {
             Icon(
                 imageVector = SurferIcons.Logout,
                 contentDescription = SurferSemantics.Decorative,
                 tint = AppTheme.materialColors.error,
             )
         },
         title = {
             Text(
                 text = stringResource(Res.string.settings_logout_guest_warning_title),
                 textAlign = TextAlign.Center,
             )
         },
         text = {
             Text(
                 text = stringResource(Res.string.settings_logout_guest_warning_message),
                 textAlign = TextAlign.Center,
             )
         },
         confirmButton = {
             Button(
                 onClick = onConfirm,
                 colors = ButtonDefaults.buttonColors(
                     containerColor = AppTheme.materialColors.error,
                     contentColor = AppTheme.materialColors.onError,
                 ),
             ) {
                 Text(stringResource(Res.string.settings_logout_guest_warning_confirm))
             }
         },
         dismissButton = {
             TextButton(onClick = onDismiss) {
                 Text(stringResource(Res.string.settings_logout_guest_warning_cancel))
             }
         },
     )
 }
 
 @Composable
 private fun appearanceSupporting(isDynamicColorEnabled: Boolean): String =
     if (isDynamicColorEnabled) {
         stringResource(Res.string.settings_appearance_hub_supporting_dynamic)
     } else {
         stringResource(Res.string.settings_appearance_hub_supporting)
     }
 
 private fun userEmailText(state: SettingsState): String =
     when {
         state.isAnonymousUser -> "anon"
         !state.userEmail.isNullOrBlank() -> state.userEmail
         else -> "anon"
     }
 
 @Preview
 @Composable
 private fun SettingsScreenPreview() {
     AppTheme {
         SettingsContent(
             state = SettingsState(),
             onEvent = {},
         )
     }
 }