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 4d96447366
commit 6089e8a822
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 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 com.couplesconnect.app.data.local.AppDatabase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import androidx.room.Room
import android.content.Context
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -33,4 +37,11 @@ object DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun provideCategoryDao(db: AppDatabase) = db.categoryDao() 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.EntitlementChecker
import com.couplesconnect.app.core.billing.FakeEntitlementChecker 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.CoupleRepositoryImpl
import com.couplesconnect.app.data.repository.FirebaseAuthRepositoryImpl import com.couplesconnect.app.data.repository.FirebaseAuthRepositoryImpl
import com.couplesconnect.app.data.repository.InviteRepositoryImpl 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.LocalAnswerRepository
import com.couplesconnect.app.domain.repository.QuestionRepository import com.couplesconnect.app.domain.repository.QuestionRepository
import com.couplesconnect.app.domain.repository.QuestionThreadRepository import com.couplesconnect.app.domain.repository.QuestionThreadRepository
import com.couplesconnect.app.domain.repository.SettingsRepository
import com.couplesconnect.app.domain.repository.UserRepository import com.couplesconnect.app.domain.repository.UserRepository
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -49,4 +51,7 @@ abstract class RepositoryModule {
@Binds @Singleton @Binds @Singleton
abstract fun bindEntitlementChecker(impl: FakeEntitlementChecker): EntitlementChecker 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.Question
import com.couplesconnect.app.domain.model.QuestionCategory import com.couplesconnect.app.domain.model.QuestionCategory
import com.couplesconnect.app.ui.answers.revealSummary import com.couplesconnect.app.ui.answers.revealSummary
import com.couplesconnect.app.ui.components.SpecialDatesSection
import com.couplesconnect.app.ui.questions.displayCategoryName import com.couplesconnect.app.ui.questions.displayCategoryName
@Composable @Composable
@ -112,6 +113,7 @@ private fun HomeContent(
latest = state.answerStats.latest, latest = state.answerStats.latest,
onHistory = onHistory onHistory = onHistory
) )
SpecialDatesSection()
CategoryPreviewGrid( CategoryPreviewGrid(
categories = state.categories, categories = state.categories,
onCategory = onCategory, onCategory = onCategory,

View File

@ -1,12 +1,15 @@
package com.couplesconnect.app.ui.settings package com.couplesconnect.app.ui.settings
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.couplesconnect.app.domain.repository.SettingsRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -53,15 +56,25 @@ data class NotificationSettingsUiState(
) )
@HiltViewModel @HiltViewModel
class NotificationSettingsViewModel @Inject constructor() : ViewModel() { class NotificationSettingsViewModel @Inject constructor(
private val settingsRepository: SettingsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(NotificationSettingsUiState()) val uiState: StateFlow<NotificationSettingsUiState> = settingsRepository.settings
val uiState: StateFlow<NotificationSettingsUiState> = _uiState.asStateFlow() .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 toggleDailyReminder(on: Boolean) = viewModelScope.launch { settingsRepository.setDailyReminder(on) }
fun togglePartnerAnswered(on: Boolean) = _uiState.update { it.copy(partnerAnsweredEnabled = on) } fun togglePartnerAnswered(on: Boolean) = viewModelScope.launch { settingsRepository.setPartnerAnswered(on) }
fun toggleStreakReminder(on: Boolean) = _uiState.update { it.copy(streakReminderEnabled = on) } fun toggleStreakReminder(on: Boolean) = viewModelScope.launch { settingsRepository.setStreakReminder(on) }
fun toggleQuietHours(on: Boolean) = _uiState.update { it.copy(quietHoursEnabled = on) } fun toggleQuietHours(on: Boolean) = viewModelScope.launch { settingsRepository.setQuietHours(on) }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)