fix(wheel-reveal): error state with retry, null-safe uid/couple, close on snapshot error
This commit is contained in:
parent
acaa8e635c
commit
6977db7600
|
|
@ -77,7 +77,8 @@ class FirestoreWheelAnswerDataSource @Inject constructor(
|
||||||
/** Live view of both partners' answers; emits whenever either side submits. */
|
/** Live view of both partners' answers; emits whenever either side submits. */
|
||||||
fun observe(coupleId: String, sessionId: String): Flow<WheelRevealDoc> = callbackFlow {
|
fun observe(coupleId: String, sessionId: String): Flow<WheelRevealDoc> = callbackFlow {
|
||||||
val reg = doc(coupleId, sessionId).addSnapshotListener { snap, err ->
|
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))
|
trySend(parse(snap, coupleId))
|
||||||
}
|
}
|
||||||
awaitClose { reg.remove() }
|
awaitClose { reg.remove() }
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
// ── ViewModel ──────────────────────────────────────────────────────────────────
|
// ── 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. */
|
/** One prompt with both partners' answers, ready to render side by side. */
|
||||||
data class WheelRevealItem(
|
data class WheelRevealItem(
|
||||||
|
|
@ -80,7 +80,8 @@ data class WheelCompleteUiState(
|
||||||
val categoryName: String = "",
|
val categoryName: String = "",
|
||||||
val partnerName: String = "Your partner",
|
val partnerName: String = "Your partner",
|
||||||
val items: List<WheelRevealItem> = emptyList(),
|
val items: List<WheelRevealItem> = emptyList(),
|
||||||
val navigateTo: String? = null
|
val navigateTo: String? = null,
|
||||||
|
val error: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -105,8 +106,12 @@ class WheelCompleteViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun observe() {
|
private fun observe() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val uid = gameSessionManager.currentUserId ?: return@launch
|
val uid = gameSessionManager.currentUserId
|
||||||
val couple = gameSessionManager.getCoupleForUser(uid) ?: return@launch
|
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
|
userId = uid
|
||||||
coupleId = couple.id
|
coupleId = couple.id
|
||||||
partnerId = couple.userIds.firstOrNull { it != uid }
|
partnerId = couple.userIds.firstOrNull { it != uid }
|
||||||
|
|
@ -117,11 +122,23 @@ class WheelCompleteViewModel @Inject constructor(
|
||||||
?.let { name -> _uiState.update { it.copy(partnerName = name) } }
|
?.let { name -> _uiState.update { it.copy(partnerName = name) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionId.isBlank()) return@launch
|
if (sessionId.isBlank()) {
|
||||||
answerDataSource.observe(couple.id, sessionId).collect { handle(it) }
|
_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) {
|
private fun handle(doc: WheelRevealDoc) {
|
||||||
val uid = userId ?: return
|
val uid = userId ?: return
|
||||||
val mine = doc.answersByUser[uid].orEmpty()
|
val mine = doc.answersByUser[uid].orEmpty()
|
||||||
|
|
@ -212,6 +229,11 @@ fun WheelCompleteScreen(
|
||||||
onSpinAgain = { onNavigate(AppRoute.CATEGORY_PICKER) },
|
onSpinAgain = { onNavigate(AppRoute.CATEGORY_PICKER) },
|
||||||
onHome = { onNavigate(AppRoute.PLAY) }
|
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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun WheelRevealPreview() {
|
fun WheelRevealPreview() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue