Compare commits
No commits in common. "d91aac5ffb943aae5aed31747328ec8f035bc842" and "a642e7e267f6a09e2b8c50f32c95a0153cd0b2d3" have entirely different histories.
d91aac5ffb
...
a642e7e267
|
|
@ -1,7 +1,7 @@
|
||||||
# Claude QA Coverage Matrix
|
# Claude QA Coverage Matrix
|
||||||
|
|
||||||
> Resume anchor. Status: `todo | pass | fail(→id) | n/a`. See `ClaudeReport.md` run-state header for current position.
|
> Resume anchor. Status: `todo | pass | fail(→id) | n/a`. See `ClaudeReport.md` run-state header for current position.
|
||||||
> **Round 4 (fix phase + re-QA) COMPLETE 2026-06-25, build `6f6f76a`:** E-003 + B-004 (P2) + A-OBS (P3) FIXED + verified live + committed. 0 open P0–P2. Only E-OBS (P3, bg push channel) open — deferred (server change + user-gated functions deploy). Regression smoke clean (launch, This-or-That end-to-end + B-001 auto-close, chat `enc:v1:` at rest, C-NAV-001 back→launcher).
|
> Round 1 in progress.
|
||||||
|
|
||||||
## Pass A — Couple-shared premium (states: neither / partner-only / self)
|
## Pass A — Couple-shared premium (states: neither / partner-only / self)
|
||||||
| Feature | neither→locked | partner→both unlock | self→unlock | Status |
|
| Feature | neither→locked | partner→both unlock | self→unlock | Status |
|
||||||
|
|
|
||||||
|
|
@ -154,19 +154,6 @@ Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges
|
||||||
intermediate screen and interaction works (selections register, progress advances, both-answered gating,
|
intermediate screen and interaction works (selections register, progress advances, both-answered gating,
|
||||||
reveal/scoring/summary correct). Premium games (Desire Sync, Memory Lane) need a premium toggle to play.
|
reveal/scoring/summary correct). Premium games (Desire Sync, Memory Lane) need a premium toggle to play.
|
||||||
- The session lifecycle is exercised by the real playthrough: `status` active→completed; reveal/results correct on both.
|
- The session lifecycle is exercised by the real playthrough: `status` active→completed; reveal/results correct on both.
|
||||||
- **VARY THE STYLE OF PLAY (don't just repeat the happy path):** across runs, deliberately exercise *different* ways a
|
|
||||||
real couple would play each game, because different inputs hit different code paths:
|
|
||||||
- **Different option/length/mood choices** — every round length (5/10/15), every mood/category, the "All topics"/
|
|
||||||
shuffle option, and **each distinct answer type** (A/B, Yes/No, True/False, 1–5 scale, multi-select, free-text).
|
|
||||||
- **Different answer *patterns* that change the result** — all-match vs all-mismatch vs partial; both-yes vs both-no
|
|
||||||
vs split (so reveals show "shared", "all private", "0 matches", "perfect/zero score" — verify each renders right).
|
|
||||||
- **Different turn orders / who-starts** — partner A starts vs partner B starts; the guesser opens before vs after
|
|
||||||
the subject finishes; both open simultaneously (race); one device much slower than the other.
|
|
||||||
- **Different exit/resume styles** — finish normally; quit mid-game; background mid-game then resume; cold-kill
|
|
||||||
mid-game then reopen; "End their game"; re-open a completed session for the replay/results; play two games
|
|
||||||
back-to-back, and a *different* game type immediately after.
|
|
||||||
- **Edge inputs** — submit with nothing selected (should be blocked), rapid double-taps on answer/confirm/next,
|
|
||||||
spamming the start button, tapping during the reveal animation. None should crash, duplicate, or desync.
|
|
||||||
- Edges: re-open a completed session, leave mid-game (resume), no stuck session, no crash, logcat clean.
|
- Edges: re-open a completed session, leave mid-game (resume), no stuck session, no crash, logcat clean.
|
||||||
- Game start/finish pushes (`onGameSessionUpdate`) exercised here; full delivery/deep-link audit in **Pass E**.
|
- Game start/finish pushes (`onGameSessionUpdate`) exercised here; full delivery/deep-link audit in **Pass E**.
|
||||||
- **Media permissions** (CAMERA, RECORD_AUDIO): granted works, denied degrades gracefully.
|
- **Media permissions** (CAMERA, RECORD_AUDIO): granted works, denied degrades gracefully.
|
||||||
|
|
@ -188,21 +175,6 @@ Account); Paywall; Your Progress/Activity; Recovery.
|
||||||
opens correctly each time — e.g. a conversation from the inbox AND from "Discuss" AND from a notification; a game
|
opens correctly each time — e.g. a conversation from the inbox AND from "Discuss" AND from a notification; a game
|
||||||
from the Play hub AND from a notification; Paywall from each gated feature; Settings sub-pages; reveal from Today
|
from the Play hub AND from a notification; Paywall from each gated feature; Settings sub-pages; reveal from Today
|
||||||
AND from history AND from `partner_answered`. A screen that works from one entry but breaks/duplicates from another = bug.
|
AND from history AND from `partner_answered`. A screen that works from one entry but breaks/duplicates from another = bug.
|
||||||
- **TAKE EVERY AVENUE (exhaustive nav fuzzing — actively hunt for nav bugs, don't just walk the happy path):** treat
|
|
||||||
navigation as something to *break*. On every screen, **tap every interactive element** — each button, card, row,
|
|
||||||
icon, chip, link, tab, header back-arrow, system back, and any "see all / history / edit / manage" affordance — and
|
|
||||||
follow where it goes. Then try the *combinations and sequences* a curious user hits:
|
|
||||||
- **Every order:** switch bottom tabs in many orders, mid-flow (open a game, jump to Messages, come back); enter a
|
|
||||||
deep screen then tab away then back; open A→B→C then back-back-back.
|
|
||||||
- **Rapid / repeated input:** double- and triple-tap navigation targets (especially "open game", "Play now",
|
|
||||||
"Create/Start session", notification taps) to surface double-push/duplicate-screen/stale-route bugs (cf. B-004).
|
|
||||||
- **Interrupt mid-navigation:** background/rotate/lock during a transition; tap a notification while already on that
|
|
||||||
screen, on a different screen, and while logged-out/unpaired; cold-start straight onto a deep link.
|
|
||||||
- **Dead-ends & traps:** from *every* screen confirm there's always a way out (back/close/home) — no screen that
|
|
||||||
strands the user, needs two backs, exits the app unexpectedly, loops, or lands blank. Re-check the asymmetric-game
|
|
||||||
waiting screens, replay/results screens, and paywall specifically.
|
|
||||||
- Log **every** wrong/duplicate/dead destination with the exact tap sequence to reproduce. Wrong/double-back or
|
|
||||||
dead-end = **P2** (P1 if it traps the user or loses their progress).
|
|
||||||
- **Back-stack / "double back":** from every entry point, **system back AND the in-app back arrow** return to the
|
- **Back-stack / "double back":** from every entry point, **system back AND the in-app back arrow** return to the
|
||||||
correct previous screen — no dead-ends, no exiting the app unexpectedly, and **no screen that requires pressing
|
correct previous screen — no dead-ends, no exiting the app unexpectedly, and **no screen that requires pressing
|
||||||
back twice** (duplicate/stacked destinations on the back stack = bug). Bottom-tab reselection and deep-link/
|
back twice** (duplicate/stacked destinations on the back stack = bug). Bottom-tab reselection and deep-link/
|
||||||
|
|
@ -231,31 +203,6 @@ Account); Paywall; Your Progress/Activity; Recovery.
|
||||||
couple — migration completes without exposing plaintext or losing/garbling old content, and a half-migrated couple
|
couple — migration completes without exposing plaintext or losing/garbling old content, and a half-migrated couple
|
||||||
is safe (no mixed read failures, no downgrade). This is the riskiest data path for existing users.
|
is safe (no mixed read failures, no downgrade). This is the riskiest data path for existing users.
|
||||||
|
|
||||||
### Pass G — Account creation, validation & fake-account abuse (MANDATORY — both the happy path AND the attacks)
|
|
||||||
Cover **every account-creation avenue a real user takes** and **every fake/abusive creation attempt an attacker would
|
|
||||||
try.** Use throwaway test accounts (sign-out → fresh sign-up; never `pm clear`). Report-first like every pass.
|
|
||||||
- **Real creation flows (happy path + validation):** sign-up (email/password and any social/anonymous path), profile
|
|
||||||
creation, and pairing — both **create-invite** and **accept-invite** sides. Verify field validation (invalid/empty
|
|
||||||
email, weak/short password, mismatched confirm, name length/emoji/unicode), the **error copy is friendly** (no raw
|
|
||||||
SDK/Firebase error leaking — cf. A-OBS), loading/disabled states, and that a brand-new unpaired account lands on the
|
|
||||||
correct "create or accept invite" home (not a broken/blank or paired view).
|
|
||||||
- **Duplicate / conflicting creation:** sign up with an **already-registered email** (clear "already in use", no crash,
|
|
||||||
offer sign-in); create a second account while one is signed in; re-run onboarding after completing it; accept an
|
|
||||||
invite while **already paired** (must be rejected cleanly); two devices accepting the **same invite** (single-use —
|
|
||||||
the second must fail gracefully).
|
|
||||||
- **Fake / malicious creation attempts (security — expect DENY, never crash or leak):** create an account that is
|
|
||||||
**NOT a member** of the test couple and attempt every cross-couple action (read messages/answers/dates/entitlements,
|
|
||||||
write to the couple, self-grant `premium`/`hasPremium`, join/hijack pairing with a guessed/expired/reused invite
|
|
||||||
code) — all must be **denied by rules** (this is the live execution of **D3**). Probe **invite-code abuse**: replay a
|
|
||||||
used code, use an expired code, brute-force/guess attempts (CSPRNG entropy + single-use + expiry must hold). Probe
|
|
||||||
**App Check**: a request without a valid token is rejected. Confirm a malformed/forged sign-up can't bypass profile
|
|
||||||
or membership requirements. **Any successful unauthorized create/read/write = P0.**
|
|
||||||
- **Account lifecycle around creation:** sign-out → sign-in (state restores, no stale couple); **delete account** then
|
|
||||||
re-create with the same email (clean slate, partner notified/unpaired); an unpaired/just-created account tapping a
|
|
||||||
stale notification or deep link is handled gracefully (no crash, sane landing).
|
|
||||||
- **Done = every creation avenue exercised** (happy + duplicate + malicious) with each attack **denied** and each happy
|
|
||||||
path validated end-to-end; findings filed with exact repro.
|
|
||||||
|
|
||||||
### Pass E — Notifications (every type delivers, deep-links, leaks nothing)
|
### Pass E — Notifications (every type delivers, deep-links, leaks nothing)
|
||||||
For each: trigger fires → delivered to the **right partner (never self)** → in **foreground/background/killed** →
|
For each: trigger fires → delivered to the **right partner (never self)** → in **foreground/background/killed** →
|
||||||
correct channel + copy with **no private content** → **tap opens exactly the right item** (loaded, not generic Home/
|
correct channel + copy with **no private content** → **tap opens exactly the right item** (loaded, not generic Home/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Claude QA Report — Full-App QA (living report)
|
# Claude QA Report — Full-App QA (living report)
|
||||||
|
|
||||||
> **RUN-STATE: Round 4 (fix phase + re-QA) COMPLETE — 2026-06-25, build HEAD `6f6f76a` on BOTH emulators. Fixed + verified LIVE the 3 tractable R3 findings: E-003 (game pushes now deep-link into the game, not the Play hub — tapped start-game push → This or That 1/5, joined), B-004 (WaitingForPartner now has a "Join the game" escape → How Well guess flow; deterministic repro fixed), A-OBS (paywall shows friendly copy, no raw "credentials issue"). Round-4 regression smoke clean: cold-launch no crash, This or That end-to-end → session auto-closes (B-001 holds), chat text `enc:v1:` at rest, C-NAV-001 back→launcher. ONLY REMAINING: E-OBS (P3) — backgrounded pushes use the FCM fallback channel; fix is server-side (set `android.notification.channel_id` across functions senders) + a functions deploy, which is USER-GATED → deferred for authorization. So: 0 open P0–P2; 1 open P3 (E-OBS, deploy-gated). Baseline restored: both free, 0 active sessions.**
|
> **RUN-STATE: Round 3 (full re-QA A–F) COMPLETE — 2026-06-25, build `ce7fc2e`. RESULT: all 12 prior fixes RE-VERIFIED HOLDING LIVE; deeper play-as-user testing surfaced 5 NEW issues — 2×P2 (B-004 intermittent guesser-stuck on WaitingForPartner; E-003 game notifications deep-link to Play hub not the game), 3×P3 (A-OBS paywall raw error copy [env]; E-OBS bg pushes use fallback channel; C-OBS RESOLVED = debug menu IS BuildConfig.DEBUG-gated, not a bug). NO P0/P1, NO security/encryption findings (Pass D clean, E2EE holds at-rest + UI). NOT yet "flawless" (2 open P2 + Pass E has E-003) → NEXT: a FIX PHASE for E-003 (client routeFor + likely server payload gameType, mirrors B-002) + a deterministic B-004 repro before fixing, then Round 4 re-QA. Baseline restored: both free, 0 active sessions. E-003/B-004 are report-only (logged, not fixed mid-round per plan).**
|
||||||
> _Round 2 result (carried): FIX PHASE COMPLETE — P1×2 (B-001 session rules, C-NAV-001 back-stack), P2×4 (A-001, B-002, C-CC-001, C-DS-001), P3×4 (A-003, B-003, E-002, F-OBS) all FIXED; D-001 (P1 rules) + E-001 (P2 routing) fixed earlier. All verified LIVE except E-002/F-OBS (code+build; live-trigger deferred). Rules deployed._
|
> _Round 2 result (carried): FIX PHASE COMPLETE — P1×2 (B-001 session rules, C-NAV-001 back-stack), P2×4 (A-001, B-002, C-CC-001, C-DS-001), P3×4 (A-003, B-003, E-002, F-OBS) all FIXED; D-001 (P1 rules) + E-001 (P2 routing) fixed earlier. All verified LIVE except E-002/F-OBS (code+build; live-trigger deferred). Rules deployed._
|
||||||
> Pass-B note: a finished game keeps its session active until a player exits the results (Back to Play); leaving both on results blocks the next game until "End their game". Exit cleanly between games.
|
> Pass-B note: a finished game keeps its session active until a player exits the results (Back to Play); leaving both on results blocks the next game until "End their game". Exit cleanly between games.
|
||||||
> **Pass-B MINDSET (user, 2026-06-24): PLAY AS THE USER** — navigate only via the real in-app path a person would tap (no deep-links/admin pokes/shortcuts); expect what a user expects. When the natural path fails, **REPORT FIRST** (log issue + severity + the user action that failed & what was expected), **THEN** a minimal workaround to proceed — never silently engineer around breakage; a flow needing a workaround is broken and must be filed.
|
> **Pass-B MINDSET (user, 2026-06-24): PLAY AS THE USER** — navigate only via the real in-app path a person would tap (no deep-links/admin pokes/shortcuts); expect what a user expects. When the natural path fails, **REPORT FIRST** (log issue + severity + the user action that failed & what was expected), **THEN** a minimal workaround to proceed — never silently engineer around breakage; a flow needing a workaround is broken and must be filed.
|
||||||
|
|
@ -15,21 +15,21 @@ _(Prior games/notifications QA from 2026-06-24 was completed + verified; superse
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Severity summary (current — after Round 4 fix phase + re-QA)
|
## Severity summary (current — after Round 3 re-QA)
|
||||||
| Severity | Open | Fixed (verified live) |
|
| Severity | Open (new in R3) | Fixed (verified holding) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| P0 | 0 | 0 |
|
| P0 | 0 | 0 |
|
||||||
| P1 | 0 | 4 |
|
| P1 | 0 | 4 |
|
||||||
| P2 | **0** | **6** |
|
| P2 | **2** (B-004, E-003) | 4 |
|
||||||
| P3 | **1** (E-OBS — deploy-gated) | **5** |
|
| P3 | **2** (A-OBS, E-OBS) | 4 |
|
||||||
|
|
||||||
**Round 4 result:** the 2 new P2 (E-003, B-004) + 1 new P3 (A-OBS) from R3 are **FIXED + verified live + committed**. **0 open P0–P2.** Only E-OBS (P3) remains, deferred because its fix needs a user-gated functions deploy. Combined with R3 (all 12 earlier fixes hold; Pass D clean; E2EE at-rest + UI intact), the app has **no open crash/data/security/premium/nav-dead-end issues** — the one remaining item is notification-channel polish requiring a backend deploy.
|
**Round 3 result:** all **12 prior fixes RE-VERIFIED HOLDING LIVE** (C-NAV-001, C-CC-001, A-001, A-003, B-001 across 4 game types, B-002, B-003, C-DS-001, D-001 + E-001/E-002 code + F-OBS indirect). **No P0/P1, no security/encryption findings** (Pass D clean; E2EE holds at-rest + in UI). Deeper play-as-user testing found **5 new issues: 2×P2 (B-004, E-003), 3×P3 (A-OBS, E-OBS, and C-OBS which RESOLVED to not-a-bug — debug menu is BuildConfig.DEBUG-gated)**. Not yet "flawless" (def: 0 open P0–P2 + Passes D/E clean) — 2 open P2 remain → fix phase + Round 4 re-QA needed.
|
||||||
|
|
||||||
**R3/R4 issue dispositions:**
|
**New R3 issues (report-only — logged, to fix next phase):**
|
||||||
- **E-003 (P2) — FIXED** `23c9923`: game pushes (`partner_started_game`/`partner_completed_part`) now route via `gameRouteForType(payload.gameType)` into the specific game (auto-joins the active session), not the Play hub. Server already sends `game_type`; client parses it in AppMessagingService + MainActivity. `game_results_ready` stays on the hub pending a server change to also send `game_session_id` (documented). **Verified live:** tapped start-game push → This or That 1/5 (joined).
|
- **B-004 (P2, intermittent):** How Well guesser can get stuck on the generic WaitingForPartner screen during a rapid game-to-game transition (screen only exits on session end; needs deterministic repro before fix; escalate to P1 if deterministic).
|
||||||
- **B-004 (P2) — FIXED** `da7fc74`: `WaitingForPartnerScreen` now resolves the active session's game route and offers a primary **"Join the game"** action (every game is async/joinable), so the partner is never stuck. **Verified live** via deterministic repro: QA started How Well → Sam opened This or That → WaitingForPartner → "Join the game" → How Well guess intro.
|
- **E-003 (P2):** game notifications (`partner_started_game`/`game_results_ready`/`partner_completed_part`) deep-link to the generic Play hub, not the specific game/results, despite "Tap to join!" (fix: extend HomeViewModel.gameRouteFor resolver to notification routing; likely needs server payload gameType).
|
||||||
- **A-OBS (P3) — FIXED** `6f6f76a`: paywall ErrorState no longer renders the raw billing/RC SDK message; shows friendly "We couldn't load subscription options right now…". **Verified live** (raw "credentials issue" gone).
|
- **A-OBS (P3):** paywall plan-load shows raw "credentials issue" error (emulator has no RevenueCat sandbox; copy should be friendlier in prod).
|
||||||
- **E-OBS (P3) — OPEN, deferred:** backgrounded pushes use `fcm_fallback_notification_channel`, bypassing code-defined channels. Fix is server-side (set `android.notification.channel_id` on every FCM send across functions, or send data-only + build client-side) + a **functions deploy (user-gated)**. Cannot verify without deploying.
|
- **E-OBS (P3):** backgrounded pushes use `fcm_fallback_notification_channel`, bypassing code-defined channels (CHANNEL_GAMES/chat) — server sends "notification" not data-only messages.
|
||||||
- **C-OBS (RESOLVED, not a bug):** Settings "Art preview/Paired home (debug)" entries ARE `BuildConfig.DEBUG`-gated (SettingsScreen.kt:469) — won't ship in release.
|
- **C-OBS (RESOLVED, not a bug):** Settings "Art preview/Paired home (debug)" entries ARE `BuildConfig.DEBUG`-gated (SettingsScreen.kt:469) — won't ship in release.
|
||||||
|
|
||||||
**Round 1: all P0–P2 found were FIXED** (A-001 premium P1, E-001 notif-routing P2).
|
**Round 1: all P0–P2 found were FIXED** (A-001 premium P1, E-001 notif-routing P2).
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
questionId = intent.getStringExtra("question_id"),
|
questionId = intent.getStringExtra("question_id"),
|
||||||
conversationId = intent.getStringExtra("conversation_id"),
|
conversationId = intent.getStringExtra("conversation_id"),
|
||||||
gameSessionId = intent.getStringExtra("game_session_id"),
|
gameSessionId = intent.getStringExtra("game_session_id"),
|
||||||
gameType = intent.getStringExtra("game_type"),
|
|
||||||
capsuleId = intent.getStringExtra("capsule_id"),
|
capsuleId = intent.getStringExtra("capsule_id"),
|
||||||
challengeId = intent.getStringExtra("challenge_id"),
|
challengeId = intent.getStringExtra("challenge_id"),
|
||||||
avatarUrl = intent.getStringExtra("sender_avatar_url")
|
avatarUrl = intent.getStringExtra("sender_avatar_url")
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ class AppMessagingService : FirebaseMessagingService() {
|
||||||
questionId = message.data["question_id"],
|
questionId = message.data["question_id"],
|
||||||
conversationId = message.data["conversation_id"],
|
conversationId = message.data["conversation_id"],
|
||||||
gameSessionId = message.data["game_session_id"],
|
gameSessionId = message.data["game_session_id"],
|
||||||
gameType = message.data["game_type"],
|
|
||||||
capsuleId = message.data["capsule_id"],
|
capsuleId = message.data["capsule_id"],
|
||||||
challengeId = message.data["challenge_id"],
|
challengeId = message.data["challenge_id"],
|
||||||
avatarUrl = message.data["sender_avatar_url"]
|
avatarUrl = message.data["sender_avatar_url"]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import androidx.core.app.NotificationManagerCompat
|
||||||
import app.closer.MainActivity
|
import app.closer.MainActivity
|
||||||
import app.closer.R
|
import app.closer.R
|
||||||
import app.closer.core.navigation.AppRoute
|
import app.closer.core.navigation.AppRoute
|
||||||
import app.closer.domain.model.GameType
|
|
||||||
import app.closer.domain.repository.AppSettings
|
import app.closer.domain.repository.AppSettings
|
||||||
import app.closer.domain.repository.SettingsRepository
|
import app.closer.domain.repository.SettingsRepository
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
@ -268,16 +267,8 @@ enum class PartnerNotificationType(
|
||||||
fun routeFor(payload: PartnerNotificationPayload, coupleId: String = ""): String = when (this) {
|
fun routeFor(payload: PartnerNotificationPayload, coupleId: String = ""): String = when (this) {
|
||||||
PARTNER_ANSWERED -> AppRoute.DAILY_QUESTION
|
PARTNER_ANSWERED -> AppRoute.DAILY_QUESTION
|
||||||
REVEAL_READY -> payload.questionId?.let { AppRoute.answerReveal(it) } ?: AppRoute.ANSWER_HISTORY
|
REVEAL_READY -> payload.questionId?.let { AppRoute.answerReveal(it) } ?: AppRoute.ANSWER_HISTORY
|
||||||
// Deep link straight into the waiting game (each game screen auto-joins the couple's active
|
PARTNER_STARTED_GAME -> AppRoute.PLAY
|
||||||
// session on open), not the generic Play hub — the push says "Tap to join". Falls back to the
|
PARTNER_COMPLETED_PART -> AppRoute.PLAY
|
||||||
// hub only if the server didn't send game_type. E-003.
|
|
||||||
PARTNER_STARTED_GAME -> gameRouteForType(payload.gameType) ?: AppRoute.PLAY
|
|
||||||
PARTNER_COMPLETED_PART -> gameRouteForType(payload.gameType) ?: AppRoute.PLAY
|
|
||||||
// Results-ready means the session is COMPLETED, so the plain game route would show "start a
|
|
||||||
// new game" (getActiveSession returns only active sessions). The correct target is the
|
|
||||||
// per-session results/replay route — but that needs the server to also send game_session_id
|
|
||||||
// in the FCM data (currently it sends only game_type). Until that server change ships, route
|
|
||||||
// to the Play hub rather than a misleading setup screen. E-003 (results-ready follow-up).
|
|
||||||
GAME_RESULTS_READY -> AppRoute.PLAY
|
GAME_RESULTS_READY -> AppRoute.PLAY
|
||||||
CHALLENGE_WAITING -> AppRoute.CONNECTION_CHALLENGES
|
CHALLENGE_WAITING -> AppRoute.CONNECTION_CHALLENGES
|
||||||
CAPSULE_UNLOCKED -> AppRoute.MEMORY_LANE
|
CAPSULE_UNLOCKED -> AppRoute.MEMORY_LANE
|
||||||
|
|
@ -335,23 +326,8 @@ data class PartnerNotificationPayload(
|
||||||
val questionId: String? = null,
|
val questionId: String? = null,
|
||||||
val conversationId: String? = null,
|
val conversationId: String? = null,
|
||||||
val gameSessionId: String? = null,
|
val gameSessionId: String? = null,
|
||||||
/** The active session's game type (e.g. "this_or_that"), used to deep link into the game. E-003. */
|
|
||||||
val gameType: String? = null,
|
|
||||||
val capsuleId: String? = null,
|
val capsuleId: String? = null,
|
||||||
val challengeId: String? = null,
|
val challengeId: String? = null,
|
||||||
/** Sender's avatar URL, used as the notification large icon when present. */
|
/** Sender's avatar URL, used as the notification large icon when present. */
|
||||||
val avatarUrl: String? = null
|
val avatarUrl: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a session's [GameType] to the entry route that resumes/joins it. Each game screen detects the
|
|
||||||
* couple's active session on open and joins it, so deep linking here drops the partner straight into
|
|
||||||
* the waiting game (mirrors HomeViewModel.gameRouteFor for the Home "Play now" CTA). E-003.
|
|
||||||
*/
|
|
||||||
private fun gameRouteForType(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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,7 @@ data class WaitingForPartnerUiState(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val gameType: String = GameType.WHEEL,
|
val gameType: String = GameType.WHEEL,
|
||||||
val partnerName: String = "Partner",
|
val partnerName: String = "Partner",
|
||||||
val navigateTo: String? = null,
|
val navigateTo: String? = null
|
||||||
/**
|
|
||||||
* Route into the partner's active game so this user can play their part, instead of being
|
|
||||||
* stuck on this "waiting" screen. Every current game is async (both play on their own device),
|
|
||||||
* so the partner who lands here can always join. Null only for an unknown game type. B-004.
|
|
||||||
*/
|
|
||||||
val joinRoute: String? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -87,8 +81,7 @@ class WaitingForPartnerViewModel @Inject constructor(
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
gameType = session.gameType,
|
gameType = session.gameType,
|
||||||
partnerName = partnerName,
|
partnerName = partnerName
|
||||||
joinRoute = gameTypeRoute(session.gameType)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -180,30 +173,11 @@ fun WaitingForPartnerScreen(
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
|
|
||||||
// Primary action: join the partner's game and play your part. Without this the
|
Button(
|
||||||
// screen was a dead-end for async games the user could actually play (B-004).
|
onClick = { onNavigate(AppRoute.PLAY) },
|
||||||
state.joinRoute?.let { route ->
|
modifier = Modifier.fillMaxWidth()
|
||||||
Button(
|
) {
|
||||||
onClick = { onNavigate(route) },
|
Text("Back to Games")
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Join the game")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.joinRoute != null) {
|
|
||||||
TextButton(
|
|
||||||
onClick = { onNavigate(AppRoute.PLAY) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Back to Games")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(
|
|
||||||
onClick = { onNavigate(AppRoute.PLAY) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Back to Games")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TextButton(onClick = viewModel::abandonPartnerGame) {
|
TextButton(onClick = viewModel::abandonPartnerGame) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -226,15 +200,6 @@ private fun gameTypeLabel(gameType: String): String = when (gameType) {
|
||||||
else -> gameType
|
else -> gameType
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Entry route that joins the partner's active game (each game screen auto-joins on open). B-004. */
|
|
||||||
private fun gameTypeRoute(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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun gameTypeGlyphKey(gameType: String): String = when (gameType) {
|
private fun gameTypeGlyphKey(gameType: String): String = when (gameType) {
|
||||||
GameType.WHEEL -> "play"
|
GameType.WHEEL -> "play"
|
||||||
GameType.THIS_OR_THAT -> "question"
|
GameType.THIS_OR_THAT -> "question"
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,7 @@ fun PaywallScreen(
|
||||||
)
|
)
|
||||||
uiState.error != null -> ErrorState(
|
uiState.error != null -> ErrorState(
|
||||||
title = "Couldn't load plans",
|
title = "Couldn't load plans",
|
||||||
// Always show friendly copy — the raw billing/RevenueCat SDK message (e.g.
|
message = uiState.error ?: "Check your connection and tap to try again.",
|
||||||
// "There was a credentials issue. Check the underlying error…") is developer
|
|
||||||
// detail and must never surface to users. A-OBS.
|
|
||||||
message = "We couldn't load subscription options right now. Check your connection and tap to try again.",
|
|
||||||
retryLabel = "Try again",
|
retryLabel = "Try again",
|
||||||
onRetry = { viewModel.retry() },
|
onRetry = { viewModel.retry() },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue