diff --git a/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt b/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt index 9f1d8049..80997ae8 100644 --- a/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width 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.automirrored.filled.ArrowBack @@ -45,6 +46,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -145,7 +147,8 @@ fun CreateInviteScreen( text = state.inviteCode!!.chunked(3).joinToString(" – "), style = MaterialTheme.typography.displaySmall, color = SettingsPrimaryDeep, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold ) } } @@ -158,6 +161,7 @@ fun CreateInviteScreen( scope.launch { snackbar.showSnackbar("Code copied!") } }, modifier = Modifier.fillMaxWidth().height(52.dp), + shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors( containerColor = SettingsPrimary, contentColor = SettingsOnPrimary @@ -193,6 +197,22 @@ fun CreateInviteScreen( } Spacer(Modifier.height(32.dp)) + } else { + // Empty / error state when no code is available + Spacer(Modifier.height(48.dp)) + Text( + "No invite code yet", + style = MaterialTheme.typography.headlineSmall, + color = SettingsInk, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(8.dp)) + Text( + "Tap back and try creating an invite again.", + style = MaterialTheme.typography.bodyMedium, + color = SettingsMuted, + textAlign = TextAlign.Center + ) } } } diff --git a/app/src/main/java/app/closer/ui/settings/AccountScreen.kt b/app/src/main/java/app/closer/ui/settings/AccountScreen.kt index 23044cc7..1305f7ae 100644 --- a/app/src/main/java/app/closer/ui/settings/AccountScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/AccountScreen.kt @@ -1,32 +1,197 @@ package app.closer.ui.settings +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.layout.size +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.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Person +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.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +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.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import app.closer.core.navigation.AppRoute -import app.closer.ui.components.PlaceholderAction -import app.closer.ui.components.PlaceholderScreen +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountScreen( onNavigate: (String) -> Unit = {} ) { - PlaceholderScreen( - title = "Your account", - section = "Settings", - description = "Manage identity, sign-in, exports, and account choices in one calm place.", - route = AppRoute.ACCOUNT, - onNavigate = onNavigate, - accent = Color(0xFFB98AF4), - primaryAction = PlaceholderAction("Notifications", AppRoute.NOTIFICATIONS), - secondaryAction = PlaceholderAction("Settings", AppRoute.SETTINGS), - chips = listOf("Identity", "Export", "Account care"), - details = listOf( - "Keep profile details separate from relationship reflections", - "Find export and account care options together", - "Adjust notification settings from the same area" + Scaffold( + containerColor = Color.Transparent, + modifier = Modifier.background(SettingsBackgroundBrush), + topBar = { + TopAppBar( + title = { Text("Account", color = SettingsInk) }, + navigationIcon = { + IconButton(onClick = { onNavigate("back") }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = SettingsInk + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .navigationBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(padding) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Signed-out / local mode state — no fake personal data + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = SettingsCard) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + Icons.Filled.Person, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = SettingsPrimaryDeep + ) + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "Local profile", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = SettingsInk + ) + Text( + text = "Your profile is stored on this device. Sign in later to back it up and connect with your partner.", + style = MaterialTheme.typography.bodySmall, + color = SettingsMuted + ) + } + } + } + + Spacer(Modifier.height(4.dp)) + + // Identity / sync / export — disabled until auth is live + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = SettingsCard) + ) { + Column { + AccountRow( + icon = Icons.Filled.Cloud, + label = "Sign in or create account", + enabled = false, + onClick = { /* Auth coming soon */ } + ) + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) + AccountRow( + icon = Icons.Filled.Download, + label = "Export your data", + enabled = false, + onClick = { /* Export coming soon */ } + ) + } + } + + Spacer(Modifier.height(8.dp)) + + // Account lifecycle + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = SettingsCard) + ) { + AccountRow( + icon = Icons.Filled.Delete, + label = "Delete account", + tint = SettingsDanger, + onClick = { onNavigate(AppRoute.DELETE_ACCOUNT) } + ) + } + } + } +} + +@Composable +private fun AccountRow( + icon: ImageVector, + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + tint: Color = SettingsMuted +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(enabled = enabled, onClick = onClick) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon(icon, contentDescription = null, tint = if (enabled) tint else SettingsMuted.copy(alpha = 0.5f)) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = when { + !enabled -> SettingsMuted.copy(alpha = 0.5f) + tint == SettingsMuted -> SettingsInk + else -> tint + }, + modifier = Modifier.weight(1f) ) - ) + if (enabled) { + Icon( + Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = SettingsMuted + ) + } + } } @Preview diff --git a/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt b/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt index 7c25af52..4bd3fbe9 100644 --- a/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt @@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.clickable +import androidx.compose.material3.Checkbox import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog @@ -45,6 +47,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight @@ -55,6 +58,8 @@ import androidx.hilt.navigation.compose.hiltViewModel data class DeleteAccountUiState( val showConfirm: Boolean = false, val isDeleting: Boolean = false, + val canDelete: Boolean = false, + val acknowledged: Boolean = false, val error: String? = null, val navigateTo: String? = null ) @@ -70,8 +75,10 @@ class DeleteAccountViewModel @Inject constructor( fun requestDelete() = _uiState.update { it.copy(showConfirm = true) } fun dismissDelete() = _uiState.update { it.copy(showConfirm = false) } + fun setAcknowledged(value: Boolean) = _uiState.update { it.copy(acknowledged = value, canDelete = value) } fun confirmDelete() { + if (!uiState.value.canDelete) return val uid = authRepository.currentUserId viewModelScope.launch { _uiState.update { it.copy(showConfirm = false, isDeleting = true, error = null) } @@ -108,11 +115,28 @@ fun DeleteAccountScreen( onDismissRequest = viewModel::dismissDelete, title = { Text("Delete your account?") }, text = { - Text("This permanently removes your profile and sign-in. Your partner will be unpaired. This cannot be undone.") + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("This permanently removes your profile and sign-in. Your partner will be unpaired. This cannot be undone.") + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().clickable { viewModel.setAcknowledged(!state.acknowledged) } + ) { + Checkbox( + checked = state.acknowledged, + onCheckedChange = viewModel::setAcknowledged + ) + Text( + text = "I understand this cannot be undone", + style = MaterialTheme.typography.bodyMedium + ) + } + } }, confirmButton = { Button( onClick = viewModel::confirmDelete, + enabled = state.canDelete, colors = ButtonDefaults.buttonColors( containerColor = SettingsDanger, contentColor = Color.White diff --git a/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt index 2e2132a5..0738c4fd 100644 --- a/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt @@ -110,6 +110,7 @@ fun RelationshipSettingsScreen( confirmButton = { Button( onClick = viewModel::confirmLeave, + enabled = !state.isLeaving, colors = ButtonDefaults.buttonColors( containerColor = SettingsDanger, contentColor = Color.White 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 80c5d771..57a002ff 100644 --- a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt @@ -4,6 +4,7 @@ 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -17,6 +18,7 @@ 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.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder @@ -33,6 +35,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -51,6 +54,110 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import app.closer.core.navigation.AppRoute +import app.closer.ui.settings.SettingsDanger +import app.closer.ui.settings.SettingsInk +import app.closer.ui.settings.SettingsMuted +import app.closer.ui.settings.SettingsPrimaryDeep +import app.closer.ui.settings.SettingsSoft + +// ==================== +// Settings Subpages +// ==================== + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsSubpage( + title: String, + onBack: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable (PaddingValues) -> Unit +) { + Scaffold( + containerColor = Color.Transparent, + modifier = modifier.background(SettingsBackgroundBrush), + topBar = { + TopAppBar( + title = { Text(title, color = SettingsInk) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = SettingsInk + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) + ) + } + ) { padding -> content(padding) } +} + +@Composable +fun SettingsSection( + title: String? = null, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(SettingsCard, RoundedCornerShape(16.dp)) + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.labelMedium, + color = SettingsMuted, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + content() + } +} + +@Composable +fun SettingsSectionDivider() { + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) +} + +// ==================== +// Account State Models +// ==================== + +/** Signed-out local mode - no account data yet */ +private data class SignedOutState( + val displayName: String = "Guest", + val email: String = "", + val isPaired: Boolean = false, + val partnerName: String? = null +) + +/** Signed-in local mode - account exists but no pairing */ +private data class SignedInUnpairedState( + val displayName: String, + val email: String, + val isPaired: Boolean = false, + val partnerName: String? = null +) + +/** Signed-in paired state */ +private data class SignedInPairedState( + val displayName: String, + val email: String, + val isPaired: Boolean = true, + val partnerName: String +) + +/** Account state (use when real account data is ready) */ +private data class AccountState( + val displayName: String = "", + val email: String = "", + val isPaired: Boolean = false, + val partnerName: String? = null +) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -99,14 +206,9 @@ fun SettingsScreen( .padding(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - // Profile card + // Profile card — editable identity (currently local-only) Card( - onClick = { - onNavigate( - if (state.isPaired) AppRoute.RELATIONSHIP_SETTINGS - else AppRoute.CREATE_INVITE - ) - }, + onClick = { onNavigate(AppRoute.ACCOUNT) }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = SettingsCard) @@ -122,25 +224,32 @@ fun SettingsScreen( modifier = Modifier.size(40.dp), tint = SettingsPrimaryDeep ) - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { Text( text = state.displayName.ifBlank { "No name set" }, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = SettingsInk ) - if (state.email.isNotBlank()) { - Text( - text = state.email, - style = MaterialTheme.typography.bodySmall, - color = SettingsMuted - ) - } + Text( + text = if (state.email.isNotBlank()) state.email else "Local profile — sign in to sync your account", + style = MaterialTheme.typography.bodySmall, + color = SettingsMuted + ) } + Icon( + Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = SettingsPrimaryDeep + ) } } - // Partner card + // Partner card — fully tappable, visually obvious Card( onClick = { onNavigate( @@ -153,7 +262,9 @@ fun SettingsScreen( colors = CardDefaults.cardColors(containerColor = SettingsCard) ) { Row( - modifier = Modifier.padding(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { @@ -227,20 +338,13 @@ fun SettingsScreen( Spacer(Modifier.height(8.dp)) - // Danger zone + // Account lifecycle — separated from legal links Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = SettingsCard) ) { Column { - SettingsRow( - icon = Icons.Filled.Favorite, - label = "Leave couple", - onClick = { onNavigate(AppRoute.RELATIONSHIP_SETTINGS) }, - tint = SettingsDanger - ) - Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) SettingsRow( icon = Icons.Filled.Warning, label = "Delete account",