fix(wheel-reveal): error state with retry, null-safe uid/couple, close on snapshot error

This commit is contained in:
null 2026-06-23 11:34:46 -05:00
parent acaa8e635c
commit 6977db7600
2 changed files with 77 additions and 7 deletions

View File

@ -77,7 +77,8 @@ class FirestoreWheelAnswerDataSource @Inject constructor(
/** Live view of both partners' answers; emits whenever either side submits. */
fun observe(coupleId: String, sessionId: String): Flow<WheelRevealDoc> = callbackFlow {
val reg = doc(coupleId, sessionId).addSnapshotListener { snap, err ->
if (err != null || snap == null) return@addSnapshotListener
if (err != null) { close(err); return@addSnapshotListener }
if (snap == null) return@addSnapshotListener
trySend(parse(snap, coupleId))
}
awaitClose { reg.remove() }

View File

@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
// ── ViewModel ──────────────────────────────────────────────────────────────────
enum class WheelRevealPhase { LOADING, WAITING, REVEAL }
enum class WheelRevealPhase { LOADING, WAITING, REVEAL, ERROR }
/** One prompt with both partners' answers, ready to render side by side. */
data class WheelRevealItem(
@ -80,7 +80,8 @@ data class WheelCompleteUiState(
val categoryName: String = "",
val partnerName: String = "Your partner",
val items: List<WheelRevealItem> = emptyList(),
val navigateTo: String? = null
val navigateTo: String? = null,
val error: String? = null
)
@HiltViewModel
@ -105,8 +106,12 @@ class WheelCompleteViewModel @Inject constructor(
private fun observe() {
viewModelScope.launch {
val uid = gameSessionManager.currentUserId ?: return@launch
val couple = gameSessionManager.getCoupleForUser(uid) ?: return@launch
val uid = gameSessionManager.currentUserId
val couple = if (uid != null) gameSessionManager.getCoupleForUser(uid) else null
if (uid == null || couple == null) {
_uiState.update { it.copy(phase = WheelRevealPhase.ERROR, error = "Couldn't load session. Try again.") }
return@launch
}
userId = uid
coupleId = couple.id
partnerId = couple.userIds.firstOrNull { it != uid }
@ -117,9 +122,21 @@ class WheelCompleteViewModel @Inject constructor(
?.let { name -> _uiState.update { it.copy(partnerName = name) } }
}
if (sessionId.isBlank()) return@launch
answerDataSource.observe(couple.id, sessionId).collect { handle(it) }
if (sessionId.isBlank()) {
_uiState.update { it.copy(phase = WheelRevealPhase.ERROR, error = "Session not found. Return to Play.") }
return@launch
}
runCatching {
answerDataSource.observe(couple.id, sessionId).collect { handle(it) }
}.onFailure {
_uiState.update { s -> s.copy(phase = WheelRevealPhase.ERROR, error = "Couldn't load results. Check your connection.") }
}
}
}
fun retry() {
_uiState.update { it.copy(phase = WheelRevealPhase.LOADING, error = null) }
observe()
}
private fun handle(doc: WheelRevealDoc) {
@ -212,6 +229,11 @@ fun WheelCompleteScreen(
onSpinAgain = { onNavigate(AppRoute.CATEGORY_PICKER) },
onHome = { onNavigate(AppRoute.PLAY) }
)
WheelRevealPhase.ERROR -> WheelErrorContent(
message = state.error ?: "Something went wrong.",
onRetry = viewModel::retry,
onHome = { onNavigate(AppRoute.PLAY) }
)
}
}
}
@ -411,6 +433,53 @@ private fun AnswerBlock(label: String, text: String, accent: Color) {
}
}
@Composable
private fun WheelErrorContent(
message: String,
onRetry: () -> Unit,
onHome: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.padding(horizontal = 28.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.weight(1f))
Text(
text = "Couldn't load results",
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(Modifier.weight(1f))
Button(
onClick = onRetry,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) {
Text("Try again", color = Color.White)
}
OutlinedButton(
onClick = onHome,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp)
) {
Text("Back to Play")
}
}
}
@Preview
@Composable
fun WheelRevealPreview() {