feat(settings): DataStore-based settings repo, notification preferences, special dates component

This commit is contained in:
null 2026-06-16 01:07:13 -05:00
parent 88004cf219
commit 11a81cb826
7 changed files with 293 additions and 12 deletions

View File

@ -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<Preferences>
) : 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<AppSettings> = 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 {}
}

View File

@ -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<Preferences> =
PreferenceDataStoreFactory.create {
context.preferencesDataStoreFile("settings")
}
}

View File

@ -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
}

View File

@ -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<AppSettings>
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)
}

View File

@ -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()
}

View File

@ -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,

View File

@ -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<NotificationSettingsUiState> = _uiState.asStateFlow()
val uiState: StateFlow<NotificationSettingsUiState> = 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)