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

Class Method, % Branch, % Line, % Instruction, %
DeleteUserAccountScreenKt 0% (0/13) 0% (0/53) 0% (0/111) 0% (0/1534)
DeleteUserAccountScreenKt$DeleteUserAccountScreen$1$1 0% (0/1) 0% (0/2) 0% (0/3) 0% (0/33)
DeleteUserAccountScreenKt$WhenMappings
Total 0% (0/14) 0% (0/55) 0% (0/114) 0% (0/1567)


 package com.georgeci.moneysurfer.feature.settings.account
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.Icon
 import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.SnackbarDuration
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 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.feature.settings.components.SettingsSubScreenScaffold
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 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_delete_account_button
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_confirm_cancel
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_confirm_confirm
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_confirm_message
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_confirm_title
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_deleted_memberships
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_deleted_section
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_deleted_sync
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_deleted_workspaces
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_intro
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_kept_local
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_kept_section
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_kept_shared
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_notice_auth_failed
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_notice_cleanup_failed
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_notice_generic
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_notice_recent_login
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_notice_wrong_password
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_password_label
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_password_missing
 import moneysurfer.feature.settings.generated.resources.settings_delete_account_title
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 
 /** Stable selectors — see docs/testing/testing-strategy.md. */
 object DeleteUserAccountTestTags {
     const val PasswordField = "deleteAccount:passwordField"
     const val DeleteButton = "deleteAccount:deleteButton"
 }
 
 @Composable
 fun DeleteUserAccountScreen(
     onNavigateBack: () -> Unit,
     viewModel: DeleteUserAccountViewModel = koinViewModel(),
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
     val snackbarHostState = remember { SnackbarHostState() }
     var pendingNotice by remember { mutableStateOf<DeleteUserAccountNotice?>(null) }
 
     val pendingNoticeText: String? = pendingNotice?.let { noticeText(it) }
     LaunchedEffect(pendingNoticeText) {
         val text = pendingNoticeText ?: return@LaunchedEffect
         snackbarHostState.showSnackbar(message = text, duration = SnackbarDuration.Short)
         pendingNotice = null
     }
 
     viewModel.HandleSideEffect { effect ->
         when (effect) {
             DeleteUserAccountEffect.NavigateBack -> onNavigateBack()
             is DeleteUserAccountEffect.Notify -> pendingNotice = effect.notice
         }
     }
 
     DeleteUserAccountContent(
         state = state,
         snackbarHostState = snackbarHostState,
         onEvent = viewModel::onEvent,
     )
 }
 
 @Composable
 private fun noticeText(notice: DeleteUserAccountNotice): String = when (notice) {
     DeleteUserAccountNotice.WrongPassword ->
         stringResource(Res.string.settings_delete_account_notice_wrong_password)
     DeleteUserAccountNotice.RequiresRecentLogin ->
         stringResource(Res.string.settings_delete_account_notice_recent_login)
     DeleteUserAccountNotice.CleanupFailed ->
         stringResource(Res.string.settings_delete_account_notice_cleanup_failed)
     DeleteUserAccountNotice.AuthDeletionFailed ->
         stringResource(Res.string.settings_delete_account_notice_auth_failed)
     DeleteUserAccountNotice.Generic ->
         stringResource(Res.string.settings_delete_account_notice_generic)
 }
 
 @Composable
 private fun DeleteUserAccountContent(
     state: DeleteUserAccountState,
     snackbarHostState: SnackbarHostState,
     onEvent: (DeleteUserAccountEvent) -> Unit,
 ) {
     if (state.showConfirmDialog) {
         ConfirmDeleteUserAccountDialog(
             onConfirm = { onEvent(DeleteUserAccountEvent.OnConfirmAccepted) },
             onDismiss = { onEvent(DeleteUserAccountEvent.OnConfirmDismissed) },
         )
     }
 
     SettingsSubScreenScaffold(
         title = stringResource(Res.string.settings_delete_account_title),
         snackbarHostState = snackbarHostState,
         onBack = { onEvent(DeleteUserAccountEvent.OnBackClick) },
         showProgressOverlay = state.isDeleting,
     ) { padding ->
         Column(
             modifier = Modifier.padding(horizontal = 16.dp),
             verticalArrangement = Arrangement.spacedBy(AppTheme.spacing.small),
         ) {
             Spacer(Modifier.height(4.dp))
             Text(
                 text = stringResource(Res.string.settings_delete_account_intro),
                 style = AppTheme.typography.bodyMedium,
                 color = AppTheme.materialColors.onSurfaceVariant,
             )
 
             SectionTitle(stringResource(Res.string.settings_delete_account_deleted_section))
             BulletLine(stringResource(Res.string.settings_delete_account_deleted_workspaces))
             BulletLine(stringResource(Res.string.settings_delete_account_deleted_memberships))
             BulletLine(stringResource(Res.string.settings_delete_account_deleted_sync))
 
             SectionTitle(stringResource(Res.string.settings_delete_account_kept_section))
             BulletLine(stringResource(Res.string.settings_delete_account_kept_shared))
             BulletLine(stringResource(Res.string.settings_delete_account_kept_local))
 
             if (state.showPasswordField) {
                 Spacer(Modifier.height(AppTheme.spacing.small))
                 OutlinedTextField(
                     value = state.password,
                     onValueChange = { onEvent(DeleteUserAccountEvent.OnPasswordChange(it)) },
                     label = { Text(stringResource(Res.string.settings_delete_account_password_label)) },
                     singleLine = true,
                     visualTransformation = PasswordVisualTransformation(),
                     keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
                     isError = state.passwordMissing,
                     supportingText = {
                         if (state.passwordMissing) {
                             Text(stringResource(Res.string.settings_delete_account_password_missing))
                         }
                     },
                     enabled = !state.isDeleting,
                     modifier = Modifier
                         .fillMaxWidth()
                         .testTag(DeleteUserAccountTestTags.PasswordField),
                 )
             }
 
             Spacer(Modifier.height(AppTheme.spacing.small))
             Button(
                 onClick = { onEvent(DeleteUserAccountEvent.OnDeleteClick) },
                 enabled = !state.isDeleting,
                 colors = ButtonDefaults.buttonColors(
                     containerColor = AppTheme.materialColors.error,
                     contentColor = AppTheme.materialColors.onError,
                 ),
                 modifier = Modifier
                     .fillMaxWidth()
                     .testTag(DeleteUserAccountTestTags.DeleteButton),
             ) {
                 Text(stringResource(Res.string.settings_delete_account_button))
             }
             Spacer(Modifier.height(padding.calculateBottomPadding() + AppTheme.spacing.large))
         }
     }
 }
 
 @Composable
 private fun SectionTitle(text: String) {
     Text(
         text = text,
         style = AppTheme.typography.titleSmall,
         color = AppTheme.materialColors.onSurface,
         modifier = Modifier.padding(top = AppTheme.spacing.small),
     )
 }
 
 @Composable
 private fun BulletLine(text: String) {
     Row {
         Text(
             text = "•",
             style = AppTheme.typography.bodyMedium,
             color = AppTheme.materialColors.onSurfaceVariant,
         )
         Spacer(Modifier.width(8.dp))
         Text(
             text = text,
             style = AppTheme.typography.bodyMedium,
             color = AppTheme.materialColors.onSurfaceVariant,
         )
     }
 }
 
 /** Final confirmation — mirrors the M3 dialog shape used by the financial-account delete. */
 @Composable
 private fun ConfirmDeleteUserAccountDialog(
     onConfirm: () -> Unit,
     onDismiss: () -> Unit,
 ) {
     AlertDialog(
         onDismissRequest = onDismiss,
         icon = {
             Icon(
                 imageVector = SurferIcons.Delete,
                 contentDescription = SurferSemantics.Decorative,
                 tint = AppTheme.materialColors.error,
             )
         },
         title = {
             Text(
                 text = stringResource(Res.string.settings_delete_account_confirm_title),
                 textAlign = TextAlign.Center,
             )
         },
         text = {
             Text(
                 text = stringResource(Res.string.settings_delete_account_confirm_message),
                 textAlign = TextAlign.Center,
             )
         },
         confirmButton = {
             Button(
                 onClick = onConfirm,
                 colors = ButtonDefaults.buttonColors(
                     containerColor = AppTheme.materialColors.error,
                     contentColor = AppTheme.materialColors.onError,
                 ),
             ) {
                 Text(stringResource(Res.string.settings_delete_account_confirm_confirm))
             }
         },
         dismissButton = {
             TextButton(onClick = onDismiss) {
                 Text(stringResource(Res.string.settings_delete_account_confirm_cancel))
             }
         },
     )
 }
 
 @Preview
 @Composable
 private fun DeleteUserAccountScreenPreview() {
     AppTheme {
         DeleteUserAccountContent(
             state = DeleteUserAccountState(userEmail = "kasia@georgeci.com"),
             snackbarHostState = SnackbarHostState(),
             onEvent = {},
         )
     }
 }