fix(home): 'Play now' resumes the waiting game, not the generic hub (B-002 P2)

Resolve the active session's gameType to its resume route (gameRouteFor) and carry it on
HomeAction.gameRoute / HomeUiState.waitingGameRoute; HomeActionTarget.Game now navigates
there (fallback Play hub). Each game screen auto-joins the couple's active session on open,
so the Home 'Play now' CTA drops the user straight into the actual waiting game.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-06-25 09:58:26 -05:00
parent 1fe4dea9c1
commit a94f44d3ec
2 changed files with 34 additions and 6 deletions

View File

@ -216,7 +216,8 @@ private fun HomeCallbacks.toActionHandler(onNavigate: (String) -> Unit): (HomeAc
HomeActionTarget.QuestionPacks -> action.categoryId?.let(onCategory) ?: onPacks() HomeActionTarget.QuestionPacks -> action.categoryId?.let(onCategory) ?: onPacks()
HomeActionTarget.Settings -> onSettings() HomeActionTarget.Settings -> onSettings()
HomeActionTarget.AnswerReveal -> onReveal() HomeActionTarget.AnswerReveal -> onReveal()
HomeActionTarget.Game -> onNavigate(AppRoute.PLAY) // Resume the specific waiting game when known (B-002); fall back to the Play hub.
HomeActionTarget.Game -> onNavigate(action.gameRoute ?: AppRoute.PLAY)
HomeActionTarget.Challenge -> onNavigate(AppRoute.CONNECTION_CHALLENGES) HomeActionTarget.Challenge -> onNavigate(AppRoute.CONNECTION_CHALLENGES)
HomeActionTarget.DatePlan -> onNavigate(AppRoute.DATE_MATCHES) HomeActionTarget.DatePlan -> onNavigate(AppRoute.DATE_MATCHES)
HomeActionTarget.MemoryCapsule -> onNavigate(AppRoute.MEMORY_LANE) HomeActionTarget.MemoryCapsule -> onNavigate(AppRoute.MEMORY_LANE)

View File

@ -3,9 +3,11 @@ package app.closer.ui.home
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute
import app.closer.crypto.CoupleEncryptionManager import app.closer.crypto.CoupleEncryptionManager
import app.closer.crypto.EncryptionStatus import app.closer.crypto.EncryptionStatus
import app.closer.crypto.SealedRevealManager import app.closer.crypto.SealedRevealManager
import app.closer.domain.model.GameType
import app.closer.data.remote.FirestoreAnswerDataSource import app.closer.data.remote.FirestoreAnswerDataSource
import app.closer.data.remote.FirestoreCapsuleDataSource import app.closer.data.remote.FirestoreCapsuleDataSource
import app.closer.data.remote.FirestoreChallengeDataSource import app.closer.data.remote.FirestoreChallengeDataSource
@ -89,7 +91,10 @@ data class HomeAction(
val target: HomeActionTarget, val target: HomeActionTarget,
val tone: HomeActionTone, val tone: HomeActionTone,
val metric: String? = null, val metric: String? = null,
val categoryId: String? = null val categoryId: String? = null,
// For the "your partner is waiting to play" CTA: the specific game route to resume
// (so "Play now" jumps into the actual waiting game, not the generic Play hub). B-002.
val gameRoute: String? = null
) )
data class PendingActionCard( data class PendingActionCard(
@ -99,6 +104,19 @@ data class PendingActionCard(
val target: HomeActionTarget val target: HomeActionTarget
) )
/**
* The entry route that resumes an in-progress game of [gameType]. Each game screen
* detects the couple's active session on open and joins it, so navigating here lets the
* Home "Play now" CTA drop the user straight back into the waiting game (B-002).
*/
private fun gameRouteFor(gameType: String?): String? = when (gameType) {
GameType.WHEEL -> AppRoute.SPIN_WHEEL_RANDOM
GameType.THIS_OR_THAT -> AppRoute.THIS_OR_THAT
GameType.HOW_WELL -> AppRoute.HOW_WELL
GameType.DESIRE_SYNC -> AppRoute.DESIRE_SYNC
else -> null
}
enum class DailyQuestionState { enum class DailyQuestionState {
UNANSWERED, UNANSWERED,
USER_ANSWERED_PARTNER_PENDING, USER_ANSWERED_PARTNER_PENDING,
@ -128,6 +146,9 @@ data class HomeUiState(
val pendingActions: List<PendingActionCard> = emptyList(), val pendingActions: List<PendingActionCard> = emptyList(),
// Retention signals — populated in loadHome() and observeAnswers() // Retention signals — populated in loadHome() and observeAnswers()
val hasWaitingGame: Boolean = false, val hasWaitingGame: Boolean = false,
// The route of the active game waiting for this user, so the Home "Play now" CTA
// resumes that specific game instead of dumping on the generic Play hub (B-002).
val waitingGameRoute: String? = null,
val hasActiveChallenge: Boolean = false, val hasActiveChallenge: Boolean = false,
val hasUpcomingDatePlan: Boolean = false, val hasUpcomingDatePlan: Boolean = false,
val hasUnlockedCapsule: Boolean = false, val hasUnlockedCapsule: Boolean = false,
@ -243,6 +264,7 @@ class HomeViewModel @Inject constructor(
// Retention signal fetches — run in parallel, failures silently default to false. // Retention signal fetches — run in parallel, failures silently default to false.
var hasWaitingGame = false var hasWaitingGame = false
var waitingGameRoute: String? = null
var hasActiveChallenge = false var hasActiveChallenge = false
var hasUpcomingDatePlan = false var hasUpcomingDatePlan = false
var hasUnlockedCapsule = false var hasUnlockedCapsule = false
@ -252,8 +274,9 @@ class HomeViewModel @Inject constructor(
val gameJob = async { val gameJob = async {
runCatching { runCatching {
val session = questionSessionRepository.getActiveSessionForCouple(coupleId) val session = questionSessionRepository.getActiveSessionForCouple(coupleId)
session != null && uid !in session.completedByUsers ?.takeIf { uid !in it.completedByUsers }
}.getOrDefault(false) session to gameRouteFor(session?.gameType)
}.getOrDefault(null to null)
} }
val challengeJob = async { val challengeJob = async {
runCatching { runCatching {
@ -276,7 +299,9 @@ class HomeViewModel @Inject constructor(
.any { it.status == "sealed" && it.unlockAt in 1L..now } .any { it.status == "sealed" && it.unlockAt in 1L..now }
}.getOrDefault(false) }.getOrDefault(false)
} }
hasWaitingGame = gameJob.await() val (waitingSession, waitingRoute) = gameJob.await()
hasWaitingGame = waitingSession != null
waitingGameRoute = waitingRoute
hasActiveChallenge = challengeJob.await() hasActiveChallenge = challengeJob.await()
hasUpcomingDatePlan = dateJob.await() hasUpcomingDatePlan = dateJob.await()
hasUnlockedCapsule = capsuleJob.await() hasUnlockedCapsule = capsuleJob.await()
@ -295,6 +320,7 @@ class HomeViewModel @Inject constructor(
partnerLeftEvent = false, partnerLeftEvent = false,
needsRecovery = needsRecovery, needsRecovery = needsRecovery,
hasWaitingGame = hasWaitingGame, hasWaitingGame = hasWaitingGame,
waitingGameRoute = waitingGameRoute,
hasActiveChallenge = hasActiveChallenge, hasActiveChallenge = hasActiveChallenge,
hasUpcomingDatePlan = hasUpcomingDatePlan, hasUpcomingDatePlan = hasUpcomingDatePlan,
hasUnlockedCapsule = hasUnlockedCapsule, hasUnlockedCapsule = hasUnlockedCapsule,
@ -598,7 +624,8 @@ class HomeViewModel @Inject constructor(
body = "A game is ready for the two of you. Jump back in and keep the ritual going.", body = "A game is ready for the two of you. Jump back in and keep the ritual going.",
cta = "Play now", cta = "Play now",
target = HomeActionTarget.Game, target = HomeActionTarget.Game,
tone = HomeActionTone.Ritual tone = HomeActionTone.Ritual,
gameRoute = waitingGameRoute
) )
Priority.CHALLENGE_WAITING -> HomeAction( Priority.CHALLENGE_WAITING -> HomeAction(