fix(daily-question): deterministic per-day offset replaces RANDOM(); shared DailyQuestionResolver; auth profile fallback
This commit is contained in:
parent
6d74c6acec
commit
77208ff1e6
|
|
@ -12,20 +12,42 @@ interface QuestionDao {
|
|||
@Query("SELECT * FROM question WHERE id = :id AND status = 'active' AND TRIM(text) <> '' LIMIT 1")
|
||||
suspend fun getQuestionById(id: String): QuestionEntity?
|
||||
|
||||
@Query("SELECT * FROM question WHERE status = 'active' AND is_premium = 0 AND TRIM(text) <> '' AND category_id <> 'unknown' ORDER BY RANDOM() LIMIT 1")
|
||||
suspend fun getDailyQuestion(): QuestionEntity?
|
||||
// Daily-question pools are selected DETERMINISTICALLY per day: a stable `ORDER BY id`
|
||||
// plus a date-derived OFFSET (computed in the repository). RANDOM() previously returned a
|
||||
// different question on every call, which made the daily question change between reloads
|
||||
// (breaking "already answered" detection → re-asking) and differ between the two partners.
|
||||
// Ordering by id + a shared day offset yields the same question across reloads and across
|
||||
// both partners' devices (they share this question DB).
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND tags LIKE '%' || :modeTag || '%' AND TRIM(text) <> '' ORDER BY RANDOM() LIMIT 1")
|
||||
suspend fun getDailyQuestionByModeTag(modeTag: String): QuestionEntity?
|
||||
@Query("SELECT COUNT(*) FROM question WHERE status = 'active' AND is_premium = 0 AND TRIM(text) <> '' AND category_id <> 'unknown'")
|
||||
suspend fun countDailyQuestion(): Int
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND tags LIKE '%quick_answer%' AND TRIM(text) <> '' ORDER BY RANDOM() LIMIT 1")
|
||||
suspend fun getDailyQuestionFromPack(): QuestionEntity?
|
||||
@Query("SELECT * FROM question WHERE status = 'active' AND is_premium = 0 AND TRIM(text) <> '' AND category_id <> 'unknown' ORDER BY id LIMIT 1 OFFSET :offset")
|
||||
suspend fun getDailyQuestionAt(offset: Int): QuestionEntity?
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND is_premium = 0 AND tags LIKE '%' || :modeTag || '%' AND TRIM(text) <> '' ORDER BY RANDOM() LIMIT 1")
|
||||
suspend fun getFreeDailyQuestionByModeTag(modeTag: String): QuestionEntity?
|
||||
@Query("SELECT COUNT(*) FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND tags LIKE '%' || :modeTag || '%' AND TRIM(text) <> ''")
|
||||
suspend fun countDailyQuestionByModeTag(modeTag: String): Int
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND is_premium = 0 AND tags LIKE '%quick_answer%' AND TRIM(text) <> '' ORDER BY RANDOM() LIMIT 1")
|
||||
suspend fun getFreeDailyQuestionFromPack(): QuestionEntity?
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND tags LIKE '%' || :modeTag || '%' AND TRIM(text) <> '' ORDER BY id LIMIT 1 OFFSET :offset")
|
||||
suspend fun getDailyQuestionByModeTagAt(modeTag: String, offset: Int): QuestionEntity?
|
||||
|
||||
@Query("SELECT COUNT(*) FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND tags LIKE '%quick_answer%' AND TRIM(text) <> ''")
|
||||
suspend fun countDailyQuestionFromPack(): Int
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND tags LIKE '%quick_answer%' AND TRIM(text) <> '' ORDER BY id LIMIT 1 OFFSET :offset")
|
||||
suspend fun getDailyQuestionFromPackAt(offset: Int): QuestionEntity?
|
||||
|
||||
@Query("SELECT COUNT(*) FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND is_premium = 0 AND tags LIKE '%' || :modeTag || '%' AND TRIM(text) <> ''")
|
||||
suspend fun countFreeDailyQuestionByModeTag(modeTag: String): Int
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND is_premium = 0 AND tags LIKE '%' || :modeTag || '%' AND TRIM(text) <> '' ORDER BY id LIMIT 1 OFFSET :offset")
|
||||
suspend fun getFreeDailyQuestionByModeTagAt(modeTag: String, offset: Int): QuestionEntity?
|
||||
|
||||
@Query("SELECT COUNT(*) FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND is_premium = 0 AND tags LIKE '%quick_answer%' AND TRIM(text) <> ''")
|
||||
suspend fun countFreeDailyQuestionFromPack(): Int
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = 'daily_fun_mc' AND status = 'active' AND is_premium = 0 AND tags LIKE '%quick_answer%' AND TRIM(text) <> '' ORDER BY id LIMIT 1 OFFSET :offset")
|
||||
suspend fun getFreeDailyQuestionFromPackAt(offset: Int): QuestionEntity?
|
||||
|
||||
@Query("SELECT * FROM question WHERE category_id = :categoryId AND status = 'active' AND TRIM(text) <> '' ORDER BY depth_level ASC, id ASC")
|
||||
suspend fun getQuestionsByCategory(categoryId: String): List<QuestionEntity>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ class FirebaseAuthDataSource @Inject constructor() {
|
|||
|
||||
val currentUserId: String? get() = auth.currentUser?.uid
|
||||
val currentUserEmail: String? get() = auth.currentUser?.email
|
||||
/** Provider profile photo (e.g. the Google avatar) — fallback when the user doc has none yet. */
|
||||
val currentUserPhotoUrl: String? get() = auth.currentUser?.photoUrl?.toString()
|
||||
val currentUserDisplayName: String? get() = auth.currentUser?.displayName
|
||||
val isSignedIn: Boolean get() = auth.currentUser != null
|
||||
val isAnonymous: Boolean get() = auth.currentUser?.isAnonymous ?: false
|
||||
val isEmailVerified: Boolean get() = auth.currentUser?.isEmailVerified ?: false
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class FirebaseAuthRepositoryImpl @Inject constructor(
|
|||
override val authState: Flow<AuthState> = dataSource.authState
|
||||
override val currentUserId: String? get() = dataSource.currentUserId
|
||||
override val currentUserEmail: String? get() = dataSource.currentUserEmail
|
||||
override val currentUserPhotoUrl: String? get() = dataSource.currentUserPhotoUrl
|
||||
override val currentUserDisplayName: String? get() = dataSource.currentUserDisplayName
|
||||
override val isSignedIn: Boolean get() = dataSource.isSignedIn
|
||||
override val isAnonymous: Boolean get() = dataSource.isAnonymous
|
||||
override val isGoogleAccount: Boolean get() = dataSource.isGoogleAccount
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import app.closer.data.local.mapper.toQuestionCategory
|
|||
import app.closer.domain.model.Question
|
||||
import app.closer.domain.model.QuestionCategory
|
||||
import app.closer.domain.repository.QuestionRepository
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -15,22 +16,53 @@ class RoomQuestionRepository @Inject constructor(
|
|||
private val questionDao: QuestionDao,
|
||||
private val categoryDao: CategoryDao
|
||||
) : QuestionRepository {
|
||||
|
||||
/**
|
||||
* Deterministic per-day index into a pool of [count] questions. Seeded by the local
|
||||
* calendar day so the chosen daily question is stable across reloads and identical on
|
||||
* both partners' devices (which share this question DB) — never RANDOM().
|
||||
*/
|
||||
private fun dailyOffset(count: Int): Int =
|
||||
if (count <= 0) 0 else (LocalDate.now().toEpochDay() % count).toInt()
|
||||
|
||||
override suspend fun getDailyQuestion(): Question? {
|
||||
return questionDao.getDailyQuestion()?.toQuestion()
|
||||
val count = questionDao.countDailyQuestion()
|
||||
if (count == 0) return null
|
||||
return questionDao.getDailyQuestionAt(dailyOffset(count))?.toQuestion()
|
||||
}
|
||||
|
||||
override suspend fun getDailyQuestionForMode(modeTag: String, isPremium: Boolean): Question? {
|
||||
return if (isPremium) {
|
||||
questionDao.getDailyQuestionByModeTag(modeTag)?.toQuestion()
|
||||
?: questionDao.getDailyQuestionFromPack()?.toQuestion()
|
||||
?: questionDao.getDailyQuestion()?.toQuestion()
|
||||
dailyByModeTag(modeTag) ?: dailyFromPack() ?: getDailyQuestion()
|
||||
} else {
|
||||
questionDao.getFreeDailyQuestionByModeTag(modeTag)?.toQuestion()
|
||||
?: questionDao.getFreeDailyQuestionFromPack()?.toQuestion()
|
||||
?: questionDao.getDailyQuestion()?.toQuestion()
|
||||
freeDailyByModeTag(modeTag) ?: freeDailyFromPack() ?: getDailyQuestion()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun dailyByModeTag(modeTag: String): Question? {
|
||||
val count = questionDao.countDailyQuestionByModeTag(modeTag)
|
||||
if (count == 0) return null
|
||||
return questionDao.getDailyQuestionByModeTagAt(modeTag, dailyOffset(count))?.toQuestion()
|
||||
}
|
||||
|
||||
private suspend fun dailyFromPack(): Question? {
|
||||
val count = questionDao.countDailyQuestionFromPack()
|
||||
if (count == 0) return null
|
||||
return questionDao.getDailyQuestionFromPackAt(dailyOffset(count))?.toQuestion()
|
||||
}
|
||||
|
||||
private suspend fun freeDailyByModeTag(modeTag: String): Question? {
|
||||
val count = questionDao.countFreeDailyQuestionByModeTag(modeTag)
|
||||
if (count == 0) return null
|
||||
return questionDao.getFreeDailyQuestionByModeTagAt(modeTag, dailyOffset(count))?.toQuestion()
|
||||
}
|
||||
|
||||
private suspend fun freeDailyFromPack(): Question? {
|
||||
val count = questionDao.countFreeDailyQuestionFromPack()
|
||||
if (count == 0) return null
|
||||
return questionDao.getFreeDailyQuestionFromPackAt(dailyOffset(count))?.toQuestion()
|
||||
}
|
||||
|
||||
override suspend fun getQuestionById(id: String): Question? {
|
||||
return questionDao.getQuestionById(id)?.toQuestion()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ interface AuthRepository {
|
|||
val authState: Flow<AuthState>
|
||||
val currentUserId: String?
|
||||
val currentUserEmail: String?
|
||||
val currentUserPhotoUrl: String?
|
||||
val currentUserDisplayName: String?
|
||||
val isSignedIn: Boolean
|
||||
val isAnonymous: Boolean
|
||||
val isGoogleAccount: Boolean
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
package app.closer.domain.usecase
|
||||
|
||||
import app.closer.core.billing.EntitlementChecker
|
||||
import app.closer.core.crash.CrashReporter
|
||||
import app.closer.data.remote.FirestoreAnswerDataSource
|
||||
import app.closer.domain.DailyModeResolver
|
||||
import app.closer.domain.model.Question
|
||||
import app.closer.domain.repository.AuthRepository
|
||||
import app.closer.domain.repository.CoupleRepository
|
||||
import app.closer.domain.repository.QuestionRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Single source of truth for "today's daily question" so the Home card and the daily-question
|
||||
* screen always resolve the SAME question. Previously Home used [QuestionRepository.getDailyQuestion]
|
||||
* (a different pool) while the screen used the Firestore assignment + mode pool — they showed
|
||||
* different questions, so Home's answered/waiting detection never matched what the user answered,
|
||||
* which made it re-ask after answering.
|
||||
*
|
||||
* Resolution order (paired): Firestore assignment → local question by that id → deterministic
|
||||
* mode-tagged daily question → deterministic generic daily question. Unpaired falls straight to
|
||||
* the deterministic local question for today's mode.
|
||||
*/
|
||||
@Singleton
|
||||
class DailyQuestionResolver @Inject constructor(
|
||||
private val questionRepository: QuestionRepository,
|
||||
private val coupleRepository: CoupleRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val firestoreAnswerDataSource: FirestoreAnswerDataSource,
|
||||
private val entitlementChecker: EntitlementChecker,
|
||||
private val crashReporter: CrashReporter,
|
||||
) {
|
||||
data class Resolved(
|
||||
val coupleId: String?,
|
||||
val question: Question?,
|
||||
val date: String,
|
||||
val mode: DailyModeResolver.DailyMode,
|
||||
)
|
||||
|
||||
suspend fun resolve(): Resolved {
|
||||
val mode = DailyModeResolver.resolve()
|
||||
val today = FirestoreAnswerDataSource.todayLocalDateString()
|
||||
val isPremium = runCatching { entitlementChecker.isPremium().first() }.getOrDefault(false)
|
||||
|
||||
val couple = authRepository.currentUserId?.let { uid ->
|
||||
runCatching { coupleRepository.getCoupleForUser(uid) }
|
||||
.onFailure { crashReporter.recordException(it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
if (couple == null) {
|
||||
val q = questionRepository.getDailyQuestionForMode(mode.modeTag, isPremium)
|
||||
?: questionRepository.getDailyQuestion()
|
||||
return Resolved(coupleId = null, question = q, date = today, mode = mode)
|
||||
}
|
||||
|
||||
val coupleId = couple.id
|
||||
val assignment = runCatching {
|
||||
firestoreAnswerDataSource.getDailyQuestionAssignment(coupleId, today)
|
||||
}.onFailure { crashReporter.recordException(it) }.getOrNull()
|
||||
|
||||
val question = if (assignment != null) {
|
||||
questionRepository.getQuestionById(assignment.questionId)
|
||||
?: questionRepository.getDailyQuestionForMode(mode.modeTag, isPremium)
|
||||
?: questionRepository.getDailyQuestion()
|
||||
} else {
|
||||
// No assignment yet — request one, but stay usable with the deterministic local
|
||||
// question (identical on both devices) if the call fails.
|
||||
runCatching {
|
||||
firestoreAnswerDataSource.requestDailyQuestionAssignment(coupleId, today)
|
||||
questionRepository.getQuestionById(
|
||||
firestoreAnswerDataSource.getDailyQuestionAssignment(coupleId, today)?.questionId ?: ""
|
||||
)
|
||||
}.onFailure { crashReporter.recordException(it) }.getOrNull()
|
||||
?: questionRepository.getDailyQuestionForMode(mode.modeTag, isPremium)
|
||||
?: questionRepository.getDailyQuestion()
|
||||
}
|
||||
|
||||
return Resolved(coupleId = coupleId, question = question, date = today, mode = mode)
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,8 @@ class HomeViewModel @Inject constructor(
|
|||
private val sealedRevealManager: SealedRevealManager,
|
||||
private val outcomeRepository: OutcomeRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val activityDataSource: app.closer.data.remote.FirestoreActivityDataSource
|
||||
private val activityDataSource: app.closer.data.remote.FirestoreActivityDataSource,
|
||||
private val dailyQuestionResolver: app.closer.domain.usecase.DailyQuestionResolver
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(HomeUiState())
|
||||
|
|
@ -198,7 +199,9 @@ class HomeViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val dailyQuestion = questionRepository.getDailyQuestion()
|
||||
// Resolve via the shared resolver so Home shows the SAME question as the
|
||||
// daily-question screen (assignment-backed), keeping answered/waiting state in sync.
|
||||
val dailyQuestion = dailyQuestionResolver.resolve().question
|
||||
val categories = questionRepository.getCategories()
|
||||
.take(6)
|
||||
.map { category ->
|
||||
|
|
|
|||
|
|
@ -106,10 +106,18 @@ class PairingSuccessViewModel @Inject constructor(
|
|||
val partnerId = couple?.userIds?.firstOrNull { it != myId }
|
||||
val partner = partnerId?.let { runCatching { userRepository.getUser(it) }.getOrNull() }
|
||||
|
||||
// Fall back to the Firebase Auth profile (e.g. the Google avatar) if the user
|
||||
// doc photo/name hasn't propagated yet, so both faces show on the congrats screen.
|
||||
val myPhoto = me?.photoUrl?.takeIf { it.isNotBlank() }
|
||||
?: authRepository.currentUserPhotoUrl?.takeIf { it.isNotBlank() }
|
||||
?: ""
|
||||
val myName = me?.displayName?.takeIf { n -> n.isNotBlank() }
|
||||
?: authRepository.currentUserDisplayName?.takeIf { n -> n.isNotBlank() }
|
||||
?: "You"
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
myName = me?.displayName?.takeIf { n -> n.isNotBlank() } ?: "You",
|
||||
myPhotoUrl = me?.photoUrl ?: "",
|
||||
myName = myName,
|
||||
myPhotoUrl = myPhoto,
|
||||
partnerName = partner?.displayName?.takeIf { n -> n.isNotBlank() } ?: "Your partner",
|
||||
partnerPhotoUrl = partner?.photoUrl ?: ""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import app.closer.analytics.RetentionAnalytics
|
||||
import app.closer.analytics.RetentionEvent
|
||||
import app.closer.core.billing.EntitlementChecker
|
||||
import app.closer.core.crash.CrashReporter
|
||||
import app.closer.data.remote.FirestoreAnswerDataSource
|
||||
import app.closer.domain.DailyModeResolver
|
||||
|
|
@ -14,7 +13,7 @@ import app.closer.domain.model.Question
|
|||
import app.closer.domain.repository.AuthRepository
|
||||
import app.closer.domain.repository.CoupleRepository
|
||||
import app.closer.domain.repository.LocalAnswerRepository
|
||||
import app.closer.domain.repository.QuestionRepository
|
||||
import app.closer.domain.usecase.DailyQuestionResolver
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import com.google.firebase.firestore.ListenerRegistration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -22,7 +21,6 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -43,13 +41,12 @@ data class LocalQuestionUiState(
|
|||
|
||||
@HiltViewModel
|
||||
class DailyQuestionViewModel @Inject constructor(
|
||||
private val repository: QuestionRepository,
|
||||
private val localAnswerRepository: LocalAnswerRepository,
|
||||
private val firestoreAnswerDataSource: FirestoreAnswerDataSource,
|
||||
private val authRepository: AuthRepository,
|
||||
private val coupleRepository: CoupleRepository,
|
||||
private val crashReporter: CrashReporter,
|
||||
private val entitlementChecker: EntitlementChecker,
|
||||
private val dailyQuestionResolver: DailyQuestionResolver,
|
||||
private val retentionAnalytics: RetentionAnalytics,
|
||||
private val db: FirebaseFirestore
|
||||
) : ViewModel() {
|
||||
|
|
@ -73,9 +70,11 @@ class DailyQuestionViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
_uiState.value = LocalQuestionUiState(isLoading = true)
|
||||
try {
|
||||
val resolvedMode = DailyModeResolver.resolve()
|
||||
val today = FirestoreAnswerDataSource.todayLocalDateString()
|
||||
val (coupleId, question) = loadCoupleAndQuestion(today, resolvedMode)
|
||||
val resolved = dailyQuestionResolver.resolve()
|
||||
val resolvedMode = resolved.mode
|
||||
val today = resolved.date
|
||||
val coupleId = resolved.coupleId
|
||||
val question = resolved.question
|
||||
val answer = question?.let { localAnswerRepository.getAnswer(it.id) }
|
||||
val partnerHasAnswered = coupleId?.let {
|
||||
runCatching { checkPartnerAnswered(it, today) }.getOrDefault(false)
|
||||
|
|
@ -132,57 +131,6 @@ class DailyQuestionViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current couple (if any) and the daily question.
|
||||
*
|
||||
* For paired users, read the couple's assigned daily question from Firestore
|
||||
* so both partners see the same prompt. If no assignment exists yet, request
|
||||
* one from the cloud function and fall back to a local mode-tagged question.
|
||||
*
|
||||
* For unpaired users, fall back to a mode-tagged question from the daily_fun_mc pack.
|
||||
*/
|
||||
private suspend fun loadCoupleAndQuestion(
|
||||
today: String,
|
||||
mode: DailyModeResolver.DailyMode
|
||||
): Pair<String?, Question?> {
|
||||
val isPremium = runCatching { entitlementChecker.isPremium().first() }.getOrDefault(false)
|
||||
|
||||
val couple = authRepository.currentUserId?.let { uid ->
|
||||
runCatching { coupleRepository.getCoupleForUser(uid) }
|
||||
.onFailure { crashReporter.recordException(it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
if (couple == null) {
|
||||
return null to (repository.getDailyQuestionForMode(mode.modeTag, isPremium)
|
||||
?: repository.getDailyQuestion())
|
||||
}
|
||||
|
||||
val coupleId = couple.id
|
||||
val assignment = runCatching {
|
||||
firestoreAnswerDataSource.getDailyQuestionAssignment(coupleId, today)
|
||||
}.onFailure { crashReporter.recordException(it) }.getOrNull()
|
||||
|
||||
val question = if (assignment != null) {
|
||||
repository.getQuestionById(assignment.questionId)
|
||||
?: repository.getDailyQuestionForMode(mode.modeTag, isPremium)
|
||||
?: repository.getDailyQuestion()
|
||||
} else {
|
||||
// No assignment yet. Request immediate assignment, but keep the app
|
||||
// usable with a local mode-tagged question in case the call fails.
|
||||
runCatching {
|
||||
firestoreAnswerDataSource.requestDailyQuestionAssignment(coupleId, today)
|
||||
repository.getQuestionById(
|
||||
firestoreAnswerDataSource.getDailyQuestionAssignment(coupleId, today)?.questionId ?: ""
|
||||
)
|
||||
}.onFailure { crashReporter.recordException(it) }.getOrNull()
|
||||
?: repository.getDailyQuestionForMode(mode.modeTag, isPremium)
|
||||
?: repository.getDailyQuestion()
|
||||
}
|
||||
|
||||
return coupleId to question
|
||||
}
|
||||
|
||||
fun updateWrittenText(text: String) {
|
||||
_uiState.update { it.copy(pendingWrittenText = text, submitted = false) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue