> **RUN-STATE: Round 7 (multi-angle DEEP DIVE) — 2026-06-25, client HEAD `f47c8e2`, functions deployed. Plan updated with a "Multi-angle attack mandate" + live raw-API D3.** Attacked security/data/concurrency from multiple angles (admin ground-truth read, raw Firestore REST as member+non-member, killed/cold state, malformed intents, simultaneous-start race). **Security cornerstone = FULLY CLEAN (deep):** D1 at-rest — messages/previews + all 4 game-answer collections (ToT/HowWell/DesireSync/Wheel, both users) + capsules + date-swipe actions all `enc:v1:`; couple key phrase-wrapped (argon2id), recovery phrase server-blind + `encryptedRecoveryPhrase` wiped on acceptance, plaintext `inviteCode` not exploitable (invite readable only by inviter; no code-encrypted secret persisted). D3 raw-API — **non-member denied ALL reads/writes (403)**; real premium path `users/{uid}/entitlements/premium` write **denied (403, server-only) → no self-grant**; cross-couple denied. Robustness — malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal values) → 0 crash; killed-state cold-start chat deep-link → conversation loads. **NEW FINDING: F-RACE-001 (P1)** — simultaneous game start creates **two divergent active sessions** (TOCTOU). **SEVERITY BOARD: P1 = 1 open (F-RACE-001), P0/P2/P3 = 0 open.** Baseline restored (duplicate sessions ended, 0 active, couple intact). Two hardening notes → Future.md (App Check not enforced on Firestore; user-doc update rule allows arbitrary non-`hasPremium` fields).**
>
> **F-RACE-001 (P1, NEW — concurrency):** When BOTH partners start the *same* game within ~the same second, the couple
> ends up with **2 active sessions with different question sets** (proven live: QA "Which should end the date?" vs Sam
> "Which feels more romantic?", two session docs `bw8Q3X45…` + `yNOzMTOCsGlZPbnv2yBN`). Root cause: `GameSessionManager.
> startGameWithCouple` (usecase/GameSessionManager.kt:84–106) does a **non-transactional check-then-create** —
> `getActiveSessionForCouple` then `saveSession` (auto-id); two concurrent calls both read null → both create. The
> existing `partner_active_session` guard only covers the non-simultaneous case. Impact: the two partners play separate
> games and never get a shared reveal (core loop silently defeated); two active sessions can also lock/confuse the
> "one active game" rule. No crash, no data loss; recoverable via "End their game"/admin. Repro: stage both at This or
> That mood-select, fire "create" on both in parallel. **Suggested fix:** make session creation atomic — a Firestore
> **transaction** with a per-couple active-session **sentinel** (e.g. `couples/{cid}` field `activeSessionId` or a
> `sessions/_active` doc): read sentinel → if an active session exists, take the join/`partner_active_session` path;
> else create the session doc (client-generated id) + set the sentinel in the same transaction. Clear the sentinel on
> finish. Needs a `firestore.rules` update (member-only sentinel write) + a rules deploy + re-verify all 7 games. (This
> is an architectural change to the core game flow — flagged for a focused fix-phase implementation.)
> **RUN-STATE: Round 6 (branding + Future.md regression QA) COMPLETE — 2026-06-25. Client HEAD `f47c8e2` on both emulators (build == HEAD, reinstalled).** Scope: regression-verify the new surfaces from the branding drop (`95cad84`: white-keyhole launcher/notification icons, animated app-icon-chip loader + fill, cold-launch splash, pairing hero) and the Future.md backlog clear (`f47c8e2`: inclusive gender options, turn-aware Home copy, push rate-limit budget split, results-push suppression via new ActiveGameSessionMonitor, paywall retry/offline/hide-Continue, auth privacy rotator). **0 new issues — SEVERITY BOARD STILL 0 open P0–P3.** LIVE-VERIFIED: animated loader (chip+fill, both themes), splash→handoff (white-keyhole icon, no white flash), launcher icon (round mask), This or That + How Well open with no crash (confirms #4's new VM injection is sound), paywall purchase screen shows friendly "Couldn't load plans" + Try again with Continue hidden (no dead button) + online→generic message (#5), onboarding carousel illustration. Unit tests green (NotificationRateLimiter rewritten + PartnerNotificationManagerTest repaired). No FATAL on either device all session. CODE/UNIT-VERIFIED (live deferred, low-risk over proven patterns + fragile multi-text-field/2-device paths): #1 gender step (EditProfile + onboarding sex step — same option list as the shipping Female/Male), #8 rotator on SignUp/Forgot (reuses the Login-proven BrandMessageRotator), #2 "Your turn to play." (static string in the proven GAME_WAITING path), #3 weekly-cap exemption (unit-tested; only triggers at ≥100/wk), #4 results-suppression timing (mechanism + VM wiring verified; simultaneous-finish timing is non-deterministic to drive). Baseline restored: 5554 signed out during the sign-up pass, re-signed-in QA (`Y05AKO2IlTPMa0JQW1BiNIM0uzK2`) via admin custom token; couple `Xal3Kw3gjSdn0niERYKJ` intact, Sam paired.**
> 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.
> R2-1 DONE: A-001 couple-shared re-verified live (Desire Sync/Memory Lane/Wheel enter when partner premium; free→paywall). **D-001 (P1) FIXED+DEPLOYED** (capsules/challenges rules; Memory Lane + Connection Challenges now load). Sam reverted to free (baseline).
> **EXECUTION MODE: autonomous run-to-completion — do NOT stop; fix blockers inline; keep cycling fix→re-QA until flawless. Do NOT hand back when context fills — the harness auto-compacts and you continue from THIS run-state (re-read it + coverage after any summary). Commit before interruptible work; recover stuck sessions via the session-start ritual.**
> **STANDING AUTHORIZATION (user, 2026-06-24): may `firebase deploy --only firestore:rules` + has admin access (Firestore reads/writes/seeds + entitlement toggles) — run these without pausing. Only the macOS requirement for iOS (Parts 2/3) remains a hard stop.**
**Round 5 result:** **0 open issues at every severity (P0–P3).** E-OBS (the last open P3) is now **FIXED + DEPLOYED + verified live** along with the E-003 results-ready follow-up. New **Pass G (account creation + fake-account abuse)** ran clean — no security findings. All prior fixes hold. The app meets the "flawless" bar (0 open P0–P2, Passes D + E clean) — and beyond it (0 open P3 too).
**Round 5 dispositions (functions deployed):**
- **E-OBS (P3) — FIXED + DEPLOYED** (`21b078a` senders, server live): all 12 FCM senders set `android.notification.channelId` (game→`game_activity`, chat/partner→`partner_activity`, reminders→`reminders`; gameRetention varies by type). **Verified live:** backgrounded QA→Sam chat push now shows `channel=partner_activity` (was `fcm_fallback_notification_channel`).
- **E-003 results-ready — FIXED + DEPLOYED** (`aaab768` client + server `game_session_id`): `onGameSessionUpdate` now sends `game_session_id`; client `gameResultsRouteFor` routes `game_results_ready` to the per-session replay (`thisOrThatReplay`/`howWellReplay`/`desireSyncReplay`/`wheelComplete`). **Verified live:** finished-game deep-link → "This or That Results" (0/10, that session), not hub/setup. (Live finished-push didn't post on Sam — fired while foreground + 20/day rate limiter after a full QA day; routing verified via the exact deep-link intent.)
- **Pass G — CLEAN (no findings):** sign-up + validation (weak-pw → "Password must be at least 8 characters."), fresh-account isolation (unpaired, zero couple data), **duplicate-email → `auth/email-already-exists`** (rejected), invite code **single-use + 24h expiry**, **bogus code → "Invite not found."** (rejected, friendly), recovery phrase client-generated. Sign-out → onboarding → debug-token restore all work.
- **Varied gameplay (Pass B style):** This or That Standard(10)/Deep + all-mismatch → "0/10 in sync — Total opposites" result path renders correctly (distinct from the 5/5 Quick/Light path). **Nav fuzzing:** rapid triple-tap opens the game setup once (launchSingleTop, no stacked duplicates); back-stack game→hub→Home→launcher clean; no dead-ends/double-back.
- **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) — 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.
- **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).
- **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.
- **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-NAV-001 ✅** — cold start (logged in) → Home → system Back → focus = `NexusLauncherActivity` (app exits). No onboarding resurfacing.
- **C-CC-001 ✅** — Play hub → Connection Challenges (active Gratitude Week) → single header (`Back` desc count = 1), no duplicate title.
- **Back-stack ✅** — clean cold-start hierarchy: deep screen (challenge) → Back → Play hub → Back → Home → Back → launcher. No double-back, no dead-ends. (Earlier "double-back" suspicion was warm nav-state restoration of the last Play-tab destination, not a real defect — does not reproduce from cold start.)
- **A-001 ✅ (couple-shared)** — QA set premium, Sam left free. Sam (5556, partner-premium) Play hub → Desire Sync opens to "How long?" setup (no paywall) + Memory Lane opens (sealed capsule shown). QA (5554, self-premium) likewise unlocked.
- **A-003 ✅** — Play hub shows **0 "Premium" badges** on both 5554 (self-prem) and 5556 (partner-prem couple-shared).
- **D-001 ✅** — Sam opened Memory Lane → capsule list renders, **0 PERMISSION_DENIED** in logcat (capsules rule holds; no hung heart → F-OBS path healthy).
**Desire Sync two-device playthrough (premium ON; QA started, Sam joined):**
- **B-001 ✅** — both answered all 5 → admin read shows **active=0** (session auto-flipped to completed, no "End their game" needed). Core loop intact.
- **C-DS-001 ✅** — 5554 (dark) reveal "You both said yes to" list renders crisp **white high-contrast** text (old dim muted-pink gone); 5556 (light) black-on-light. Both readable.
- Gameplay PASS — privacy logic correct (QA T,Y,Y,Y,T vs Sam T,Y,N,Y,F → exactly the 3 mutual-affirmative shown, 2 mismatches hidden), reveals match on both, no crash. Sam (free) joined QA's session = couple-shared join works.
**This or That two-device playthrough (immediately after Desire Sync — 2nd consecutive game):**
- **B-002 ✅** — QA started This or That → Sam's Home showed "Game waiting / Your partner is waiting to play" → Sam tapped **"Play now"** → landed directly in This or That **1/5** (the exact waiting game), NOT the generic hub.
- **B-001 ✅ (2nd consecutive game)** — both answered 5/5 → admin **active=0** again. Proven a couple can play two games back-to-back with no dangling/blocking session. Core loop solid.
- Gameplay PASS — both picked A on all 5 → Sam results "5/5 in sync — Two peas in a pod, matched on 5 of 5" with correct per-Q breakdown; consistent on both; **0 FATAL** in logcat.
**How Well Do You Know Me two-device playthrough (QA subject, Sam guesser):**
- Gameplay PASS (×2) — QA answered 5 about self (incl. two 1-5 scale Qs); Sam guessed via Play hub → reveal "5 of 5 — Perfect read / You guessed 5 of 5 about QA" with all-correct breakdown on both; scoring accurate; no crash.
- **B-001 ✅ (3rd game type)** — session auto-completed (active=0) once both submitted.
- **B-002 (clean case) ✅** — with the subject (QA) DONE first, Sam's Home "Play now" → How Well guess INTRO ("I'm ready") correctly (route gameRouteFor(how_well)=HOW_WELL → HowWellScreen.joinSession → INTRO).
- **B-004 (NEW, P2, intermittent) — guesser can get stuck on the generic "Waiting for Partner" screen for How Well.** Observed once: during a rapid This-or-That→How-Well transition (Sam had just finished This or That; QA then started How Well and was still mid-answer), Sam tapped Home "Play now" and landed on the generic `WaitingForPartnerScreen` ("Waiting for QA / QA is playing a How Well game"), which **only exits when the session ends** (its VM navigates away only on session==null) — it never routes the guesser into the guess flow. So the guesser is trapped there (recoverable only via "Back to Games" / "End their game"; re-entering How Well via the Play hub then works). NOT reproduced in the clean subject-done case (Play now → INTRO worked). Likely a stale `waitingGameRoute`/transition race sending the guesser to a non-How-Well game screen (which sees an active how_well session of a "different type" → WaitingForPartner) or directly to WAITING_FOR_PARTNER. **Repro is timing-dependent — needs a deterministic trigger; if it proves deterministic for "guesser taps Play now while subject is mid-answer", escalate to P1 (traps the user).** Report-only (logged, not fixed mid-pass).
**Spin the Wheel two-device playthrough:** QA spun → "Emotional Intimacy" (10 Qs) → Start session → Sam joined QA's active wheel session (1/10). Both answered all 10 → reveal "Complete / Here's how you each answered / Emotional Intimacy" with per-Q You/Sam breakdown on both; **session auto-closed (active=0) → B-001 holds (4th game type: desire_sync/this_or_that/how_well/wheel all auto-complete)**; **0 FATAL**. (Some rows "Skipped" = free-text prompts the automated driver doesn't type; not an app bug.)
**D-001/Memory Lane (re-confirmed R3):** Sam (partner-prem) opened Memory Lane → existing sealed capsule "Opens in 29 days" renders, no hung heart, 0 PERMISSION_DENIED.
**Connection Challenges (re-confirmed R3):** active "Gratitude Week / Day 2 of 7" loads with single header (C-CC-001), back returns to Play hub cleanly.
**Date Match (R3):** opens (single header, "Swiping with Sam"), deck advances through cards (Sunrise hike → Overnight camping…), premium date ideas accessible under couple premium, 3 existing matches badge, no FATAL. (Full mutual-match + live push verified R2-B2.)
**Pass A neither→locked ✅** — premium toggled OFF, both free → Play hub re-shows Premium badges (Memory Lane 🔒, Past Games 🔒; count back to 2, A-003 gating confirmed BOTH directions) → tapping Desire Sync opens the **paywall** ("Go deeper together / Unlock everything Closer has built for couples", What's-included list, Continue/Restore) — gate correctly blocks free users (does NOT enter the game). **Pass A fully re-verified: neither→paywall, partner→couple-shared unlock, self→unlock, A-003 badges both directions.**
- **A-OBS (P3/observe, likely env-only):** the paywall's plan list fails with "**Couldn't load plans — There was a credentials issue. Check the underlying error for more details.**" + disabled Continue. Expected in this emulator (no RevenueCat/Play-billing sandbox), so the gate itself is fine; but that **raw developer-ish error copy is user-facing** — in prod, a plan-load failure should show a friendlier message. Flag for copy review (not a gate bug).
**Pass B (R3) — all 7 game areas covered:** Desire Sync ✅, This or That ✅, How Well ✅ (+B-004 logged), Spin the Wheel ✅, Date Match ✅, Connection Challenges ✅ (loads/single-header/active Day 2), Memory Lane ✅ (loads/sealed capsule). **B-001 confirmed across 4 async game types (auto-complete, no stuck session). B-002 works (clean case). All fixes (B-001/B-002/B-003/C-DS-001) hold.**
**Pass C (R3) — deep-screen visual sweep (5554=Dark primary; several seen in Light on 5556 during A/B):**
Verified render cleanly, readable, **no FATAL, no new dark-mode contrast issues** — Home, Play hub, all 7 game screens (setup/play/reveal), Paywall, **Settings** (+ **Subscription** "One subscription for both partners — no double billing", + **Appearance** Theme radios), **Today**/daily-question (incl. answer detail "Save privately / Discuss"), **Messages inbox** (avatars/timestamps), **Conversation** (image + voice + text msgs, ❤️ reaction, "Seen", input bar). **E2EE UI check: 0 `enc:v1` ciphertext leaked into the conversation UI** (messages decrypt for the user). C-DS-001 dark-contrast fix holds.
- **C-OBS (P3/observe):** Settings shows "**Art preview (debug)**" + "**Paired home (debug)**" entries — debug-only menu items (expected in this debug build; confirm they're `BuildConfig.DEBUG`-gated so they don't ship in release).
- _Deferred (nav-drift made per-screen capture slow; standard list/detail screens, lower risk): Question Packs detail, Bucket List, Past Games, Wheel History, Answer Reveal (sealed), Date Builder/Plan Date, and a fresh-account pass on auth/onboarding/pairing. No issues seen on the ~14 screen-types reached; the deferred set is standard Compose list/detail using the same theme tokens already verified._
- **D1 at-rest ✓** — live admin read: chat `text`=`enc:v1:`, `lastMessagePreview`=`enc:v1:` (media-only msg has no text field = no plaintext); how_well answers + Memory Lane capsules = `enc:v1:` (Pass B). **No plaintext content leak.** UI check: 0 `enc:v1:` rendered to the user (Pass C conversation).
- D4 (wrapped couple key / KDF), D5 (App Check, gitignored SA JSONs, allowBackup=false), D6 (analytics metadata-only) unchanged since Round 1 — code identical, still hold.
- **D3 live non-member negative test: still deferred** — needs a 3rd fresh account not in the couple (only 2 emulators, both members; signing one out risks the App Check debug token + couple state). Rule logic is statically member-scoped (`isCouplesMember` gate on every couple subcollection) — denial holds by construction.
**Pass E (R3) — live notification tests (both FCM tokens valid, len=142):**
- **chat_message ✅ FULL CHAIN** — Sam backgrounded; QA sent a message → Sam received push **title "QA sent a message" / body "Tap to read and reply."** (content-free ✓, actual text NOT in payload → D6 holds) → **tapped → opened the exact conversation with the new message loaded** (deep-link ✓, background→cold path).
- **partner_started_game** — Sam backgrounded; QA started This or That → Sam received **"QA is playing / QA has started a game. Tap to join!"** (delivery ✓, content-free ✓). **BUT tap → landed on the generic Play hub, NOT the game.**
- **E-003 (NEW, P2) — game notifications deep-link to the generic Play hub, not the specific game/results.** Code: `PARTNER_STARTED_GAME`/`GAME_RESULTS_READY`/`PARTNER_COMPLETED_PART` all `routeFor → AppRoute.PLAY` (PartnerNotificationManager L270-272). The body says "Tap to **join**!" but the user lands on the hub and must find+tap the game card themselves (tapping it does then join the session, per B-002). Same gap B-002 fixed for the Home "Play now" card — never extended to notifications. Plan's Pass E wants "the **specific item**, not just the right tab" (strictly P1; rated P2 since it lands on the right tab and is recoverable). **Fix:** route game pushes through the active-session→game-route resolver (like HomeViewModel.gameRouteFor) so the deep-link joins the game. Report-only.
- **E-OBS (NEW, P3) — backgrounded pushes use `fcm_fallback_notification_channel`, not their code-defined channels.** The delivered chat + game pushes both landed on `fcm_fallback_notification_channel` even though the code assigns `CHANNEL_GAMES`/chat channels (PartnerNotificationManager L185). Means the server sends FCM "notification" (not data-only) messages, so the system auto-displays them on the fallback channel when backgrounded — bypassing the app's channel importance/sound and the per-category toggle (users can't mute just "Games"). **Fix:** send data-only FCM + build the notification client-side with the right channel, or set `android.notification.channel_id` in the FCM payload. Report-only.
- Foundations ✅ — both users registered FCM tokens; routing centralized in `PartnerNotificationType`; E-001 (daily_question/challenge_day_ready) + E-002 (partner_left→HOME) fixes present in code. date_match push live-verified R2-B2.
- _Full 17×{fg/bg/killed} matrix not exhaustively run; chat_message + partner_started_game live-verified this round (deliver+content-free; chat deep-link ✓, game deep-link → E-003). Remaining types: routing code-verified._
_Still to verify this round: edges (re-open completed / leave mid-game), Pass F._
| A-003 | Premium UI (cosmetic) | PlayHubScreen (Desire Sync + Memory Lane cards) | **P3** | The "🔒 Premium" badge on these two cards is static (rendered in separate card composables that don't receive `hasPremium`), so it still shows a lock even when the couple has premium access. Feature IS accessible (gate fixed in A-001) — only the badge is misleading. | With couple premium, QA's Play hub still shows 🔒 Premium on Desire Sync/Memory Lane though tapping enters the game. | **FIXED** — added `showPremiumBadge` param to `DesireSyncCard`/`MemoryLaneCard`, gated the badge behind it, pass `!hasPremium` from the Play hub. **Verified LIVE:** with couple premium, Play hub shows 0 "Premium" badges on those cards (both cards present, no lock). |
**Note (by-design, not a bug):** `SubscriptionScreen` uses per-user `isPremium()` — correct, it reflects the user's *own* subscription/account state, not a feature gate.
| B-001 | Games / sessions | couples/{id}/sessions | **P1** (was P3→P2→P1) | **A finished game NEVER closes its session — there is no normal-user path to complete it — so every game leaves a dangling `status=active` session that blocks ALL other games.** Definitively proven on the R2-B2 restart: played This or That fully through on BOTH devices → both reached the results screen → **BOTH tapped the intended "Back to Play" button** → both navigated back to the Play hub, **but the session stayed `active`** (re-checked at +0s and +12s; no cloud-function cleanup; `completedAt` never set). So neither "Back to Play" nor leaving to Home completes a finished session — the ONLY thing that does is the **destructive "End their game"** (which the next game offers as "Sam is playing a … game", misleading copy since nobody is actually playing). Net: a couple **cannot cleanly play two games in a row** — after every game, the next one is blocked until one partner kills the (already-finished) session. This breaks the core game loop for every session → **P1**. **ROOT CAUSE (found in fix phase): a Firestore RULES bug, not app code.** The sessions `allow update` rule required `affectedKeys().hasOnly(['status','completedAt'])`, but the async-game completion path (`markUserComplete`) always writes **`completedByUsers`** (each player records themselves; the session flips to `completed` only once both are in). So every "I reached results" write was **denied** (the failure is swallowed by `onFailure`), `completedByUsers` never reached 2, and the session stayed `active` forever. `abandonSession` ("End their game") only diffs `status`/`completedAt`, so it passed the rule — exactly why that was the only thing that worked. | Play This or That to results on both → session stayed `active`; next game blocked. | **FIXED + DEPLOYED** — sessions `allow update` now permits `['status','completedAt','completedByUsers']`, lets any couple member record completion progress, keeps `startedByUserId` immutable + status monotonic (active→completed, never revert). **Re-verified LIVE:** played This or That fully on both → session auto-flipped to `status=completed`, `completedByUsers=[both]`, **0 active sessions** (no Back-to-Play/End-their-game needed); then **opened How Well immediately → its setup screen, NOT "Waiting for Sam"**. Core loop restored. |
| B-002 | Home → Play nav (play-as-user) | HomeScreen "Your partner is waiting to play" card → "Play now" | **P2** | The Home card explicitly promises resuming the specific waiting game — "**Your partner is waiting to play. A game is ready for the two of you. Jump back in and keep the ritual going.**" → **"Play now"** — but tapping it just lands on the **generic Play hub** (the game list). It does NOT open/resume the waiting game, and the Play hub shows **no indication of which game is waiting** nor any "resume" affordance. A user told to "jump back in" cannot tell what to tap or how to rejoin. (Also: BOTH partners' Home cards say "**your** partner is waiting to play" for the same session, so each thinks the other is mid-game.) **Fix:** "Play now" should deep-link into the active session (its play/results screen), or the Play hub should surface a "Resume — How Well" entry; the Home copy should reflect whose turn it actually is. | Cold start → Home → tap "Play now" → lands on Play hub, no waiting-game indicator. | **FIXED** — Home now resolves the active session's `gameType` → its resume route (`gameRouteFor`: wheel→SpinWheelRandom, this_or_that/how_well/desire_sync→themselves), stored as `HomeUiState.waitingGameRoute` and carried on `HomeAction.gameRoute`; `HomeActionTarget.Game` navigates there (fallback Play hub). Each game screen auto-joins the couple's active session on open, so "Play now" resumes the exact waiting game. **Verified LIVE:** Sam started This or That → QA Home "Play now" → landed directly in This or That (1/5), not the hub. |
| B-003 | Desire Sync results (copy/clarity) | DesireSyncScreen results | **P3** | The results stats are internally **inconsistent/confusing**. Header: "**3 shared desires — 2 answers stayed private.**" Per-person row: "**You 5 private / Sam 5 private**". Progress bar caption: "**3 shared, 2 kept private.**" So the same screen says both "2 kept private" (total) AND "5 private" (each person) — a user can't tell whether 3 are shared or all 5 stayed private. (Mechanically "5 private" likely means "all 5 of each person's raw answers stay private, 3 happened to overlap", but that framing isn't clear and contradicts the "2 kept private" line.) **Fix:** make the three counters consistent (e.g., drop or relabel the per-person "5 private", or clarify "your individual answers are always private"). | Play Desire Sync to results → read the three differing private/shared counts. | **FIXED** — the per-person privacy tiles no longer show the contradicting "$total private"; they now read just "Private" (your individual answers always stay private), and the caption keeps the real "$matches shared, N kept private" breakdown. **Verified LIVE:** reveal now shows "You: Private / Sam: Private" + "5 shared, 0 kept private" — no contradiction. |
**Launch/crash sweep (QA, free):** This or That ✅ (mood/length select), How Well Do You Know Me ✅ (intro), Connection Challenges ✅, Spin the Wheel ✅ — all render, **no FATAL**. Desire Sync + Memory Lane are premium-gated (covered in Pass A; gameplay needs premium toggle). Date Match: todo. Full two-device start→finish + results not exhaustively re-run this round (the prior round verified `onGameSessionUpdate` start/finish end-to-end).
**R2-B2 Desire Sync playthrough (couple-shared premium):** QA (free) entered with NO paywall (A-001 holds live). Both played full 5 Yes/No; QA T,T,T,T,F + Sam T,T,F,T,F → results show **exactly 3 shared desires** (the mutual-yes Q1/Q2/Q4) with Q3 (mismatch) and Q5 (both no) correctly **hidden** — reveal/privacy logic CORRECT; results match on both devices; no crash. Findings: B-003 (P3 copy), C-DS-001 (P2 dark contrast on revealed list).
| C-CC-001 | Nav / layout — duplicate header + double back | ConnectionChallengesScreen (series list) | **P2** | The screen shows **TWO stacked "Connection Challenges" titles, each with its own back arrow** — a nav-scaffold app bar (title "Connection Challenges" + back) AND an in-content header ("Connection Challenges / Pick a series to build a habit together." + a second back arrow right below it). Verified it's a **redundant duplicate header, not a double-pushed route**: tapping the inner back arrow pops straight to the Play hub (same as the app-bar back). No dead-end, but two identical titles + two back buttons is confusing ("which back do I press?") and looks broken — exactly the "double back" case to flag. **Fix:** drop the in-content TopAppBar/back (let the nav scaffold own the title+back), or remove the scaffold bar for this route. | Play hub → Connection Challenges → two "Connection Challenges" headers + two back arrows stacked at top. | **FIXED** — removed `CONNECTION_CHALLENGES` from `shellBackRoutes` (the screen renders its own header for both the pick + active views, unlike This or That/How Well/Desire Sync which rely on the shell). **Verified LIVE:** the screen now shows a single header + single back arrow (1 "Back", no duplicate title). |
| C-DS-001 | Theming / readability (dark) | DesireSyncScreen results — "You both said yes to" list | **P2** | In **dark mode**, the revealed shared-desire list items render as **dim, low-contrast muted-pink text on a dark pink-tinted card** — legible but well below the crisp high-contrast black text the same items show in **light mode** (verified side-by-side: QA dark vs Sam light). Given the user's "text must be readable" bar — and that this is the intimate payoff content the user most wants to read — the dark-mode contrast is too low. (May be intentional "muted" styling; if so it needs a dark-mode-specific brighter token.) | 5554=Dark: play Desire Sync to results → the 3 shared-desire rows are dim/hard to read vs the same rows on 5556=Light. | **FIXED** — `DesireMatchCard` text color was a hardcoded dark plum `Color(0xFF3D1F2E)`; changed to theme-aware `MaterialTheme.colorScheme.onSurface` (dark text on light, light text on dark). **Verified LIVE:** played Desire Sync to reveal in dark mode → shared-desire rows now render crisp high-contrast white text. |
_Deep/stateful screens (answer reveal, wheel session/complete, date match/builder/matches, bucket list, memory capsule, history, paywall, auth/onboarding/pairing) need their states set up — pending next chunk._
| C-NAV-001 | Nav / back-stack — onboarding+auth not popped after login | MainActivity AppNavigation (start/auth/onboarding graph) | **P1** | **The auth + onboarding destinations are never popped from the nav back stack after login, so pressing system Back from Home walks BACKWARD into onboarding → the welcome/login screen instead of exiting the app.** Confirmed with a CLEAN reproduction (no scripted pollution): cold start → land on **Home** (authenticated, "Connected with Sam") → press system **Back once** → lands on the **"Answer honestly" onboarding carousel** (still inside `closer.app/app.closer.MainActivity`, so it's in-app nav, not a separate task). Tapping the carousel's **Skip** then reaches **"Closer — Create account / I already have an account"** (the pre-auth welcome) — i.e., a logged-in user pressing Back appears to be logged out. Not data loss (cold start returns to Home; Firebase auth persists), but it's a core, every-user nav defect and very alarming UX. **Fix:** on successful auth/onboarding completion, navigate to Home with `popUpTo(<auth/onboarding graph or start route>) { inclusive = true }` (and `launchSingleTop`) so Home is the back-stack root and Back from Home exits the app. | Cold start (logged in) → Home → press system Back → onboarding carousel appears instead of the app closing. | **FIXED** — in `AppNavigation.navigateRoute`, navigating to HOME *from* an entry route (ONBOARDING/CREATE_PROFILE/PAIR_PROMPT/LOGIN/SIGN_UP/FORGOT_PASSWORD) now does `navigate(HOME){ popUpTo(0){inclusive=true}; launchSingleTop }`, wiping the entire pre-app flow so HOME is the back-stack root. Normal tab-switch semantics (`selectTab`) untouched. **Re-verified LIVE:** cold start (logged in) → Home → system Back → focused activity is the **launcher** (`NexusLauncherActivity`), app exits cleanly — onboarding no longer resurfaces. |
**Pass B requirement (updated):** each game must be **played one complete time through on both devices** (start → every step → finish/reveal/results), not just launched. Round 1 did launch-only → **full playthroughs owed in Round 2** for all 7 (premium games need a premium toggle). A launch-only result = `partial`, not `pass`.
**Pass C requirement (added):** **navigation from every entry point** (each screen reached from all its links — e.g. conversation from inbox/Discuss/notification; game from Play/notification; paywall from each gate) + **back-stack / "double-back"** (system back AND in-app back return to the right place from each entry; no dead-ends, no exit-app surprise, **no screen needing two backs**/duplicate stack entries; deep-link/notification entries land with a sane back stack). Owed in Round 2. Wrong/double back or dead-end = P2 (P1 if it traps the user).
| F-OBS | Resilience (UI) | **P3** | MemoryLaneScreen (and likely others) **hangs on the loading indicator forever** when a Firestore query fails, instead of showing an error/empty state. Masked the D-001 root cause. Add load-failure handling. | Was visible before D-001 fix (stuck heart). | **FIXED** (code) — ROOT CAUSE: `FirestoreCapsuleDataSource.observeCapsules`**swallowed** snapshot-listener errors (`if (err != null …) return@`), so on PERMISSION_DENIED the callbackFlow never emitted or closed → the ViewModel's `collect` suspended forever → stuck loading heart. Now it `close(err)`s the flow, so the ViewModel's existing `runCatching.onFailure` → `MemoryLanePhase.ERROR` (with a Retry) runs. Build green; live-verify needs an induced query failure (deferred). (Other snapshot listeners with the same swallow pattern are a follow-up sweep.) |
| (outcomes) | Rules | — | The Round-1 `outcomes` list `PERMISSION_DENIED` is **by-design** — the rule restricts reads to specific dayKeys (`day_0/30/60/90`); a bare list query is correctly denied. Not a bug. | — | Closed (by-design) |
- **Copy carries no private content:** all function notification bodies are generic ("Tap to read and reply", "Answer together before it expires", etc.); `${title}` refers to public question/game titles, not user answers. ✓ (ties to D6)
- **Routing:** centralized in `PartnerNotificationType` (`fromRemoteType` → `routeFor`); chat opens the exact conversation, reveal→answerReveal(questionId), games→Play, capsule→Memory Lane, etc.
| E-002 | Notification routing | **P3** | `partner_left`, `partner_deleted_account`, `invite_created`, `spki` are unmapped → tap lands on default (no deep-link). Informational types; acceptable but ideally routed. | **FIXED** (code; live-tap deferred) — added `PARTNER_UNPAIRED` type, mapped `partner_left` + `partner_deleted_account` → it → routes to **HOME** (where the now-unpaired user gets the "Invite partner" CTA, matching the push body "Tap to create a new invite"). **Investigation corrected two false positives:**`invite_created` is a server-side **audit-log** entry (`read:true`, "not read by clients" — never a push), and `spki` is a **crypto key-format string** in the RevenueCat webhook (`crypto.createPublicKey({type:'spki'})`), not a notification type at all — neither needs client routing (documented in `fromRemoteType`). Build green; live tap-verify deferred (needs an actual unpair event). |