From e2fd65f0700ca599a3106dff33102a9b9f75c99f Mon Sep 17 00:00:00 2001 From: null Date: Sun, 21 Jun 2026 17:25:39 -0500 Subject: [PATCH] feat: security screen, theme polish, settings navigation, build config --- app/build.gradle.kts | 4 +- app/src/main/java/app/closer/MainActivity.kt | 56 ++++- .../closer/core/navigation/AppNavigation.kt | 4 + .../app/closer/core/navigation/AppRoute.kt | 3 + .../closer/data/local/SettingsDataStore.kt | 7 +- .../domain/repository/SettingsRepository.kt | 4 +- .../app/closer/ui/settings/SecurityScreen.kt | 230 ++++++++++++++++++ .../app/closer/ui/settings/SettingsScreen.kt | 137 +---------- .../closer/ui/settings/SettingsViewModel.kt | 14 +- app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- 11 files changed, 312 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/app/closer/ui/settings/SecurityScreen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76b84bb4..a9da5fbf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -148,7 +148,9 @@ dependencies { // Image loading implementation("io.coil-kt:coil-compose:2.7.0") - // Biometric auth — used in Settings to gate recovery phrase reveal + // AppCompat — required by BiometricPrompt (needs FragmentActivity base) + implementation("androidx.appcompat:appcompat:1.7.0") + // Biometric auth — recovery phrase reveal + app-level login lock implementation("androidx.biometric:biometric:1.1.0") // Google Sign-In via Credential Manager diff --git a/app/src/main/java/app/closer/MainActivity.kt b/app/src/main/java/app/closer/MainActivity.kt index d2e0af7a..c88fd8cc 100644 --- a/app/src/main/java/app/closer/MainActivity.kt +++ b/app/src/main/java/app/closer/MainActivity.kt @@ -2,19 +2,29 @@ package app.closer import android.content.Intent import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import app.closer.core.navigation.AppNavigation import app.closer.domain.repository.AppSettings +import app.closer.domain.repository.AuthRepository import app.closer.domain.repository.SettingsRepository import app.closer.domain.repository.ThemeMode import app.closer.ui.theme.CloserTheme @@ -22,8 +32,9 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class MainActivity : ComponentActivity() { +class MainActivity : AppCompatActivity() { @Inject lateinit var settingsRepository: SettingsRepository + @Inject lateinit var authRepository: AuthRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,6 +47,11 @@ class MainActivity : ComponentActivity() { ThemeMode.DARK -> true } + var sessionVerified by remember { mutableStateOf(false) } + val needsBiometricLock = settings.biometricLoginEnabled + && authRepository.currentUserId != null + && !sessionVerified + CloserTheme(darkTheme = useDarkTheme) { SideEffect { WindowCompat.getInsetsController(window, window.decorView).apply { @@ -47,7 +63,14 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - AppNavigation() + if (needsBiometricLock) { + BiometricLockScreen( + activity = this@MainActivity, + onUnlocked = { sessionVerified = true } + ) + } else { + AppNavigation() + } } } } @@ -58,3 +81,28 @@ class MainActivity : ComponentActivity() { setIntent(intent) } } + +@Composable +private fun BiometricLockScreen(activity: AppCompatActivity, onUnlocked: () -> Unit) { + LaunchedEffect(Unit) { + BiometricPrompt( + activity, + ContextCompat.getMainExecutor(activity), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + onUnlocked() + } + } + ).authenticate( + BiometricPrompt.PromptInfo.Builder() + .setTitle("Unlock Closer") + .setSubtitle("Verify it's you to continue") + .setAllowedAuthenticators( + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + .build() + ) + } + Box(modifier = Modifier.fillMaxSize()) +} diff --git a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt index e2128139..70fdb13f 100644 --- a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt +++ b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt @@ -69,6 +69,7 @@ import app.closer.ui.settings.AppearanceScreen import app.closer.ui.settings.DeleteAccountScreen import app.closer.ui.settings.NotificationSettingsScreen import app.closer.ui.settings.PrivacyScreen +import app.closer.ui.settings.SecurityScreen import app.closer.ui.settings.RelationshipSettingsScreen import app.closer.ui.settings.SettingsScreen import app.closer.ui.settings.SubscriptionScreen @@ -440,6 +441,9 @@ fun AppNavigation( composable(route = AppRoute.DELETE_ACCOUNT) { DeleteAccountScreen(onNavigate = navigateRoute) } + composable(route = AppRoute.SECURITY) { + SecurityScreen(onNavigate = navigateRoute) + } composable(route = AppRoute.YOUR_PROGRESS) { YourProgressScreen( onBack = navigateBackOrHome diff --git a/app/src/main/java/app/closer/core/navigation/AppRoute.kt b/app/src/main/java/app/closer/core/navigation/AppRoute.kt index 35d4e891..29d5b586 100644 --- a/app/src/main/java/app/closer/core/navigation/AppRoute.kt +++ b/app/src/main/java/app/closer/core/navigation/AppRoute.kt @@ -34,6 +34,7 @@ object AppRoute { const val SUBSCRIPTION = "subscription" const val RELATIONSHIP_SETTINGS = "relationship_settings" const val DELETE_ACCOUNT = "delete_account" + const val SECURITY = "security_settings" const val WHEEL_HISTORY = "wheel_history" const val GAME_HISTORY = "game_history" const val THIS_OR_THAT_REPLAY = "this_or_that_replay/{sessionId}" @@ -95,6 +96,7 @@ object AppRoute { Definition(SUBSCRIPTION, "Subscription", "settings"), Definition(RELATIONSHIP_SETTINGS, "Relationship Settings", "settings"), Definition(DELETE_ACCOUNT, "Delete Account", "settings"), + Definition(SECURITY, "Security", "settings"), Definition(WHEEL_HISTORY, "Wheel History", "wheel"), Definition(GAME_HISTORY, "Past Games", "play"), Definition(THIS_OR_THAT_REPLAY, "This or That Results", "play"), @@ -164,6 +166,7 @@ object AppRoute { SUBSCRIPTION, RELATIONSHIP_SETTINGS, DELETE_ACCOUNT, + SECURITY, CONNECTION_CHALLENGES, MEMORY_LANE, WAITING_FOR_PARTNER, diff --git a/app/src/main/java/app/closer/data/local/SettingsDataStore.kt b/app/src/main/java/app/closer/data/local/SettingsDataStore.kt index e20f36b1..06d1c622 100644 --- a/app/src/main/java/app/closer/data/local/SettingsDataStore.kt +++ b/app/src/main/java/app/closer/data/local/SettingsDataStore.kt @@ -36,6 +36,7 @@ class SettingsDataStore @Inject constructor( private val OUTCOME_REMINDER_ENABLED = booleanPreferencesKey("outcome_reminder_enabled") private val OUTCOME_BASELINE_SHOWN_AT = longPreferencesKey("outcome_baseline_shown_at") private val OUTCOME_LAST_PROMPTED_DAY = stringPreferencesKey("outcome_last_prompted_day") + private val BIOMETRIC_LOGIN = booleanPreferencesKey("biometric_login") override val settings: Flow = dataStore.data.map { prefs -> AppSettings( @@ -55,7 +56,8 @@ class SettingsDataStore @Inject constructor( themeMode = ThemeMode.fromStorageValue(prefs[THEME_MODE]), outcomeReminderEnabled = prefs[OUTCOME_REMINDER_ENABLED] ?: true, outcomeBaselineShownAt = prefs[OUTCOME_BASELINE_SHOWN_AT] ?: 0L, - outcomeLastPromptedDay = prefs[OUTCOME_LAST_PROMPTED_DAY] ?: "" + outcomeLastPromptedDay = prefs[OUTCOME_LAST_PROMPTED_DAY] ?: "", + biometricLoginEnabled = prefs[BIOMETRIC_LOGIN] ?: false ) } @@ -97,4 +99,7 @@ class SettingsDataStore @Inject constructor( override suspend fun setThemeMode(mode: ThemeMode) = dataStore.edit { it[THEME_MODE] = mode.name }.let {} + + override suspend fun setBiometricLogin(enabled: Boolean) = + dataStore.edit { it[BIOMETRIC_LOGIN] = enabled }.let {} } diff --git a/app/src/main/java/app/closer/domain/repository/SettingsRepository.kt b/app/src/main/java/app/closer/domain/repository/SettingsRepository.kt index 2d680a43..b60f599a 100644 --- a/app/src/main/java/app/closer/domain/repository/SettingsRepository.kt +++ b/app/src/main/java/app/closer/domain/repository/SettingsRepository.kt @@ -25,7 +25,8 @@ data class AppSettings( val themeMode: ThemeMode = ThemeMode.DEVICE, val outcomeReminderEnabled: Boolean = true, val outcomeBaselineShownAt: Long = 0L, - val outcomeLastPromptedDay: String = "" + val outcomeLastPromptedDay: String = "", + val biometricLoginEnabled: Boolean = false ) interface SettingsRepository { @@ -41,4 +42,5 @@ interface SettingsRepository { suspend fun setOutcomeReminderEnabled(enabled: Boolean) suspend fun markOutcomeBaselineShown() suspend fun setOutcomeLastPromptedDay(dayKey: String) + suspend fun setBiometricLogin(enabled: Boolean) } diff --git a/app/src/main/java/app/closer/ui/settings/SecurityScreen.kt b/app/src/main/java/app/closer/ui/settings/SecurityScreen.kt new file mode 100644 index 00000000..88d6578e --- /dev/null +++ b/app/src/main/java/app/closer/ui/settings/SecurityScreen.kt @@ -0,0 +1,230 @@ +package app.closer.ui.settings + +import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Fingerprint +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.closer.data.local.RecoveryPhraseStore +import app.closer.domain.repository.SettingsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class SecurityUiState( + val biometricLoginEnabled: Boolean = false, + val hasRecoveryPhrase: Boolean = false, + val recoveryPhrase: String? = null +) + +@HiltViewModel +class SecurityViewModel @Inject constructor( + private val settingsRepository: SettingsRepository, + private val recoveryPhraseStore: RecoveryPhraseStore +) : ViewModel() { + + private val _recoveryPhrase = MutableStateFlow(null) + + val uiState: StateFlow = combine( + settingsRepository.settings, + _recoveryPhrase + ) { settings, phrase -> + SecurityUiState( + biometricLoginEnabled = settings.biometricLoginEnabled, + hasRecoveryPhrase = recoveryPhraseStore.load() != null, + recoveryPhrase = phrase + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SecurityUiState()) + + fun setBiometricLogin(enabled: Boolean) { + viewModelScope.launch { settingsRepository.setBiometricLogin(enabled) } + } + + fun revealRecoveryPhrase() { + _recoveryPhrase.update { recoveryPhraseStore.load() } + } + + fun hideRecoveryPhrase() { + _recoveryPhrase.update { null } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SecurityScreen( + onNavigate: (String) -> Unit = {}, + viewModel: SecurityViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + val context = LocalContext.current + + fun launchBiometricForPhrase() { + val activity = context as? FragmentActivity ?: return + BiometricPrompt( + activity, + ContextCompat.getMainExecutor(context), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + viewModel.revealRecoveryPhrase() + } + } + ).authenticate( + BiometricPrompt.PromptInfo.Builder() + .setTitle("Recovery phrase") + .setDescription("Authenticate to view your recovery phrase") + .setAllowedAuthenticators( + androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG or + androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + .build() + ) + } + + state.recoveryPhrase?.let { phrase -> + AlertDialog( + onDismissRequest = viewModel::hideRecoveryPhrase, + title = { Text("Recovery phrase", color = SettingsInk) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + "Write this down and keep it somewhere safe. You'll need it to restore your encrypted history if both devices are lost.", + style = MaterialTheme.typography.bodySmall, + color = SettingsMuted + ) + Text( + phrase, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = SettingsInk + ) + } + }, + confirmButton = { + TextButton(onClick = viewModel::hideRecoveryPhrase) { Text("Done") } + }, + containerColor = SettingsSoft + ) + } + + SettingsSubpage(title = "Security", onBack = { onNavigate("back") }) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .navigationBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(padding) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = SettingsCard) + ) { + Column { + // Biometric login toggle + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon(Icons.Filled.Fingerprint, contentDescription = null, tint = SettingsMuted) + Text( + text = "Biometric login", + style = MaterialTheme.typography.bodyLarge, + color = SettingsInk, + modifier = Modifier.weight(1f) + ) + Switch( + checked = state.biometricLoginEnabled, + onCheckedChange = { viewModel.setBiometricLogin(it) } + ) + } + + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) + + // Recovery phrase row — greyed out until a phrase is stored on-device + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (state.hasRecoveryPhrase) + Modifier.clickable { launchBiometricForPhrase() } + else Modifier + ) + .padding(horizontal = 16.dp, vertical = 14.dp) + .alpha(if (state.hasRecoveryPhrase) 1f else 0.35f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon(Icons.Filled.Lock, contentDescription = null, tint = SettingsMuted) + Text( + text = "Recovery phrase", + style = MaterialTheme.typography.bodyLarge, + color = SettingsInk, + modifier = Modifier.weight(1f) + ) + } + } + } + + if (!state.hasRecoveryPhrase) { + Text( + text = "Recovery phrase becomes available after you invite your partner.", + style = MaterialTheme.typography.bodySmall, + color = SettingsMuted, + modifier = Modifier.padding(horizontal = 4.dp) + ) + } + + Spacer(Modifier.height(8.dp)) + } + } +} diff --git a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt index b08cdc98..06d74dca 100644 --- a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt @@ -47,10 +47,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -62,15 +58,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity import androidx.hilt.navigation.compose.hiltViewModel import app.closer.core.navigation.AppRoute -import app.closer.core.navigation.ExternalLinks import app.closer.domain.model.OutcomeDay import app.closer.ui.components.OutcomeCheckInDialog import app.closer.ui.settings.SettingsDanger @@ -188,30 +180,6 @@ fun SettingsScreen( ) { val state by viewModel.uiState.collectAsState() val snackbar = remember { SnackbarHostState() } - val context = LocalContext.current - - fun launchBiometric() { - val activity = context as? FragmentActivity ?: return - BiometricPrompt( - activity, - ContextCompat.getMainExecutor(context), - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - viewModel.revealRecoveryPhrase() - } - } - ).authenticate( - BiometricPrompt.PromptInfo.Builder() - .setTitle("Recovery phrase") - .setDescription("Authenticate to view your recovery phrase") - .setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_STRONG or - BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) - .build() - ) - } - LaunchedEffect(state.navigateTo) { state.navigateTo?.let { onNavigate(it); viewModel.onNavigated() } } @@ -293,32 +261,6 @@ fun SettingsScreen( } } - state.recoveryPhrase?.let { phrase -> - AlertDialog( - onDismissRequest = viewModel::hideRecoveryPhrase, - title = { Text("Recovery phrase", color = SettingsInk) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - "Write this down and keep it somewhere safe. You'll need it to restore your encrypted history if both devices are lost.", - style = MaterialTheme.typography.bodySmall, - color = SettingsMuted - ) - Text( - phrase, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold, - color = SettingsInk - ) - } - }, - confirmButton = { - TextButton(onClick = viewModel::hideRecoveryPhrase) { Text("Done") } - }, - containerColor = SettingsSoft - ) - } - LaunchedEffect(state.outcomeSubmitSuccess) { if (state.outcomeSubmitSuccess) { snackbar.showSnackbar("Check-in saved") @@ -499,57 +441,21 @@ fun SettingsScreen( Spacer(Modifier.height(8.dp)) - // Security section — visible only when a recovery phrase is stored on-device - if (state.hasRecoveryPhrase) { - Text( - text = "Security", - style = MaterialTheme.typography.labelLarge, - color = SettingsMuted, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) - ) - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = SettingsCard) - ) { - SettingsRow( - icon = Icons.Filled.Lock, - label = "Recovery phrase", - onClick = { launchBiometric() } - ) - } - Spacer(Modifier.height(8.dp)) - } - - // Legal - Text( - text = "Legal", - style = MaterialTheme.typography.labelLarge, - color = SettingsMuted, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) - ) - + // Security Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = SettingsCard) ) { - Column { - SettingsLegalRow( - label = "Privacy Policy", - onClick = { ExternalLinks.openUrl(context, ExternalLinks.PRIVACY_POLICY) } - ) - Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) - SettingsLegalRow( - label = "Terms of Service", - onClick = { ExternalLinks.openUrl(context, ExternalLinks.TERMS_OF_SERVICE) } - ) - } + SettingsRow( + icon = Icons.Filled.Lock, + label = "Security", + onClick = { onNavigate(AppRoute.SECURITY) } + ) } - Spacer(Modifier.height(8.dp)) - // Account lifecycle — separated from legal links + // Account lifecycle Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), @@ -622,32 +528,3 @@ private fun SettingsRow( ) } } - -@Composable -private fun SettingsLegalRow( - label: String, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - color = SettingsInk, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Icon( - Icons.AutoMirrored.Filled.ArrowForwardIos, - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = SettingsMuted - ) - } -} diff --git a/app/src/main/java/app/closer/ui/settings/SettingsViewModel.kt b/app/src/main/java/app/closer/ui/settings/SettingsViewModel.kt index bc39ea3c..b1e6162d 100644 --- a/app/src/main/java/app/closer/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/app/closer/ui/settings/SettingsViewModel.kt @@ -40,9 +40,7 @@ data class SettingsUiState( val outcomeBaselineDialogDue: Boolean = false, val outcomeFollowUpDay: OutcomeDay? = null, val outcomeSubmitSuccess: Boolean = false, - val outcomeError: String? = null, - val hasRecoveryPhrase: Boolean = false, - val recoveryPhrase: String? = null + val outcomeError: String? = null ) @HiltViewModel @@ -113,8 +111,7 @@ class SettingsViewModel @Inject constructor( partnerName = partnerName, isPaired = couple != null, outcomeBaselineDialogDue = outcomeBaselineDialogDue, - outcomeFollowUpDay = followUpDay, - hasRecoveryPhrase = recoveryPhraseStore.load() != null + outcomeFollowUpDay = followUpDay ) } } @@ -176,13 +173,6 @@ class SettingsViewModel @Inject constructor( return if (outcomes.any { it.dayKey == due.first.key }) null else due.first } - fun revealRecoveryPhrase() { - val phrase = recoveryPhraseStore.load() ?: return - _uiState.update { it.copy(recoveryPhrase = phrase) } - } - - fun hideRecoveryPhrase() = _uiState.update { it.copy(recoveryPhrase = null) } - fun signOut() { _uiState.update { it.copy(isSigningOut = true) } viewModelScope.launch { diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 9e8c2274..9674312d 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,4 +1,4 @@ -