diff --git a/app/src/main/java/com/couplesconnect/app/data/remote/FirebaseAuthDataSource.kt b/app/src/main/java/com/couplesconnect/app/data/remote/FirebaseAuthDataSource.kt index 3421cbdd..92178e44 100644 --- a/app/src/main/java/com/couplesconnect/app/data/remote/FirebaseAuthDataSource.kt +++ b/app/src/main/java/com/couplesconnect/app/data/remote/FirebaseAuthDataSource.kt @@ -17,6 +17,7 @@ class FirebaseAuthDataSource @Inject constructor() { private val auth = FirebaseAuth.getInstance() val currentUserId: String? get() = auth.currentUser?.uid + val currentUserEmail: String? get() = auth.currentUser?.email val isSignedIn: Boolean get() = auth.currentUser != null val authState: Flow = callbackFlow { diff --git a/app/src/main/java/com/couplesconnect/app/data/repository/FirebaseAuthRepositoryImpl.kt b/app/src/main/java/com/couplesconnect/app/data/repository/FirebaseAuthRepositoryImpl.kt index 31c6453a..7bba5e8e 100644 --- a/app/src/main/java/com/couplesconnect/app/data/repository/FirebaseAuthRepositoryImpl.kt +++ b/app/src/main/java/com/couplesconnect/app/data/repository/FirebaseAuthRepositoryImpl.kt @@ -14,6 +14,7 @@ class FirebaseAuthRepositoryImpl @Inject constructor( override val authState: Flow = dataSource.authState override val currentUserId: String? get() = dataSource.currentUserId + override val currentUserEmail: String? get() = dataSource.currentUserEmail override val isSignedIn: Boolean get() = dataSource.isSignedIn override suspend fun signInAnonymously(): Result = diff --git a/app/src/main/java/com/couplesconnect/app/domain/repository/AuthRepository.kt b/app/src/main/java/com/couplesconnect/app/domain/repository/AuthRepository.kt index 5059f0f4..9a3cd5a2 100644 --- a/app/src/main/java/com/couplesconnect/app/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/couplesconnect/app/domain/repository/AuthRepository.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow interface AuthRepository { val authState: Flow val currentUserId: String? + val currentUserEmail: String? val isSignedIn: Boolean suspend fun signInAnonymously(): Result suspend fun signInWithEmail(email: String, password: String): Result diff --git a/app/src/main/java/com/couplesconnect/app/ui/settings/SettingsScreen.kt b/app/src/main/java/com/couplesconnect/app/ui/settings/SettingsScreen.kt index 9f199d35..8a8b46a5 100644 --- a/app/src/main/java/com/couplesconnect/app/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/couplesconnect/app/ui/settings/SettingsScreen.kt @@ -1,36 +1,259 @@ package com.couplesconnect.app.ui.settings +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.ArrowForwardIos +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar 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.tooling.preview.Preview +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.couplesconnect.app.core.navigation.AppRoute -import com.couplesconnect.app.ui.components.PlaceholderAction -import com.couplesconnect.app.ui.components.PlaceholderScreen +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( - onNavigate: (String) -> Unit = {} + onNavigate: (String) -> Unit = {}, + viewModel: SettingsViewModel = hiltViewModel() ) { - PlaceholderScreen( - title = "Tend the edges", - section = "Settings", - description = "The control center for account, privacy, notifications, subscription, and relationship preferences.", - route = AppRoute.SETTINGS, - onNavigate = onNavigate, - accent = Color(0xFF6C8EA4), - primaryAction = PlaceholderAction("Account", AppRoute.ACCOUNT), - secondaryAction = PlaceholderAction("Privacy", AppRoute.PRIVACY), - chips = listOf("Preferences", "Boundaries", "Careful controls"), - details = listOf( - "Personal settings stay separate from couple content", - "Privacy and notifications have focused places", - "Subscription management can connect to paywall later" - ) - ) + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(state.navigateTo) { + state.navigateTo?.let { onNavigate(it); viewModel.onNavigated() } + } + + Scaffold( + topBar = { + TopAppBar(title = { Text("Settings", style = MaterialTheme.typography.titleLarge) }) + } + ) { padding -> + if (state.isLoading) { + Column( + modifier = Modifier.fillMaxSize().padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CircularProgressIndicator() + } + } else { + Column( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .navigationBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(padding) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Profile card + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + 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 = MaterialTheme.colorScheme.primary + ) + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = state.displayName.ifBlank { "No name set" }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + if (state.email.isNotBlank()) { + Text( + text = state.email, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + + // Partner card + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + if (state.isPaired) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = if (state.isPaired) Color(0xFFE07A5F) else MaterialTheme.colorScheme.onSurfaceVariant + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = if (state.isPaired) "Connected with" else "No partner yet", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (state.isPaired) { + Text( + text = state.partnerName ?: "Your partner", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + } else { + Text( + text = "Invite someone to connect", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + if (!state.isPaired) { + Icon( + Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = null, + modifier = Modifier + .size(16.dp) + .clickable { onNavigate(AppRoute.CREATE_INVITE) }, + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + + Spacer(Modifier.height(4.dp)) + + // General section + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + Column { + SettingsRow( + icon = Icons.Filled.Notifications, + label = "Notifications", + onClick = { onNavigate(AppRoute.NOTIFICATIONS) } + ) + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) + SettingsRow( + icon = Icons.Filled.Star, + label = "Subscription", + onClick = { onNavigate(AppRoute.SUBSCRIPTION) } + ) + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) + SettingsRow( + icon = Icons.Filled.Lock, + label = "Privacy & Terms", + onClick = { onNavigate(AppRoute.PRIVACY) } + ) + } + } + + Spacer(Modifier.height(8.dp)) + + OutlinedButton( + onClick = viewModel::signOut, + enabled = !state.isSigningOut, + modifier = Modifier.fillMaxWidth().height(52.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + if (state.isSigningOut) CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.error, + strokeWidth = 2.dp + ) + else Text("Sign out", style = MaterialTheme.typography.labelLarge) + } + + Spacer(Modifier.height(16.dp)) + } + } + } } -@Preview @Composable -fun SettingsScreenPreview() { - SettingsScreen() +private fun SettingsRow( + icon: ImageVector, + label: String, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + Icon( + Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } diff --git a/app/src/main/java/com/couplesconnect/app/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/couplesconnect/app/ui/settings/SettingsViewModel.kt new file mode 100644 index 00000000..140ed10f --- /dev/null +++ b/app/src/main/java/com/couplesconnect/app/ui/settings/SettingsViewModel.kt @@ -0,0 +1,75 @@ +package com.couplesconnect.app.ui.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.couplesconnect.app.core.navigation.AppRoute +import com.couplesconnect.app.domain.repository.AuthRepository +import com.couplesconnect.app.domain.repository.CoupleRepository +import com.couplesconnect.app.domain.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class SettingsUiState( + val isLoading: Boolean = true, + val displayName: String = "", + val email: String = "", + val partnerName: String? = null, + val isPaired: Boolean = false, + val isSigningOut: Boolean = false, + val navigateTo: String? = null +) + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val authRepository: AuthRepository, + private val userRepository: UserRepository, + private val coupleRepository: CoupleRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(SettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadSettings() + } + + private fun loadSettings() { + viewModelScope.launch { + val userId = authRepository.currentUserId ?: run { + _uiState.update { it.copy(isLoading = false) } + return@launch + } + val email = authRepository.currentUserEmail ?: "" + val user = runCatching { userRepository.getUser(userId) }.getOrNull() + val couple = coupleRepository.getCoupleForUser(userId) + val partnerId = couple?.userIds?.firstOrNull { it != userId } + val partnerName = partnerId?.let { + runCatching { userRepository.getUser(it)?.displayName }.getOrNull() + } + _uiState.update { + it.copy( + isLoading = false, + displayName = user?.displayName ?: "", + email = email, + partnerName = partnerName, + isPaired = couple != null + ) + } + } + } + + fun signOut() { + _uiState.update { it.copy(isSigningOut = true) } + viewModelScope.launch { + authRepository.signOut() + _uiState.update { it.copy(isSigningOut = false, navigateTo = AppRoute.ONBOARDING) } + } + } + + fun onNavigated() = _uiState.update { it.copy(navigateTo = null) } +}