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.Settings -> onSettings()
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.DatePlan -> onNavigate(AppRoute.DATE_MATCHES)
HomeActionTarget.MemoryCapsule -> onNavigate(AppRoute.MEMORY_LANE)

View File

@ -3,9 +3,11 @@ package app.closer.ui.home
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute
import app.closer.crypto.CoupleEncryptionManager
import app.closer.crypto.EncryptionStatus
import app.closer.crypto.SealedRevealManager
import app.closer.domain.model.GameType
import app.closer.data.remote.FirestoreAnswerDataSource
import app.closer.data.remote.FirestoreCapsuleDataSource
import app.closer.data.remote.FirestoreChallengeDataSource
@ -89,7 +91,10 @@ data class HomeAction(
val target: HomeActionTarget,
val tone: HomeActionTone,
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(
@ -99,6 +104,19 @@ data class PendingActionCard(
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 {
UNANSWERED,
USER_ANSWERED_PARTNER_PENDING,
@ -128,6 +146,9 @@ data class HomeUiState(
val pendingActions: List<PendingActionCard> = emptyList(),
// Retention signals — populated in loadHome() and observeAnswers()
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 hasUpcomingDatePlan: 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.
var hasWaitingGame = false
var waitingGameRoute: String? = null
var hasActiveChallenge = false
var hasUpcomingDatePlan = false
var hasUnlockedCapsule = false
@ -252,8 +274,9 @@ class HomeViewModel @Inject constructor(
val gameJob = async {
runCatching {
val session = questionSessionRepository.getActiveSessionForCouple(coupleId)
session != null && uid !in session.completedByUsers
}.getOrDefault(false)
?.takeIf { uid !in it.completedByUsers }
session to gameRouteFor(session?.gameType)
}.getOrDefault(null to null)
}
val challengeJob = async {
runCatching {
@ -276,7 +299,9 @@ class HomeViewModel @Inject constructor(
.any { it.status == "sealed" && it.unlockAt in 1L..now }
}.getOrDefault(false)
}
hasWaitingGame = gameJob.await()
val (waitingSession, waitingRoute) = gameJob.await()
hasWaitingGame = waitingSession != null
waitingGameRoute = waitingRoute
hasActiveChallenge = challengeJob.await()
hasUpcomingDatePlan = dateJob.await()
hasUnlockedCapsule = capsuleJob.await()
@ -295,6 +320,7 @@ class HomeViewModel @Inject constructor(
partnerLeftEvent = false,
needsRecovery = needsRecovery,
hasWaitingGame = hasWaitingGame,
waitingGameRoute = waitingGameRoute,
hasActiveChallenge = hasActiveChallenge,
hasUpcomingDatePlan = hasUpcomingDatePlan,
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.",
cta = "Play now",
target = HomeActionTarget.Game,
tone = HomeActionTone.Ritual
tone = HomeActionTone.Ritual,
gameRoute = waitingGameRoute
)
Priority.CHALLENGE_WAITING -> HomeAction(