diff --git a/app/src/main/java/app/closer/data/remote/FirestoreWheelAnswerDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreWheelAnswerDataSource.kt index a0b0c474..b7dfae5f 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreWheelAnswerDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreWheelAnswerDataSource.kt @@ -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 = 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() } diff --git a/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt index 9ef83e38..c708d729 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt @@ -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 = 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,11 +122,23 @@ 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) { val uid = userId ?: return val mine = doc.answersByUser[uid].orEmpty() @@ -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() {