From 11a81cb8261719b7c6133cf38811413d3fa56bc1 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 16 Jun 2026 01:07:13 -0500 Subject: [PATCH] feat(settings): DataStore-based settings repo, notification preferences, special dates component --- .../app/data/local/SettingsDataStore.kt | 49 +++++ .../couplesconnect/app/di/DatabaseModule.kt | 15 +- .../couplesconnect/app/di/RepositoryModule.kt | 5 + .../domain/repository/SettingsRepository.kt | 20 ++ .../app/ui/components/SpecialDatesSection.kt | 181 ++++++++++++++++++ .../couplesconnect/app/ui/home/HomeScreen.kt | 2 + .../ui/settings/NotificationSettingsScreen.kt | 33 +++- 7 files changed, 293 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/couplesconnect/app/data/local/SettingsDataStore.kt create mode 100644 app/src/main/java/com/couplesconnect/app/domain/repository/SettingsRepository.kt create mode 100644 app/src/main/java/com/couplesconnect/app/ui/components/SpecialDatesSection.kt diff --git a/app/src/main/java/com/couplesconnect/app/data/local/SettingsDataStore.kt b/app/src/main/java/com/couplesconnect/app/data/local/SettingsDataStore.kt new file mode 100644 index 00000000..c277869f --- /dev/null +++ b/app/src/main/java/com/couplesconnect/app/data/local/SettingsDataStore.kt @@ -0,0 +1,49 @@ +package com.couplesconnect.app.data.local + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import com.couplesconnect.app.domain.repository.AppSettings +import com.couplesconnect.app.domain.repository.SettingsRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SettingsDataStore @Inject constructor( + private val dataStore: DataStore +) : SettingsRepository { + + private val DAILY_REMINDER = booleanPreferencesKey("daily_reminder") + private val PARTNER_ANSWERED = booleanPreferencesKey("partner_answered") + private val STREAK_REMINDER = booleanPreferencesKey("streak_reminder") + private val QUIET_HOURS = booleanPreferencesKey("quiet_hours") + private val ONBOARDING_COMPLETE = booleanPreferencesKey("onboarding_complete") + + override val settings: Flow = dataStore.data.map { prefs -> + AppSettings( + dailyReminderEnabled = prefs[DAILY_REMINDER] ?: true, + partnerAnsweredEnabled = prefs[PARTNER_ANSWERED] ?: true, + streakReminderEnabled = prefs[STREAK_REMINDER] ?: false, + quietHoursEnabled = prefs[QUIET_HOURS] ?: false, + onboardingComplete = prefs[ONBOARDING_COMPLETE] ?: false + ) + } + + override suspend fun setDailyReminder(enabled: Boolean) = + dataStore.edit { it[DAILY_REMINDER] = enabled }.let {} + + override suspend fun setPartnerAnswered(enabled: Boolean) = + dataStore.edit { it[PARTNER_ANSWERED] = enabled }.let {} + + override suspend fun setStreakReminder(enabled: Boolean) = + dataStore.edit { it[STREAK_REMINDER] = enabled }.let {} + + override suspend fun setQuietHours(enabled: Boolean) = + dataStore.edit { it[QUIET_HOURS] = enabled }.let {} + + override suspend fun setOnboardingComplete(complete: Boolean) = + dataStore.edit { it[ONBOARDING_COMPLETE] = complete }.let {} +} diff --git a/app/src/main/java/com/couplesconnect/app/di/DatabaseModule.kt b/app/src/main/java/com/couplesconnect/app/di/DatabaseModule.kt index 4d284aad..d6a539c9 100644 --- a/app/src/main/java/com/couplesconnect/app/di/DatabaseModule.kt +++ b/app/src/main/java/com/couplesconnect/app/di/DatabaseModule.kt @@ -1,13 +1,17 @@ package com.couplesconnect.app.di +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.room.Room import com.couplesconnect.app.data.local.AppDatabase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import androidx.room.Room -import android.content.Context import javax.inject.Singleton @Module @@ -33,4 +37,11 @@ object DatabaseModule { @Provides @Singleton fun provideCategoryDao(db: AppDatabase) = db.categoryDao() + + @Provides + @Singleton + fun provideDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create { + context.preferencesDataStoreFile("settings") + } } diff --git a/app/src/main/java/com/couplesconnect/app/di/RepositoryModule.kt b/app/src/main/java/com/couplesconnect/app/di/RepositoryModule.kt index 519752f1..989dc2e8 100644 --- a/app/src/main/java/com/couplesconnect/app/di/RepositoryModule.kt +++ b/app/src/main/java/com/couplesconnect/app/di/RepositoryModule.kt @@ -2,6 +2,7 @@ package com.couplesconnect.app.di import com.couplesconnect.app.core.billing.EntitlementChecker import com.couplesconnect.app.core.billing.FakeEntitlementChecker +import com.couplesconnect.app.data.local.SettingsDataStore import com.couplesconnect.app.data.repository.CoupleRepositoryImpl import com.couplesconnect.app.data.repository.FirebaseAuthRepositoryImpl import com.couplesconnect.app.data.repository.InviteRepositoryImpl @@ -15,6 +16,7 @@ import com.couplesconnect.app.domain.repository.InviteRepository import com.couplesconnect.app.domain.repository.LocalAnswerRepository import com.couplesconnect.app.domain.repository.QuestionRepository import com.couplesconnect.app.domain.repository.QuestionThreadRepository +import com.couplesconnect.app.domain.repository.SettingsRepository import com.couplesconnect.app.domain.repository.UserRepository import dagger.Binds import dagger.Module @@ -49,4 +51,7 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindEntitlementChecker(impl: FakeEntitlementChecker): EntitlementChecker + + @Binds @Singleton + abstract fun bindSettingsRepository(impl: SettingsDataStore): SettingsRepository } diff --git a/app/src/main/java/com/couplesconnect/app/domain/repository/SettingsRepository.kt b/app/src/main/java/com/couplesconnect/app/domain/repository/SettingsRepository.kt new file mode 100644 index 00000000..6aba184c --- /dev/null +++ b/app/src/main/java/com/couplesconnect/app/domain/repository/SettingsRepository.kt @@ -0,0 +1,20 @@ +package com.couplesconnect.app.domain.repository + +import kotlinx.coroutines.flow.Flow + +data class AppSettings( + val dailyReminderEnabled: Boolean = true, + val partnerAnsweredEnabled: Boolean = true, + val streakReminderEnabled: Boolean = false, + val quietHoursEnabled: Boolean = false, + val onboardingComplete: Boolean = false +) + +interface SettingsRepository { + val settings: Flow + suspend fun setDailyReminder(enabled: Boolean) + suspend fun setPartnerAnswered(enabled: Boolean) + suspend fun setStreakReminder(enabled: Boolean) + suspend fun setQuietHours(enabled: Boolean) + suspend fun setOnboardingComplete(complete: Boolean) +} diff --git a/app/src/main/java/com/couplesconnect/app/ui/components/SpecialDatesSection.kt b/app/src/main/java/com/couplesconnect/app/ui/components/SpecialDatesSection.kt new file mode 100644 index 00000000..f9cbbcef --- /dev/null +++ b/app/src/main/java/com/couplesconnect/app/ui/components/SpecialDatesSection.kt @@ -0,0 +1,181 @@ +package com.couplesconnect.app.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cake +import androidx.compose.material.icons.filled.CardGiftcard +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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 androidx.compose.ui.unit.sp + +private val Purple = Color(0xFF7C6F9E) +private val PurpleLight = Color(0xFFF0EDF9) +private val GreenPill = Color(0xFF81B29A) + +@Composable +fun SpecialDatesSection(modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Your Special Dates", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF27211F) + ) + + // Anniversary featured card + Surface( + shape = RoundedCornerShape(20.dp), + color = PurpleLight, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + DateBlock(day = "14", month = "Jul", tint = Purple) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Your Anniversary", + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF27211F) + ) + TodayPill() + } + Text( + text = "Added by Jessica", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF4E4642) + ) + } + + Icon( + imageVector = Icons.Filled.CardGiftcard, + contentDescription = null, + tint = Purple, + modifier = Modifier.size(24.dp) + ) + } + } + + // Birthday rows + BirthdayRow(name = "Jessica", day = "10", month = "May") + BirthdayRow(name = "Mark", day = "25", month = "Aug") + } + } +} + +@Composable +private fun BirthdayRow(name: String, day: String, month: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + DateBlock(day = day, month = month, tint = Purple, compact = true) + + Text( + text = "$name's Birthday", + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF27211F), + modifier = Modifier.weight(1f) + ) + + Icon( + imageVector = Icons.Filled.Cake, + contentDescription = null, + tint = Purple.copy(alpha = 0.7f), + modifier = Modifier.size(20.dp) + ) + } +} + +@Composable +private fun DateBlock( + day: String, + month: String, + tint: Color, + compact: Boolean = false +) { + val size = if (compact) 44.dp else 54.dp + Box( + modifier = Modifier + .size(size) + .background(tint.copy(alpha = 0.12f), RoundedCornerShape(12.dp)), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = day, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = tint, + fontSize = if (compact) 14.sp else 18.sp, + lineHeight = if (compact) 16.sp else 20.sp + ) + Text( + text = month, + style = MaterialTheme.typography.labelSmall, + color = tint, + fontSize = if (compact) 9.sp else 10.sp + ) + } + } +} + +@Composable +private fun TodayPill() { + Surface( + shape = RoundedCornerShape(999.dp), + color = GreenPill + ) { + Text( + text = "Today", + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), + style = MaterialTheme.typography.labelSmall, + color = Color.White, + fontWeight = FontWeight.SemiBold + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFBFA) +@Composable +fun SpecialDatesSectionPreview() { + SpecialDatesSection() +} diff --git a/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt b/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt index ea36f773..d6cb096c 100644 --- a/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt @@ -40,6 +40,7 @@ import com.couplesconnect.app.domain.model.LocalAnswer import com.couplesconnect.app.domain.model.Question import com.couplesconnect.app.domain.model.QuestionCategory import com.couplesconnect.app.ui.answers.revealSummary +import com.couplesconnect.app.ui.components.SpecialDatesSection import com.couplesconnect.app.ui.questions.displayCategoryName @Composable @@ -112,6 +113,7 @@ private fun HomeContent( latest = state.answerStats.latest, onHistory = onHistory ) + SpecialDatesSection() CategoryPreviewGrid( categories = state.categories, onCategory = onCategory, diff --git a/app/src/main/java/com/couplesconnect/app/ui/settings/NotificationSettingsScreen.kt b/app/src/main/java/com/couplesconnect/app/ui/settings/NotificationSettingsScreen.kt index 3d879865..233d59cb 100644 --- a/app/src/main/java/com/couplesconnect/app/ui/settings/NotificationSettingsScreen.kt +++ b/app/src/main/java/com/couplesconnect/app/ui/settings/NotificationSettingsScreen.kt @@ -1,12 +1,15 @@ package com.couplesconnect.app.ui.settings import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.couplesconnect.app.domain.repository.SettingsRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -53,15 +56,25 @@ data class NotificationSettingsUiState( ) @HiltViewModel -class NotificationSettingsViewModel @Inject constructor() : ViewModel() { +class NotificationSettingsViewModel @Inject constructor( + private val settingsRepository: SettingsRepository +) : ViewModel() { - private val _uiState = MutableStateFlow(NotificationSettingsUiState()) - val uiState: StateFlow = _uiState.asStateFlow() + val uiState: StateFlow = settingsRepository.settings + .map { s -> + NotificationSettingsUiState( + dailyReminderEnabled = s.dailyReminderEnabled, + partnerAnsweredEnabled = s.partnerAnsweredEnabled, + streakReminderEnabled = s.streakReminderEnabled, + quietHoursEnabled = s.quietHoursEnabled + ) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), NotificationSettingsUiState()) - fun toggleDailyReminder(on: Boolean) = _uiState.update { it.copy(dailyReminderEnabled = on) } - fun togglePartnerAnswered(on: Boolean) = _uiState.update { it.copy(partnerAnsweredEnabled = on) } - fun toggleStreakReminder(on: Boolean) = _uiState.update { it.copy(streakReminderEnabled = on) } - fun toggleQuietHours(on: Boolean) = _uiState.update { it.copy(quietHoursEnabled = on) } + fun toggleDailyReminder(on: Boolean) = viewModelScope.launch { settingsRepository.setDailyReminder(on) } + fun togglePartnerAnswered(on: Boolean) = viewModelScope.launch { settingsRepository.setPartnerAnswered(on) } + fun toggleStreakReminder(on: Boolean) = viewModelScope.launch { settingsRepository.setStreakReminder(on) } + fun toggleQuietHours(on: Boolean) = viewModelScope.launch { settingsRepository.setQuietHours(on) } } @OptIn(ExperimentalMaterial3Api::class)