diff --git a/ClaudeQACoverage.md b/ClaudeQACoverage.md index 80a3c844..d00bb41d 100644 --- a/ClaudeQACoverage.md +++ b/ClaudeQACoverage.md @@ -1,120 +1,94 @@ # Claude QA Coverage Matrix -> Resume anchor. Status: `todo | pass | fail(→id) | n/a`. See `ClaudeReport.md` run-state header for current position. -> **Round 6 (branding + Future.md regression) COMPLETE 2026-06-25, client `f47c8e2`:** new surfaces from `95cad84` (white-keyhole icons, animated chip+fill loader, splash, pairing hero) + `f47c8e2` (inclusive gender, turn copy, push-budget split, results-suppression `ActiveGameSessionMonitor`, paywall retry/offline/hide-Continue, auth rotator). **0 new issues; still 0 open P0–P3.** Live: loader (both themes), splash→handoff, launcher icon, ToT+How Well open (no crash → #4 VM injection sound), paywall purchase screen (friendly error + Try again + Continue hidden, online→generic msg), onboarding illustration. Unit tests green. Gender step / rotator / turn-copy / results-timing / weekly-cap = code+unit-verified (live deferred: fragile multi-text-field & 2-device timing; low risk over proven patterns). Baseline restored (QA re-signed-in via admin token; couple intact). -> **Round 5 (functions deploy + expanded re-QA) COMPLETE 2026-06-25, client `765916a` + functions DEPLOYED:** E-OBS FIXED+DEPLOYED (12 senders set channelId; chat push → `partner_activity` live) + E-003 results-ready FIXED+DEPLOYED (finished-game → per-session results). **0 open P0–P3.** New Pass G (account creation + fake-account) clean. Varied gameplay (Standard/Deep, 0-match) + nav fuzzing — no new bugs. Baseline restored (couple intact, throwaway deleted, Sam re-paired). -> _Round 4: E-003 + B-004 (P2) + A-OBS (P3) FIXED + verified live._ +> **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`. +> Build `23dd6a7`. Position + verdict: see `ClaudeReport.md` run-state. **Verdict: 0 open issues (P0–P3); A–G covered, I/J pending.** +> Hygiene: this is a *current-status* matrix, not a per-round changelog — `fail→id` flips to `pass` once a fix is +> confirmed (ID archived below); finished rounds collapse to the history line. (See Report hygiene in `ClaudeQAPlan.md`.) -## Pass G — Account creation, validation & fake-account abuse -**R5 live:** sign-up flow end-to-end (email/pw/confirm → profile 3 steps → unpaired home) ✓; weak-password → friendly -"at least 8 characters" error ✓; fresh-account **isolation** (unpaired "Invite my partner", zero couple data) ✓; -**duplicate-email → `auth/email-already-exists`** rejected ✓; invite code **single-use + 24h expiry** shown, **bogus -code "ZZZ-ZZZ" → "Invite not found."** rejected (friendly, not paired) ✓; **recovery phrase** client-generated ✓; -sign-out → onboarding carousel → debug-token restore ✓. **No security findings.** (Rules-level non-member READ denial: -covered by app-level isolation + static member-scoped rules audit; live crafted-request blocked by App Check.) +## Status at a glance +| Pass | Coverage | Status | +|---|---|---| +| A — Couple-shared premium | all gated features × neither/partner/self | ✅ pass | +| B — Games lifecycle | all 7 games played full, 2-device, real user-nav | ✅ pass | +| C — Visual (light+dark) | ~14 core screen-types both themes | ✅ pass · deep/list screens **deferred** | +| D — Security & encryption | D1 at-rest · D2 rules · D3 live raw-API · D4–D6 | ✅ clean | +| E — Notifications | chat + game start/finish/results live, both-client + suppression | ✅ pass · full fg/bg/killed matrix **partial** | +| F — Resilience | concurrency · offline · lifecycle · process-death · time | ✅ pass | +| G — Account creation / fake-account | sign-up · validation · duplicate · invite-abuse | ✅ pass | +| H — Branding & artwork | consumer brand walk → prompts | see `ClaudeBrandingReview.md` | +| I — Performance & route efficiency | — | **todo (Round 8)** | +| J — Accessibility | — | **todo (Round 8)** | -## Pass A — Couple-shared premium (states: neither / partner-only / self) +**Archived issue IDs (fixed + confirmed, detail in git):** A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · C-NAV-001 · D-001 · E-001 · E-002 · E-003 · E-OBS · F-OBS. Pending one confirm: **F-RACE-001**. + +--- + +## Pass A — Couple-shared premium (neither / partner-only / self) | Feature | neither→locked | partner→both unlock | self→unlock | Status | |---|---|---|---|---| -| Chat media + reactions | pass | pass | pass | pass (couple-shared) | -| Play: Desire Sync | pass | **fail→A-001** | pass | fail→A-001 | -| Play: Memory Lane | pass | **fail→A-001** | pass | fail→A-001 | -| Play: Connection Challenges | pass | fail→A-001 | pass | fail→A-001 | -| Question Packs (premium) | pass | fail→A-001 | pass | fail→A-001 | -| Wheel: Category Picker / Spin / History | pass | fail→A-001 | pass | fail→A-001 | -| Date Match / Plan Date | pass | fail→A-001 | pass | fail→A-001 | -| Subscription screen (own status) | n/a | n/a | n/a | pass (by-design per-user) | +| Chat media + reactions | pass | pass | pass | ✅ pass (reference pattern) | +| Play: Desire Sync | pass | pass | pass | ✅ pass | +| Play: Memory Lane | pass | pass | pass | ✅ pass | +| Play: Connection Challenges | pass | pass | pass | ✅ pass | +| Question Packs (premium) | pass | pass | pass | ✅ pass | +| Wheel: Category Picker / Spin / History | pass | pass | pass | ✅ pass | +| Date Match / Plan Date | pass | pass | pass | ✅ pass | +| Subscription screen (own status) | n/a | n/a | n/a | ✅ pass (by-design per-user) | -Pass A: **complete** (1 systemic P1). **A-001 FIXED** (e8892a9) — couple-shared everywhere; re-verify each feature in re-QA. New cosmetic A-003 (P3, badge). Subscription screen by-design. -**R3 re-verified LIVE (2026-06-25):** neither→paywall ("Go deeper together"), partner→couple-shared unlock (Sam free entered Desire Sync + Memory Lane), self→unlock; A-003 badges hidden under premium / shown when free (count 0↔2). New A-OBS (P3): paywall plan-load shows raw "credentials issue" error (env: no RevenueCat sandbox). +Verified live: neither→paywall ("Go deeper together"); partner→couple-shared unlock (Sam free entered Desire Sync + Memory Lane); self→unlock; premium badges hidden under premium / shown when free. (A-001 couple-shared gap + A-003 badge fixed & confirmed.) -## Pass B — Games lifecycle (start / play / finish + results) -**RESTARTED 2026-06-24 (R2-B2): full re-run from game #1 with the PLAY-AS-THE-USER mindset** (navigate only via the -real in-app path; report-first-then-workaround on any broken flow). Prior R2 This or That / How Well passes are -superseded — redo every game cleanly. (Prior result for reference: This or That 5/5 ✅, How Well 5/5 ✅.) -**✅ R2-B2 COMPLETE — all 7 games played one full time through on both devices via real user nav; gameplay all PASS.** -Findings surfaced by playing-as-user: **B-001 (P1)** finished session never closes → blocks next game; **C-NAV-001 (P1)** -back from Home resurfaces onboarding/auth; **B-002 (P2)** Home "Play now" → generic hub; **C-CC-001 (P2)** Connection -Challenges dup header/double-back; **C-DS-001 (P2)** Desire Sync dark-mode low contrast; **B-003 (P3)** confusing Desire -Sync counts. Sam reverted to free (baseline). `date_match` push verified live (Pass E bonus). - -| Game | starts | plays | finishes/results | no crash | Status | +## Pass B — Games lifecycle (start / play / finish + results, 2-device, real user-nav) +All 7 played one complete time through on both devices via the real in-app path; gameplay all PASS. +| Game | starts | plays | finishes/results | no crash | Evidence | |---|---|---|---|---|---| -| 1. This or That | pass | **pass (full, user-nav)** | **pass** | pass | **R2-B2: 5/5 via Play hub, answers synced, results match both (4/5 "Two peas in a pod", Q2 Differ correct), no crash ✅. Session-lifecycle bug B-001 (P1) hit on exit.** | -| 2. How Well Do You Know Me | pass | **pass (full, user-nav)** | **pass** | pass | **R2-B2: QA answered 5 (incl. a 1-5 scale Q5); Sam predicted via Play hub — 3 correct + 1 deliberate miss (Kind tone vs Specific examples) + scale match → results show 4/5 "You really know each other" with the wrong one marked ✗ on BOTH devices, scoring accurate, no crash ✅** | -| 3. Desire Sync | pass | **pass (full, user-nav)** | **pass** | pass | **R2-B2: QA(free) entered w/o paywall (A-001 live ✅); both answered 5 Yes/No → exactly 3 mutual desires revealed, mismatches hidden (privacy correct), results match both, no crash ✅. Findings: B-003 (P3 confusing counts), C-DS-001 (P2 dark-mode low contrast on revealed list).** | -| 4. Connection Challenges | pass | **pass (day-cycle, user-nav)** | **pass** | pass | **R2-B2: opened (D-001 rules hold ✅); started Gratitude Week → both completed Day 1 → day ✓, 🔥1 streak, advanced to Day 2 "Both of you showed up today", synced on both, no crash ✅. (7-day series is time-gated; full per-day cycle verified.) Finding: C-CC-001 (P2 duplicate header + double back). Minor: first partner's view shows next-day content + "waiting for partner" before the day is mutually done (self-resolves).** | -| 5. Memory Lane | pass | **pass (create+seal, user-nav)** | **pass (sealed)** | pass | **R2-B2: loads clean (D-001 ✅, no hung heart); QA wrote a capsule (title+body), picked "1 month" → sealed "Opens in 29 days"; **encrypted at rest** (title+content `enc:v1:`, `unlockAt`=+30d, status=sealed); Sam sees the same sealed capsule cross-device; no crash / no PERMISSION_DENIED ✅. Unlock/reveal is future-dated (can't test w/o time-travel). Single header (no C-CC-001 here).** | -| 6. Spin the Wheel | pass | **pass (full, user-nav)** | **pass** | pass | **R2-B2: QA(free) entered (A-001 ✅); spun → "Date Night" category → both answered all 10 prompts (multi-select) → reveal "Here's how you each answered" with per-Q You/partner breakdown matching on BOTH devices, no crash ✅. Wheel session synced (Sam joined QA's active session). Dark answer text a bit dim (C-OBS pattern, readable).** | -| 7. Date Match | pass | **pass (full, user-nav)** | **pass** | pass | **R2-B2: QA(free) entered (A-001 ✅, in Play hub below Question Packs); both swiped date-idea deck (❌/⭐/💗); QA + Sam both liked the same 3 → 3 `date_matches` created (sunrise_hike/kayak/rock_climbing); Sam got "It is a match!" modal + LIVE "It's a match!" push notification; "Your Matches" shows all 3 "Mutual love"; no crash / no PERMISSION_DENIED ✅. (Premium-badged ideas accessible via couple premium.)** | +| 1. This or That | pass | pass | pass | pass | 5/5 via Play hub, answers synced, results match both ("Two peas in a pod"). | +| 2. How Well Do You Know Me | pass | pass | pass | pass | QA answered 5 (incl. 1–5 scale); Sam predicted via hub → 4/5, wrong one marked ✗ on both, scoring accurate. | +| 3. Desire Sync | pass | pass | pass | pass | QA(free) entered w/o paywall; both answered 5 Yes/No → exactly 3 mutual desires, mismatches hidden, match on both. | +| 4. Connection Challenges | pass | pass | pass | pass | Gratitude Week → both did Day 1 → 🔥1, advanced to Day 2 synced. (7-day series time-gated; per-day cycle verified.) | +| 5. Memory Lane | pass | pass (create+seal) | pass (sealed) | pass | Capsule sealed "Opens in 29 days", encrypted at rest (title+content `enc:v1:`), cross-device. Unlock future-dated. | +| 6. Spin the Wheel | pass | pass | pass | pass | Spun → category → both answered all 10, per-Q You/partner breakdown matches both, session synced. | +| 7. Date Match | pass | pass | pass | pass | Both swiped deck, 3 mutual likes → 3 `date_matches`, "It's a match!" modal + live push, "Your Matches" shows all 3. | -_Note: stale active session blocked games (B-001); cleared via in-app "End their game" (recovery verified). Exit each game via Back to Play between games so the session closes._ -**REQUIREMENT (updated): each game must be played ONE COMPLETE time through on both devices (every step → finish/ -reveal/results), not just launched.** All rows above are currently `launch ok / partial` only → **full playthrough -still owed for every game** in Round 2 (premium games need a premium toggle). A launch-only row counts as `partial`, not `pass`. +Note: exit each game via "Back to Play" between games so the session closes (B-001 auto-completion fix verified). F-RACE-001 (simultaneous start) fixed — see Pass F. ## Pass C — Visual (light + dark), all ~50 routes -**R3 (2026-06-25):** ~14 screen-types swept in Dark (5554), several in Light (5556 during A/B) — all render clean, -readable, no FATAL, no new dark-mode contrast issues; **0 `enc:v1:` leaked to conversation UI**. Covered: Home, Play -hub, all 7 game screens (setup/play/reveal), Paywall, Settings (+Subscription +Appearance), Today/daily-question -(+answer detail), Messages inbox, Conversation (image+voice+text+reaction). C-DS-001 dark-contrast fix holds. -**Back-stack ✅** deep→hub→Home→launcher clean (no double-back; C-NAV-001 holds). C-OBS resolved (debug menu gated). -_Deferred (nav-drift; standard list/detail, lower-risk): Question Packs detail, Bucket List, Past Games, Wheel History, -Answer Reveal (sealed), Date Builder/Plan Date, fresh-account auth/onboarding/pairing._ +~14 screen-types swept Dark (5554) + several Light (5556): all render clean, readable, no FATAL, no dark-mode contrast issues; **0 `enc:v1:` leaked to conversation UI**. Covered: Home, Play hub, all 7 game screens (setup/play/reveal), Paywall, Settings (+Subscription +Appearance), Today/daily-question (+answer detail), Messages inbox, Conversation (image+voice+text+reaction). Back-stack clean (deep→hub→Home→launcher, no double-back). +- **Deferred** (standard list/detail, lower risk; cover in Round 8): Question Packs detail · Bucket List · Past Games · Wheel History · Answer Reveal (sealed) · Date Builder/Plan Date · fresh-account auth/onboarding/pairing. -## Pass D — Security & Encryption (D1–D6) -**R7 DEEP DIVE (multi-angle, 2026-06-25):** **D1 at-rest — CLEAN (admin ground-truth read):** messages `text` + -`lastMessagePreview`, all 4 game-answer collections (`this_or_that`/`how_well`/`desire_sync`/`wheel`, both users), -capsule title+content, `date_swipes.actions` = `enc:v1:`; `wrappedCoupleKey` = ciphertext (recovery-phrase-wrapped, -**argon2id**); `encryptedRecoveryPhrase` server-blind + **wiped on acceptance** (confirmed absent on accepted invite); -plaintext `inviteCode` **not exploitable** (no code-encrypted secret persists; `/invites/{code}` readable only by -inviter). **D3 raw-API negative (LIVE, executed — no longer deferred):** non-member ID token (Identity Toolkit -`signInWithCustomToken`) → Firestore REST on couple doc/conversation/messages/answers/session/capsules/partner-profile -= **all `403 PERMISSION_DENIED`**; non-member writes (couple doc, partner entitlement, **real path -`users/{uid}/entitlements/premium`**) = **all `403` → no self-grant**. Member token reads `200` (characterizes layer: -**App Check not enforced on Firestore — rules are the sole gate, and they hold**). Only writable = cosmetic own-doc -fields (`plan`) that **no gate reads**. **No P0/P1 security findings.** Two hardening notes → `Future.md`. +## Pass D — Security & encryption (D1–D6) — clean, no P0/P1 +- **D1 at-rest (admin ground-truth):** messages `text` + `lastMessagePreview`, all 4 game-answer collections (this_or_that/how_well/desire_sync/wheel, both users), capsule title+content, `date_swipes.actions` = `enc:v1:`; `wrappedCoupleKey` ciphertext (recovery-phrase-wrapped, **argon2id**); `encryptedRecoveryPhrase` server-blind + **wiped on acceptance**; plaintext `inviteCode` **not exploitable** (no code-encrypted secret persists; `/invites/{code}` readable only by inviter). +- **D2 rules:** no catch-all, no blanket `if true`; sessions update allowlist + immutable `startedByUserId` + monotonic status; `hasPremium` + entitlements server-only; ciphertext enforced on private fields; capsules/challenges member-scoped. +- **D3 raw-API negative (LIVE):** non-member ID token → Firestore REST on couple doc/conversation/messages/answers/session/capsules/partner-profile = **all 403**; non-member writes incl. real `users/{uid}/entitlements/premium` = **all 403 → no self-grant**. Member token reads 200 → **App Check not enforced on Firestore; rules are the sole gate and hold**. +- **D4/D5/D6:** wrapped couple key + KDF; App Check (client), gitignored SA JSONs, `allowBackup=false`; analytics metadata-only. Unchanged, hold. +- Two hardening notes → `Future.md` (App Check off on Firestore; `users/{uid}` update rule allows arbitrary non-`hasPremium` fields). - -**R3:** D2 deployed rules re-audited ✅ (B-001 sessions + D-001 capsules/challenges fixes present; hasPremium + -entitlements server-only; ciphertext enforced; no catch-all). D1 at-rest ✅ (chat text + lastMessagePreview = -`enc:v1:`; how_well answers + capsules = `enc:v1:`). D4/D5/D6 unchanged since R1 (code identical) → hold. -**D3 live non-member: deferred** (needs a 3rd fresh account; only 2 emulators, both couple members; rule logic -statically member-scoped). No P0/P1 security findings. - -## Pass E — Notifications (17 types × {foreground, background, killed} + tap-to-open) -**R6 live (games + messages, 2026-06-25, build `f47c8e2`):** full live two-device run. -- **chat_message** ✅ end-to-end: Sam→QA (QA bg) posts on **channel=partner_activity**, title "Sam sent a message" - (partner name, not private), body "Tap to read and reply." — **message text NOT in payload** (privacy holds); - small icon = white monochrome mark; tap→**main conversation with content** (verified via the exact intent — - shade-tap is flaky in the adb harness, lands on launcher, but the contentIntent routing is sound). -- **partner_started_game** ✅: QA started This or That → Sam (bg) posts on **channel=game_activity**, "QA is playing / - QA has started a game. Tap to join!" (content-free); tap→**joins the active session** (same 1/5 prompt). -- **partner_finished_game / results** ✅: both finished → **results push DELIVERED to backgrounded QA** (Round 5 - couldn't confirm this live) on **channel=game_activity**, "Sam finished the game / Sam finished — tap to see your - results!" (content-free); tap→**per-session This or That results** (5/5), per E-003. -- **#4 results-suppression** ✅: Sam stayed foreground on the session throughout → received **0** notifications - (the partner_completed_part + partner_finished_game pushes to Sam were suppressed by ActiveGameSessionMonitor), - while backgrounded QA got the results push. Clean confirmation of both delivery + suppression. -- No FATAL either device; baseline tidy (0 active sessions, couple intact). **No issues found.** - - -**R3 live:** FCM tokens valid for both. **chat_message ✅ full chain** (bg deliver + content-free + tap→exact -conversation w/ content). **partner_started_game**: bg deliver + content-free ✅; tap→Play hub (not the game) = -**E-003 (P2)**. **E-OBS (P3)**: bg pushes use fcm_fallback channel. date_match live-verified R2-B2. E-001/E-002 fixes -present in code. Full 17×{fg/bg/killed} matrix not exhaustively run; routing centralized + code-verified for the rest. +## Pass E — Notifications (type × {foreground / background / killed} + tap-to-open, both clients) +Full live two-device run (games + messages): +- **chat_message** ✅ end-to-end — channel `partner_activity`, title "Sam sent a message" (name, not private), body content-free, **text NOT in payload**; tap → exact conversation with content; white monochrome small icon. +- **partner_started_game** ✅ — channel `game_activity`, "QA is playing… Tap to join!" (content-free); tap → joins the active session. +- **partner_finished_game / results** ✅ — results push delivered to backgrounded partner, channel `game_activity`, content-free; tap → per-session results (per E-003 fix). +- **results-suppression** ✅ — partner foregrounded on the session received 0 pushes (ActiveGameSessionMonitor), while backgrounded partner got the results push. Delivery + suppression both confirmed. +- **Deferred (Round 8):** the full 17-type × {fg/bg/killed} matrix isn't exhaustively run live — remaining types are routing-code-verified + centralized in `PartnerNotificationType`; date_match push verified live. New types added to the plan's Pass E inventory (`join_game`, `partner_joined_game`, `game_ended`, `date_plan_update`, etc.) = **todo**. ## Pass F — Resilience / lifecycle / concurrency / time -**R7 DEEP DIVE:** **concurrency race — FOUND + FIXED F-RACE-001 (P1):** simultaneous game start created 2 divergent -sessions (TOCTOU in `startGameWithCouple`). **Fixed** via atomic transactional create on a per-couple `sessions/_active` -pointer (`startSessionAtomically`) + rule + deploy. **Verified live:** atomic create → 1 session + pointer; sequential -2nd start → joins (1); **parallel-tap race → 1 session (was 2)**; 0 FATAL. **Malformed/abusive deep-link intents** -(unknown type, missing extras, injection/path-traversal) → 0 crash. **Killed-state cold-start** chat deep-link → -conversation loads, 0 crash. Minor follow-up note: the race-loser sometimes lands on the Play hub rather than -WaitingForPartner→"Join the game" (no dup/crash; pre-existing routing). +- **Concurrency race:** F-RACE-001 (P1) found + **fixed** (atomic transactional create on per-couple `sessions/_active` pointer) + **verified live** (parallel-tap race → 1 session, was 2). *Pending one confirmation round, then prune.* +- **Offline:** airplane mode → Today renders from cache, no crash. +- **Lifecycle:** rotation/config-change → state preserved; ~6 cold restarts → clean to Home (auth persists). +- **Robustness:** malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal) → 0 crash; killed-state cold-start chat deep-link → conversation loads. +- **Deferred (Round 8):** time-travel-gated content (capsule unlock, challenge day-gating); broader network-flaky across answers/dates; account-lifecycle (unpair→re-pair, deletion cascade) deep run. Minor note: race-loser sometimes lands on Play hub vs WaitingForPartner (no dup/crash; pre-existing routing). +## Pass G — Account creation, validation & fake-account abuse +Sign-up end-to-end (email/pw/confirm → 3-step profile → unpaired home) ✅; weak-password → friendly "at least 8 characters" ✅; fresh-account isolation (zero couple data) ✅; **duplicate-email → `auth/email-already-exists`** rejected ✅; invite single-use + 24h expiry, **bogus code → "Invite not found."** ✅; recovery phrase client-generated ✅; sign-out → onboarding → debug-token restore ✅. **No security findings.** (Non-member READ denial = live D3 above + app-level isolation.) -**R3:** offline (airplane mode) → Today renders from cache, no crash ✅; rotation/config-change → landscape renders, -state preserved, no crash ✅; process-death/restore → ~6 cold restarts all clean to Home (auth persists) ✅; -concurrency → both devices played games simultaneously, sessions synced + B-001 auto-complete on concurrent finish ✅. -Time-gated content (capsule "Opens in 29 days", challenge day-gating) can't be time-traveled — noted. +## Passes H / I / J +- **H Branding** — deliverable in `ClaudeBrandingReview.md` (consumer brand walk → ready-to-paste art prompts). +- **I Performance & route efficiency** — **todo (Round 8):** gfxinfo/jank, redundant-read counts, listener-leak check, route smoke checklist. +- **J Accessibility** — **todo (Round 8):** font_scale 1.3/1.5/2.0, TalkBack semantics, WCAG-AA contrast, 48dp targets, keyboard, reduce-motion. + +--- + +## Round history (one line each) +- **R7** — security/concurrency deep dive (multi-angle): cornerstone clean; F-RACE-001 found+fixed+verified. 0 new open. +- **R6** — branding drop + Future.md backlog regression: 0 new open. +- **R5** — Cloud Functions deployed (E-OBS/E-003) + new Pass G clean: 0 open. +- **R2–R4** — play-as-user game restart + fix phase; all P0–P2 fixed + verified (archived IDs above). diff --git a/ClaudeQAPlan.md b/ClaudeQAPlan.md index 82c555bd..332590df 100644 --- a/ClaudeQAPlan.md +++ b/ClaudeQAPlan.md @@ -8,28 +8,45 @@ > parity → **Part 3** = run these same passes on iOS + a cross-platform (Android↔iOS) pass. **Parts 2 & 3 live in > `ClaudeiOSPlan.md`** (note: iOS build/run/QA requires macOS — not possible from this Linux box). +## Where every finding goes (route it here — exactly one home each) +| What you found | Where it goes | Form | +|---|---|---| +| **A bug** — broken / incorrect / crashing / insecure, premium bypass, wrong-or-missing notification, dead-end nav | **`ClaudeReport.md`** | Table row: stable ID (`A-001`, `E-003`…) + severity (P0–P3) + repro + status | +| **An idea / improvement** — works but could be better, confusing copy, missing affordance, rough-but-not-broken flow, "it'd be great if…", feature idea | **`Future.md`** `## QA` | Short title + what prompted it + suggested improvement | +| **New artwork to create** — illustrations, glyphs, image-gen prompts | **`ClaudeBrandingReview.md`** | House-style prompt + placement | +| **What got tested + its status** (pass / fail / todo / deferred) | **`ClaudeQACoverage.md`** | Coverage cell (the resume anchor) | + +- A branding **defect** (mis-colored, clipped, off-brand, low-contrast art) is a **bug → `ClaudeReport.md`**, not a brand + idea — only *new art to create* goes to `ClaudeBrandingReview.md`. +- Logging an idea in `Future.md` is **never** a substitute for filing a real defect: if it's broken, it gets an ID in + `ClaudeReport.md` too. +- Bug lifecycle: filed in `ClaudeReport.md` → fixed → kept **one** confirmation round → pruned to the archived-ID line + (detail lives in git). `Future.md` ideas sit in the backlog until built. (See **Report hygiene** under Reporting.) + ## Context Drive the real app on both emulators, verify each thing live, report, fix, re-verify. Five QA dimensions: 1. **Couple-shared premium** — if EITHER partner is premium, **all** premium features unlock for **both**. -2. **Games** — each starts, plays, finishes correctly on both devices. +2. **Games** — each starts, plays, **joins, resumes**, finishes, **and reopens results** correctly on both devices. 3. **Full visual pass, light + dark** — every screen, text readable, nothing clipped/invisible. 4. **Security & encryption (cornerstone)** — every private field is ciphertext at rest, rules hold against non-members, keys/recovery are sound. Findings here default to P0. -5. **Notifications** — all 17 types deliver to the right partner (foreground/background/killed), deep-link - correctly, and leak no private content. +5. **Notifications** — the **full suite**: every type delivers to the right partner (foreground/background/killed), + deep-links correctly, opens the right destination on **both clients**, covers all **game/join-game** flows, handles + stale notifications, and leaks no private content. Scope decisions: **exhaustive** visual pass (all ~50 screens, both modes); **full scope incl. pre-pairing** flows (fresh throwaway account); **couple-shared everywhere** — per-user gates are bugs, fixed by routing through -`core/billing/CouplePremiumChecker.kt`. +`core/billing/CouplePremiumChecker.kt`; **full notification suite** — every type, game + join-game pushes, deep-links, +stale-notification handling, and all in-app paths into joining/resuming/results, verified on **both clients**. **Early known signal:** only chat uses `CouplePremiumChecker`; games/packs/dates/wheel gate on the user's own `EntitlementChecker.isPremium()` — so premium almost certainly does NOT unlock for the free partner there. Pass A confirms + enumerates this; the fix phase applies couple-shared everywhere. ## Execution mode — run to completion (autonomous; do NOT stop) -- **Do not stop to check in or ask for approval.** Run all five passes → the fix phase → re-QA rounds **continuously - until a flawless round** (zero open P0–P2, Passes D + E clean, every game fully played through, navigation/back-stack - verified). Don't hand control back early. +- **Do not stop to check in or ask for approval.** Run all passes (A–J) → the fix phase → re-QA rounds **continuously + until a flawless round** (zero open P0–P2, Passes D + E clean, every game fully played through, all notification + routes verified, navigation/back-stack verified). Don't hand control back early. - **Unblock yourself:** if anything **blocks progress** (a stale/blocking session, a crash, a build break, a missing prerequisite state, a broken nav path that prevents reaching a screen), **fix it immediately and continue** — even though passes are otherwise report-only. Blocking issues are fixed inline so the run can proceed; non-blocking @@ -110,7 +127,10 @@ State lives in **files**, not memory: - **`ClaudeReport.md`** = the issue log (committed). Each issue row is **self-contained in text** (repro + expected + actual) — screenshots are session-only and won't survive a compaction; never rely on a screenshot path alone. - **`ClaudeQACoverage.md`** = the coverage matrix: every screen×mode, feature×premium-state, game×lifecycle, - notification×{foreground,background,killed}, each `todo | pass | fail(→issue id)`. The resume anchor. + notification×{foreground,background,killed}, each `todo | pass | fail→id | not implemented→Future.md | blocked→id`. + The resume anchor. +- **`Future.md`** (`## QA`) = the non-bug improvement/idea backlog; **`ClaudeBrandingReview.md`** = the branding/artwork + review + image-prompt backlog. Both committed alongside the report/coverage. - **Persistent memory** (`memory/`): QA methodology + exact commands; emulator↔account↔coupleId mapping; `scratchpad/set_premium.js` + admin tooling; the couple-shared-premium-everywhere goal + the per-user-gate gap. - **Run-state header** pinned at the TOP of `ClaudeReport.md`, always current: `Round N | Pass X | Chunk Y | @@ -123,7 +143,8 @@ State lives in **files**, not memory: - **Chunking**: run small chunks (Pass C one screen-group; Pass A one feature), checkpoint after each. - **Session-start ritual**: (1) read run-state header + both MD files; (2) `adb devices` shows **both** emulators online; (3) **installed build == current HEAD** (rebuild+reinstall if unsure — never QA a stale APK); (4) continue - at the first `todo` / unverified-fix. + at the first `todo` / unverified-fix; (5) if a prior chunk left an active/stuck game session, recover it via in-app + "End their game" (log if needed), then redo that chunk. ## Batch sizing — sub-batch each pass to ONE context window (Round-1 calibration) A pass is a **category**, not a unit of work. Execute each pass as **sub-batches (chunks)**, where a chunk = the @@ -138,8 +159,12 @@ prevents half-done/lost work and gives cleaner per-chunk verification + revertab | B Games | **one game per chunk** — full two-device playthrough + edges + commit | 7 | | C Visual | **one screen-group per chunk** (both themes, ~6–10 screens, montage-reviewed + nav/back for that group) — never "all screens" at once (heaviest, image-bound) | 6–8 | | D Security | D1 at-rest · D2 rules + D3 negative · D4 keys/recovery · D5–D7 appcheck/secrets/leaks/migration | ~4 | -| E Notifications | **3–5 types per chunk** × {foreground/background/killed} + tap-to-open | ~4 | +| E Notifications | **3–5 types per chunk** × {foreground/background/killed} + tap-to-open; **game/join-game notification chunks** included; both clients (QA→Sam, Sam→QA) | ~5–6 | | F Resilience | **one dimension per chunk** (concurrency · lifecycle/process-death · network · time · account-lifecycle) | ~5 | +| G Account creation | **one creation/abuse dimension per chunk** (happy/validation · duplicate/conflict · fake-account abuse · lifecycle) | ~4 | +| H Branding | **one screen-group per chunk** (consumer brand walk → ready-to-paste art prompts) | ~4 | +| I Performance | **one route-group per chunk** — gfxinfo/jank + read-count instrumentation (build the route smoke checklist) | ~3 | +| J Accessibility | **one a11y setting per chunk** (font scale · TalkBack · contrast · targets · keyboard · reduce-motion) | ~5 | Context-cost tips: prefer **code/admin-read audits** (cheap) before live UI sweeps; **montage** screenshots (dark|light pairs) to review many at once; keep one chunk = one TodoWrite focus. @@ -154,7 +179,7 @@ Context-cost tips: prefer **code/admin-read audits** (cheap) before live UI swee ## Severity scale (label every issue) - **P0 Critical** — crash/ANR, data loss, encryption/security leak, feature fully broken, premium bypass. - **P1 Major** — feature partly broken, premium not unlocking for partner, wrong/missing notification, dead-end nav. -- **P2 Minor** — readability/contrast, clipping/overflow/truncation, theme not adapting, inconsistent styling. +- **P2 Minor** — readability/contrast, clipping/overflow/truncation, theme not adapting, inconsistent styling, wrong/double-back navigation. - **P3 Polish** — spacing/alignment/copy nits. ## QA passes (Round 1 = baseline) @@ -163,11 +188,13 @@ Context-cost tips: prefer **code/admin-read audits** (cheap) before live UI swee Test each gated feature in 3 states: **neither** premium → locked + paywall; **partner-only** premium → BOTH unlock; **self** premium → unlock. Toggle Sam premium, confirm QA (free) unlocks; toggle off. Features: Play-hub games (Desire Sync + any premium-badged), Connection Challenges, Memory Lane; Question Packs; -Spin the Wheel / Category Picker / Wheel History; Date Match / Plan Date / Date Builder; chat media + reactions -(regression — already couple-shared); Subscription/Settings reflects entitlement. +Spin the Wheel / Category Picker / Wheel History (+ any premium wheel categories); Date Match / Plan Date / Date +Builder; chat media + reactions + any premium chat tools (regression — already couple-shared); Subscription/Settings +reflects entitlement. Gated files (for the fix): `ui/play/PlayHubViewModel`, `ui/desiresync/DesireSyncScreen`, `ui/wheel/{CategoryPicker,SpinWheel,WheelHistory}*`, `ui/questions/QuestionPackLibrary*`, `ui/dates/{DateMatch,DateMatches}Screen`, `ui/memorylane/MemoryLaneScreen`, `ui/challenges/ConnectionChallengesScreen`. +Also: **any VM/screen calling `EntitlementChecker.isPremium()` directly** (grep for it) is a candidate gate. ### Pass B — Games lifecycle (MANDATORY: play each game ONE complete time through) Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges, Memory Lane, Spin the Wheel, + Date Match. @@ -188,6 +215,12 @@ 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, 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. +- **GAME JOIN PATHS (mandatory — the second partner must JOIN, not just co-play):** the starter begins from real + in-app nav; the joiner then enters from **every** user-facing entry point — notification tap, Play-hub active state, + Home active-game card, Today prompt, waiting-room/resume screen, in-app foreground banner, game history/replay, and + (after the natural paths) deep-link/crafted intent + cold-start from a push. A game isn't complete unless **both** + partners can **start, join, resume, finish, reopen results, and recover from a stale/ended session** — with no + duplicate sessions, wrong routes, stuck waiting screens, broken back nav, or premium-gate mistakes. - **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 DEPTHS and QUESTION COUNTS — cover the matrix, don't settle for one combo:** play each game across @@ -204,11 +237,14 @@ Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges 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. + spamming the start button, tapping during the reveal animation, switching tabs mid-game, receiving/tapping a + notification mid-game. None should crash, duplicate, or desync. - 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**. - **Media permissions** (CAMERA, RECORD_AUDIO): granted works, denied degrades gracefully. -- **Done = every game has one verified complete playthrough** (a launch-only "opens, no crash" row is `partial`, not `pass`). +- **Done = every game has one verified complete playthrough** (a launch-only "opens, no crash" row is `partial`, not + `pass`). Coverage row format: `game × starter × join-entry × premium-state × depth/count × lifecycle-edge × result`; + only `pass` when start/join/play/finish/reopen/recover are all verified. ### Pass C — Visual pass, light + dark, ALL screens Every route in `core/navigation/AppRoute.kt` (~50), in **both** modes: text contrast/readability (no invisible/ @@ -219,13 +255,22 @@ Settings + all sub-pages (Account, Notifications, Appearance, Privacy, Subscript Account); Paywall; Your Progress/Activity; Recovery. - **Probe:** `ui/theme/Theme.kt` hardcoded brand colors + chat's custom `closerBackgroundBrush` — verify dark mode truly adapts; grep screens for hardcoded `Color(0x...)`. -- **States, not just happy path:** empty / loading / error / not-paired where they exist; many need data setup - (seeding is user-gated) — note unreachable states in coverage rather than skipping silently. -- **Readability at scale:** default font size + spot-check largest system font scale on text-heavy screens. +- **States, not just happy path:** empty / loading / error / not-paired / locked-premium / signed-out / + stale-or-deleted-target / populated-with-many where they exist; many need data setup (seeding is user-gated) — note + unreachable states in coverage rather than skipping silently. +- **Readability at scale:** default font size + spot-check largest system font scale on text-heavy screens. (The full + accessibility sweep — large-font on every primary flow, TalkBack labels, touch targets, keyboard, reduce-motion — is + **Pass J**; per-route performance/jank is **Pass I**.) - **Navigation from every entry point:** reach each screen from **all** the places that link to it and confirm it 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 AND from history AND from `partner_answered`. A screen that works from one entry but breaks/duplicates from another = bug. +- **All routes into a game / join-game state (verify each opens the correct game + session + partner-state + mode + + premium/couple-entitlement + back stack):** Play-hub cards (incl. premium-gated), active-session banners, Home/Today + game prompts, game history, replay/results, waiting screens, notification-opened screens, in-app banners, + "join/resume/continue/view results/end (their) game", deep-link/crafted intent, and bottom-tab return into an active + game. Wrong/duplicate destination, double-back, stale-session join, dead-end, or a route that bypasses the + premium/couple check = 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 @@ -298,41 +343,87 @@ try.** Use throwaway test accounts (sign-out → fresh sign-up; never `pm clear` - **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) -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/ -dead-end) → no duplicates → rate limiter (20/day,100/week) doesn't drop legit ones. -Inventory (type → trigger → destination), all 17: `chat_message`(onMessageWritten→conversation, foreground→chat-head -bubble), `partner_started_game`/`partner_finished_game`(onGameSessionUpdate→game/results), `partner_answered` -(onAnswerWritten→reveal), `daily_question`(assignDailyQuestion)/`daily_question_reminder`/`daily_reminder` -(dailyQuestionReminder→Today), `date_match`(createDateMatch→match), `partner_joined`+`invite_created` -(acceptInviteCallable→pairing/home), `partner_left`(onCoupleLeave)/`partner_deleted_account`(onUserDelete→home/ -relationship settings), `memory_capsule_unlocked`(scheduled→capsule), `challenge_day_ready`(→Connection Challenges), -`outcome_reminder`(scheduledOutcomesReminder), `reengagement`(reengagement/gameRetention), `gentle_reminder` -(sendGentleReminderCallable), `spki`(identify + confirm handled). -- **Tap-to-open:** every notification opens the **specific item** from foreground/background/killed; tapping in-app - doesn't stack/duplicate; logged-out/unpaired tap is graceful. Wrong/dead destination = P1. -- **Scheduled/time-based:** trigger manually (invoke callable/function or seed due condition — user-gated). -- **Foundations:** FCM token registration on sign-in (`TokenRegistrar`) + `onNewToken`; POST_NOTIFICATIONS prompt + - denied path; channels (`di/NotificationModule`); deep-link routing (`MainActivity.deepLinkRouteFromIntent` → - `AppNavigation`); foreground/background split (`core/notifications/AppMessagingService`). -- Build a delivery matrix (type × {foreground,background,killed}) in ClaudeQACoverage.md. Missed delivery or wrong - deep-link = P1; private content in any payload = P0. +### Pass E — Full notification suite, deep-links & join-game navigation (every type, both clients, every app state) +Run the **complete** suite across **both clients** (QA→Sam AND Sam→QA). Each type verified end-to-end: **trigger fires +→ delivered to the right partner (never self/non-member/ex-partner) → correct channel + copy with no private content → +tap opens exactly the right item (loaded, not generic Home/dead-end) → sane back stack → privacy/authz re-checked on +open**. No duplicates; rate limiter (20/day, 100/week) doesn't drop legit ones. +- **Both-client × app-state matrix (per type):** QA→Sam and Sam→QA, each in **foreground / background / killed + (cold-start)**, plus **already on the target screen**, **on a different screen**, **logged out**, **unpaired**, with + a **stale/expired/completed/deleted target**, and **both users opening around the same time**. Not a `pass` unless it + works from both clients in every state that applies. +- **Six assertions per notification:** (1) trigger fires correctly — right event, not early, not twice, sender doesn't + get their own (unless intended), retry/idempotency doesn't duplicate; (2) delivered to the right person — correct + token, old tokens unused after sign-out/account-switch; (3) copy + channel correct — friendly, right channel/ + priority, no raw Firebase error/raw IDs, no private content in text/payload/logs/analytics/crash; (4) tap opens the + exact destination — specific conversation/session/capsule/match/question/settings/pairing, never blank, never a crash + on missing/stale/malformed/unauthorized data, no duplicate/stacked copies, completed→results/replay, expired/deleted→ + graceful fallback; (5) back stack sane — back returns sensibly (Home/prev context), no double-back, no unexpected + exit/loop/blank; (6) deep-link re-checks auth + couple membership + pairing + entitlement + target ownership + + session status + existence — a non-member/logged-out/stale/unpaired open must NOT reach private content and must fail + gracefully. +- **Inventory (type → Cloud-Function trigger → recipient → destination)** — verify each; mark any unimplemented type + `not implemented→Future.md` (don't count as pass): + `chat_message`(onMessageWritten → partner → conversation; foreground→chat-head bubble) · + `partner_started_game`/`partner_finished_game`(onGameSessionUpdate → partner → game/join · results/reveal) · + `join_game`/`game_invite` & `partner_joined_game` (if present → partner/starter → join screen · waiting-room update) · + `partner_answered`(onAnswerWritten → partner → reveal) · + `game_abandoned`/`game_ended` (if present → partner → safe ended state, not a stuck session) · + `daily_question`(assignDailyQuestion)/`daily_question_reminder`/`daily_reminder`(dailyQuestionReminder → Today) · + `date_match`(createDateMatch → match) · `date_plan_update` (if present → date plan/builder/match) · + `partner_joined`+`invite_created`(acceptInviteCallable → pairing/home) · + `partner_left`(onCoupleLeave)/`partner_deleted_account`(onUserDelete → home/relationship settings) · + `memory_capsule_unlocked`(scheduled → capsule) & `memory_capsule_created` (if present → Memory Lane/locked capsule) · + `challenge_day_ready`(→ Connection Challenges) & `challenge_day_completed` (if present → challenge progress) · + `outcome_reminder`(scheduledOutcomesReminder) · `reengagement`(reengagement/gameRetention) · + `gentle_reminder`(sendGentleReminderCallable) · `spki`(key identity/confirm → security/key screen) · + `subscription_entitlement_changed` & `security_recovery` (if present). +- **Game-notification suite (per game):** A starts from Play hub → B gets the start/join push (if supported) → B taps + and lands on the correct join/waiting/active screen → B can join from there → A sees B joined/answered → both finish + → finish push opens the exact results/reveal → re-opening the push after completion opens replay/results (not a dead + active session) → if A ends/quits, B is notified or shown a graceful ended state → a **stale** game push routes to + results/history or a clear expired-session message → simultaneous start/join yields **one** session, neither stuck → + premium gate holds (neither-premium push must NOT bypass paywall; either-premium unlocks for both). +- **Join-game navigation suite:** every entry that leads to joining/resuming a game opens the correct game + session + + partner-state + mode + entitlement + back stack — Play-hub card, active-game banner/card, Home active-game card, + Today game prompt, notification tap, in-app foreground banner, game history/replay, partner waiting screen, results/ + reveal, "End their game"/stuck-session recovery, deep-link/crafted intent, cold-start from push, bottom-tab return + into an active game, any push action buttons, and any "join/resume/continue/view results/play again". No wrong game + type, no accidental stale-session join, no duplicate session on double-tap, back returns correctly. +- **Payload security (P0 on any hit):** inspect raw payload + logs — no plaintext message/answer/capsule/date-plan/ + bucket-list/swipe content, no raw invite code/seed, no recovery phrase, no wrapped/decrypted key material, no + email/name unless intentionally public; payload carries only the minimum routing metadata. Any private content = P0. +- **Malformed / stale intents:** fire crafted deep-links with missing/unknown type, missing/wrong target or couple ID, + wrong game type, expired/completed/deleted target, unauthorized couple/session, malformed params, duplicate/rapid + taps, a push for another user/previous partner, while logged-out/unpaired, while on the target screen, and during a + different active game → never crash/leak, always a graceful fallback + sane back stack. +- **Scheduled/time-based:** trigger manually (invoke callable/function or seed the due condition — user-gated). +- **Foundations:** FCM token registration on sign-in (`TokenRegistrar`) + `onNewToken` + token cleanup on sign-out/ + account-switch; POST_NOTIFICATIONS prompt + denied path; channels (`di/NotificationModule`); deep-link routing + (`MainActivity.deepLinkRouteFromIntent` → `AppNavigation`); foreground/background split + (`core/notifications/AppMessagingService`); no duplicate local+remote notification. +- **Coverage:** record per row `type × trigger × recipient × app-state × destination × back-stack × privacy × + both-client` in ClaudeQACoverage.md; only `pass` when delivery + routing + back-stack + privacy + both-client are all + verified. Missed delivery or wrong deep-link = P1; private content in any payload = P0. ### Pass F — Resilience, concurrency, lifecycle & time (cross-cutting; a 2-user realtime app needs these) -- **Concurrency / realtime races (two partners at once):** both answer the daily question simultaneously; both start - a game / swipe a date / react at the same time; partner acts while you're mid-flow. No lost writes, no stuck state, - no duplicate sessions, reveal still correct. (This is where a couples app breaks.) +- **Concurrency / realtime races (two partners at once):** both answer the daily question simultaneously; both + start/join the same game; both swipe a date / react at once; one quits while the other submits; both tap a + notification at once; partner acts while you're mid-flow. No lost writes, no stuck state, no duplicate sessions, + reveal still correct. (This is where a couples app breaks.) - **Lifecycle / process death:** background mid-flow + return; force-kill the app and relaunch (Android may kill the process) — state/auth/draft restore sanely; deep-link/notification after process death still loads (verified for chat — extend to all). Rotation/config-change doesn't lose Compose state. Low-memory. - **Network resilience:** offline / flaky / airplane mid-action across answers, games, dates (not just chat media) — graceful failure + retry/queue, no crash, no silent data loss, recovery on reconnect. -- **Idempotency / rapid input:** double-tap send/submit, rapid nav, double-start — guarded (no double-send, no crash). +- **Idempotency / rapid input:** double-tap send/submit, rapid nav, double-start, double-join, repeated paywall-unlock + taps — guarded (no double-send, no duplicate session, no crash). - **Time-dependent behavior:** daily-question rollover (6 PM CST assignment), streak day-boundary + repair window, - capsule unlock times, reminder schedules — test across a date change (manipulate device clock / trigger functions). + capsule unlock times, reminder schedules, challenge-day availability, timezone change — test across a date change + (manipulate device clock / trigger functions). - **Account/couple lifecycle:** brand-new (empty) account; unpaired state; pair → unpair → re-pair; partner leaves - mid-session; account deletion cascade; same account on two devices. No orphaned/broken state. + mid-session; account deletion cascade; same account on two devices; stale notifications after unpair/delete are + graceful; invite accepted while already paired is rejected cleanly. No orphaned/broken state. - **Crash reporting:** confirm crashes/ANRs are actually captured (Crashlytics) so field issues surface. ### Pass H — Branding & artwork (every screen: could it carry more of the brand? where would art help?) @@ -366,12 +457,95 @@ written as ready-to-paste ChatGPT image-generation prompts** — the user genera - Branding *defects* (mis-colored, clipped, off-brand, low-contrast art) are bugs → `ClaudeReport.md`. Pure "works but could be warmer / a feature idea" → `Future.md` `## QA`. New art to create → `ClaudeBrandingReview.md`. +### Pass I — Performance & route efficiency (jank, redundant reads, caching) [FUTURE.md P14] +Before store polish, profile **every top route** and **every high-cardinality list** for jank, repeated Firestore +reads, missing cache use, and slow navigation. Drive each route as a user and instrument reads/frames. +- **Frame / jank:** scroll every long list (Messages inbox + conversation, Answer History, Question Packs, Past Games, + Wheel History, Bucket List, Date deck, Activity/Progress) and open every top route while watching + `adb shell dumpsys gfxinfo framestats` (or Perfetto / Studio Profiler) — flag dropped/janky frames, slow first + frame, and `Choreographer: Skipped N frames` / main-thread stalls in logcat. Transitions/animations stay smooth (~60fps). +- **Redundant Firestore / network reads:** count listeners/gets per screen. Switching bottom tabs and returning must + **not** refetch unchanged data; opening a screen twice must not double-read; **snapshot listeners detach on leave** + (no leaked/stacked listeners — a 2-user realtime app accumulates these fast). Watch for N+1 reads on lists. +- **Caching / lazy-load:** static question/category data is cached locally (Room) and not re-fetched each entry; large + lists use lazy paging (`LazyColumn`/paging, not load-all); images cached (Coil); offline reads serve from cache. +- **Latency:** measure cold-start-to-interactive (splash→loader→Home) and tab/route transition latency; flag anything + perceptibly slow (>~300ms). +- **Deliverable:** a reusable **route smoke-test checklist** (every top route × {load time · jank · read count}), + captured as a runnable script so each round re-checks cheaply. +- **Remediation when found:** lazy-load/page large lists; cache local question/category data; dedupe + scope snapshot + listeners; skip redundant fetches on tab switches; add skeleton/loading states (cf. FUTURE.md P8) over blocking spinners. +- Findings: real jank/leak/redundant-read = bug → `ClaudeReport.md` (P2; **P1** if it ANRs or leaks listeners, **P0** if + it drops data); "could be smoother / add skeletons" → `Future.md` `## QA`. + +### Pass J — Accessibility (font scale · contrast · screen reader · targets · keyboard · reduce-motion) [FUTURE.md P15] +Every **primary flow** must be usable with accessibility settings on. Enable each setting and walk the core flows +(auth, onboarding, pairing, Home, a full game, daily question + reveal, Messages, Paywall, Settings) end to end. +This is the deep home for a11y; the Pass C contrast/font spot-checks feed into it. +- **Font scaling:** `adb shell settings put system font_scale 1.3` (then 1.5, 2.0) — every primary flow stays usable: + **no clipped/overlapping text, no cut-off or hidden buttons/actions** (scroll where needed). **Acceptance: all primary + flows usable at increased font scale without clipped buttons or hidden actions.** Restore `font_scale 1.0` after. +- **Screen reader (TalkBack):** every interactive element has a meaningful semantics/`contentDescription` (icon-buttons + especially: back, send, like, close, the brand-mark loader, game option cards); decorative images are silenced + (`clearAndSetSemantics {}` / null desc); reading order is logical; no unlabeled "Button"; custom controls (spin wheel, + date swipe deck, answer cards) are operable + announced; no focus traps. +- **Contrast:** body text + essential icons meet WCAG AA (4.5:1 body / 3:1 large) in **both** themes — measure, don't + eyeball; re-check the known dim spots (game answer text, muted captions, the C-DS-001 area). +- **Touch targets:** interactive targets ≥ **48dp** (icon buttons, chips, nav, close/back, reaction buttons, swipe-deck + actions). Flag anything smaller. +- **Keyboard / external input:** with a hardware keyboard, forms (sign-up, message, capsule, profile) tab in a sane + order, IME/Enter actions work, focus is visible, no traps. +- **Reduce-motion:** with "Remove animations" (`adb shell settings put global animator_duration_scale 0`), the loader, + celebration particles, reveals, splash handoff, and transitions degrade gracefully and **no motion-gated content + becomes unreachable** (the loader/particles already honor this — verify everywhere). Restore to `1` after. +- **Remediation:** add semantics labels, raise touch targets, fix contrast tokens, guard motion behind the reduce-motion flag. +- Findings: missing label / clipped-at-large-font / sub-48dp / failing contrast = bug → `ClaudeReport.md` (**P2**; **P1** + if it blocks a primary flow for assistive-tech users); polish → `Future.md` `## QA`. + ## Reporting → ClaudeReport.md (living QA report) - Header: date, build, devices, round number + run-state header. -- One section per pass (A/B/C/D/E/F), each a table: **ID | Area | Screen/Route | Mode | Severity | Description | Repro +- One section per pass (A–J), each a table: **ID | Area | Screen/Route | Mode | Severity | Description | Repro | Evidence | Suggested fix | Status**. - Summary: counts by severity. Report only during passes — no fixes recorded until the fix phase. +### Report hygiene — keep it CLEAN, lean, and never dangling (the report is a *current-state* doc, not an archive) +The report's job is to show, at a glance, **what's wrong right now** — not to accumulate a history of everything ever +fixed. Stale fixed rows and stacked old run-states make it unreadable and hide the real signal. So: +- **A Fixed row survives exactly ONE confirmation round, then it's removed.** When you fix an issue, mark its row + `Fixed` (with the commit) and keep it through the **next** re-QA round. Once that round re-verifies it, **delete the + row** — the full root-cause/fix detail already lives in the **commit message** (the row cites the hash), so nothing is + lost. Don't carry confirmed-fixed issues across multiple rounds. +- **One run-state header, always.** Keep only the **current** `Round N | Pass X | Chunk Y | NEXT ACTION` block pinned + at the top. Don't stack prior rounds' headers — collapse finished rounds into at most a **single one-line history** + entry each (e.g. `R6: branding regression — 0 new`), or drop them entirely once their fixes are confirmed-and-pruned. +- **Open issues first; resolved issues compact.** Order every pass section **open (P0→P3) on top**; keep a short + `Resolved & confirmed (archived — detail in git)` line listing only the **IDs** of older fixed-and-verified issues + (not their tables). The big per-issue tables exist only for **currently-open** and **fixed-this-round-pending-confirm** + issues. +- **Severity board reflects NOW.** One board, current counts; `Open` is the number that actually matters. When `Open` + hits 0 at every level, the report should be **short** — current run-state, a 0/0 board, the archived-ID line, and the + operational constants (devices/accounts, standing-auth, playbook pointers). If it's long while everything is fixed, + it needs pruning. + +### Coverage-matrix hygiene (`ClaudeQACoverage.md` — a *current-status* matrix, not a per-round changelog) +- **Flip, don't stack.** When a fix is confirmed, change that row's `fail→id` to `pass` and move the ID to an archived + line — never leave a confirmed-fixed `fail→id` dangling, and never keep a contradicting "still owed" note next to a + completed row. +- **One status per cell, current.** Each screen/feature/game/notification shows its **latest** status only; collapse + prior rounds' narration into a single one-line **round history**. Keep an at-a-glance pass-status table at the top. +- **Keep the resume signal sharp.** What a returning session needs is *what's left* — surface `todo`/`deferred`/ + `blocked` items plainly; don't bury them under superseded prose. + +### Extremely-easy-to-read mandate (applies to ClaudeReport.md, ClaudeQACoverage.md, and Future.md) +Optimize every QA doc for a reader who has **5 seconds** to find the current state: +- **Lead with the answer.** Top of the file = current round + the one-line verdict (e.g. "0 open P0–P3; security clean") + before any detail. +- **Tables over prose** for issues; **short rows**. Put long root-cause analysis in the **commit**, not the row — the + row gets a one-sentence description + repro, then the commit hash. +- **No walls of text.** Break run-state into scannable lines; bold the few words that matter; no multi-paragraph + headers. If a paragraph is longer than ~3 lines, it's probably commit material, not report material. +- **Consistent shape every round** so a returning reader (or a post-compaction resume) finds things in the same place. + ## Fix phase (only AFTER all passes of the round complete) - Work strictly by severity: **all P0 → P1 → P2 → P3**. - **One issue at a time**: implement → `./gradlew :app:assembleDebug` → install both → verify THAT fix live (correct @@ -384,10 +558,16 @@ written as ready-to-paste ChatGPT image-generation prompts** — the user genera - Gated actions (entitlement toggles, deploys) are **user-authorized per occurrence**. - **New issues found while fixing** are logged (new ID), not silently fixed beyond scope — next re-QA round catches them. -**Definition of done:** a **pass** is done when every coverage row is `pass`/`fail→id`; a **round** is done when all -five passes are done; **flawless** = one full round with **zero open P0–P2 and Passes D + E fully clean**. Then stop -(P3s optional). Don't re-open a clean pass within the same round. +**Definition of done:** a **pass** is done when every coverage row is `pass`/`fail→id`/`not implemented→Future.md`/ +`blocked→id`; a **round** is done when all passes (A–J) are done; **flawless** = one full round with **zero open P0–P2 +and Passes D + E fully clean** (no open P0/P1 in I/J), **every game fully played through, every notification type +verified or explicitly `not implemented→Future.md`, all join-game navigation paths and all back-stack checks +verified**. Then stop (P3s optional). Don't re-open a clean pass within the same round. ## Re-QA loop (until flawless) -After the fix phase, re-run Pass A/B/C/D/E/F (regression + confirm fixes). Repeat **fix → re-QA** rounds until a full +After the fix phase, re-run Pass A–J (regression + confirm fixes). Repeat **fix → re-QA** rounds until a full round yields zero P0–P2 and Passes D+E fully clean. +- **Prune on confirmation (Report hygiene):** the moment a re-QA round re-verifies a `Fixed` issue, **delete its row** + from `ClaudeReport.md` (move its ID to the compact `Resolved & confirmed (archived — detail in git)` line) and + collapse that finished round's run-state header. A fixed issue lives in the report for **one** confirmation round + only — never let confirmed-fixed rows or old run-states accumulate. See **Report hygiene** under Reporting. diff --git a/ClaudeReport.md b/ClaudeReport.md index 67b2fed5..21cd9d57 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -1,202 +1,48 @@ # Claude QA Report — Full-App QA (living report) -> **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. **F-RACE-001 (P1) — FIXED + VERIFIED LIVE** (atomic transactional session create). **SEVERITY BOARD: 0 open at ALL levels (P0–P3).** 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).** +> **Verdict (2026-06-25): 0 open issues at every severity (P0–P3). Security cornerstone fully clean. App is at the "flawless" bar.** > -> **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.) -> **FIX IMPLEMENTED + VERIFIED LIVE (2026-06-25):** `QuestionSessionRepository.startSessionAtomically` runs a Firestore -> transaction on a per-couple pointer doc `couples/{cid}/sessions/_active` — reads the pointer (+ the pointed session), -> and either returns `AlreadyActive` (caller joins) or atomically `set`s the new session + re-points the lock. Concurrent -> starts contend on the one pointer doc, so the loser's transaction retries, re-reads the now-set pointer, and joins -> instead of creating a duplicate. `GameSessionManager.startGameWithCouple` now calls it (all 7 games funnel through it); -> rule added (`sessions/_active` member-writable) + deployed. **Verified:** atomic create → 1 session + pointer written; -> sequential 2nd start → joins (1 session); **literal parallel-tap race → 1 session (was 2)**; 0 FATAL; pointer -> self-heals on completion (checks pointed session status). Files: `domain/repository/QuestionSessionRepository.kt`, -> `data/repository/QuestionSessionRepositoryImpl.kt`, `domain/usecase/GameSessionManager.kt`, `firestore.rules`. +> This report shows **current state only**. Fixed issues live here for **one** confirmation round, then they're pruned +> to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`. -> **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.** +## Run-state (current) +`Round 7 (multi-angle deep dive) — COMPLETE | 0 open P0–P3 | NEXT ACTION: Round 8 re-QA — confirm F-RACE-001, then prune it; run Passes I/J live.` +- **Build:** client HEAD `23dd6a7` (includes the F-RACE-001 fix, verified live), Cloud Functions deployed. +- **Devices / accounts:** emulator-5554 = QA (`Y05AKO2IlTPMa0JQW1BiNIM0uzK2`) · emulator-5556 = Sam (`imDjjO…`) · paired, coupleId `Xal3Kw3gjSdn0niERYKJ`, both free (baseline restored). +- **Docs:** Playbook `ClaudeQAPlan.md` · Coverage `ClaudeQACoverage.md` · Ideas `Future.md` `## QA` · Branding `ClaudeBrandingReview.md`. -> **RUN-STATE: Round 5 (functions deploy + expanded re-QA) COMPLETE — 2026-06-25. Client HEAD `765916a` on both emulators; Cloud Functions DEPLOYED (`firebase deploy --only functions` → "Deploy complete", all 30+ fns updated). Fixed + verified LIVE: E-OBS (all 12 FCM senders now set `android.notification.channelId` → backgrounded chat push lands on `partner_activity`, NOT `fcm_fallback`), E-003 results-ready (server sends `game_session_id`; finished-game deep-link → per-session "This or That Results" screen, not hub/setup). Expanded coverage per user request: VARIED GAMEPLAY (Standard/Deep + 0-match "Total opposites" result path), exhaustive NAV FUZZING (rapid triple-tap opens setup once via launchSingleTop; back-stack clean; no dead-ends/double-back), and NEW PASS G account-creation/fake-account — ALL SECURE: sign-up+validation (weak-pw → friendly error), fresh-account isolation (zero couple data), duplicate-email → `auth/email-already-exists`, invite single-use+24h-expiry + bogus code → "Invite not found", recovery phrase client-generated. **SEVERITY BOARD: 0 open at ALL levels (P0–P3).** Baseline restored: couple intact, both free, 0 active sessions, throwaway test account deleted, Sam re-paired.** -> _Round 4 (carried): E-003 game-push + B-004 WaitingForPartner "Join the game" + A-OBS paywall copy all FIXED + verified live._ -> _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 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). -> Round 1 complete (all 5 passes run report-only; P0–P2 found were fixed in-line). Fixes: A-001 (e8892a9), E-001 (ce12abb). Open P3: A-003, B-001, E-002. -> **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.** -> Playbook: `ClaudeQAPlan.md`. Coverage matrix: `ClaudeQACoverage.md`. Report-only during passes (no fixes until the fix phase). -> Devices: emulator-5554 (QA=`Y05AKO`) + emulator-5556 (Sam=`imDjjO`), paired (coupleId `Xal3Kw3gjSdn0niERYKJ`). Build == HEAD `64f0a7e`. - -_(Prior games/notifications QA from 2026-06-24 was completed + verified; superseded by this full-app effort.)_ - ---- - -## Severity summary (current — after Round 4 fix phase + re-QA) -| Severity | Open | Fixed (verified live) | +## Severity board +| Severity | Open | Fixed (pending 1 confirm) | |---|---|---| | P0 | 0 | 0 | -| P1 | 0 | 4 | -| P2 | **0** | **6** | -| P3 | **0** | **6** | +| P1 | 0 | 1 (F-RACE-001) | +| P2 | 0 | 0 | +| P3 | 0 | 0 | -**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). +## Open issues +**None.** -**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. - -**R3/R4 issue dispositions:** -- **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. - -**Round 1: all P0–P2 found were FIXED** (A-001 premium P1, E-001 notif-routing P2). -**Round 2 (Pass B play-as-user restart) new/changed:** **B-001** escalated **P3→P1** (a finished game never closes its -session via any normal path — proven: both tapped "Back to Play", session stayed `active` 12s later — so every game -blocks the next, recoverable only via the destructive "End their game"; breaks the core loop); **B-002 (P2, new)** Home -"Play now / your partner is waiting to play" lands on the generic Play hub instead of resuming/indicating the waiting -game (confirmed with a live session too). Open P3: A-003 (badge), E-002 (informational notif routing), F-OBS -(load-fail handling). Deferred for "flawless": exhaustive deep/stateful screens (Pass C), full live notif matrix + D3. - ---- - -## Round 3 re-QA log (2026-06-25, build `ce7fc2e`) — fix regression + deferred coverage -**Fixes re-verified LIVE this round:** -- **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. -- **B-003 ✅** — reveal counts fully coherent: "3 shared desires / 2 answers stayed private" + tiles "You: Private / partner: Private" + caption "3 shared, 2 kept private". No contradicting "5 private". -- **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._ - -**Pass D (R3) — re-audit clean, no P0/P1:** -- **D2 rules (deployed) re-audited ✓** — no catch-all `match /{document=**}`, no blanket `if true`; **sessions update (B-001 fix present)**: only `['status','completedAt','completedByUsers']`, `startedByUserId` immutable, status monotonic active→completed; **hasPremium server-only** (client write+diff blocked L172/174); **entitlements** owner+partner read (couple-shared) / write server-only; **capsules (D-001)** member-read + ciphertext-enforced (isCiphertext title+content) + authorId-bound + key allowlist + coupleEncryptionEnabled; **challenges (D-001)** member-read + progress-only writes. -- **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._ - -## Pass A — Couple-shared premium ✅ pass complete -**Target:** if either partner is premium, all premium features unlock for both. -**Result:** only chat is couple-shared. Every other feature gate is per-user → a free user whose partner paid stays locked. - -| ID | Area | Screen/Route | Severity | Description | Repro | Status | -|---|---|---|---|---|---|---| -| A-001 | Premium gating | PlayHubViewModel, DesireSyncScreen, MemoryLaneScreen, ConnectionChallengesScreen, QuestionPackLibraryViewModel, wheel CategoryPicker/SpinWheel/WheelHistory(VMs) | **P1** | These gated on per-user `EntitlementChecker.isPremium()` instead of couple-shared. A free partner of a premium user stayed locked. | Set Sam premium, QA free → QA Play hub still showed 🔒 on Desire Sync + Memory Lane. | **FIXED** `e8892a9` — routed all gates through `CouplePremiumChecker` (now exposes isPremium/hasPremium resolving partner internally). Verified: Sam premium → QA enters Desire Sync; both free → QA → paywall. | -| 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). | -| A-002 | Premium (control) | ConversationViewModel (chat) | — | **Working correctly** (couple-shared) — kept as the reference pattern for the A-001 fix. | Verified prior round: partner-premium unlocks chat media/reactions for the free partner. | OK | - -**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. - -## Pass B — Games lifecycle (launch/crash sweep done; full two-device lifecycle partial) -| ID | Area | Screen/Route | Severity | Description | Repro | Status | -|---|---|---|---|---|---|---| -| 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). - -## Pass C — Visual (light + dark) (main screens verified; deep/stateful screens pending) -**Method:** 5554=Dark, 5556=Light; readable dark|light pair montages + a code scan for non-adapting colors. -**Verified clean (both themes, readable, no clipping):** Home, Today, Play, Messages inbox, Settings. `closerBackgroundBrush()` is theme-aware (adapts). No FATAL on these. -| ID | Area | Screen | Severity | Description | Status | +## Fixed this round — pending one confirmation round (then prune) +| ID | Severity | Area | Description | Fix | Status | |---|---|---|---|---|---| -| C-OBS | Theming | ~20 screens (AnswerRevealScreen 15, WheelSessionScreen 14, DateMatchScreen 10, PaywallScreen 9, BucketListScreen 9, SettingsScreen 7, HomeScreen 5, …) | observe | Use hardcoded `Color(0x…)` literals (195 total) that don't adapt to theme — a dark-mode contrast risk to verify per-screen. Main screens checked look fine; deep/stateful screens (reveal, wheel session, dates, bucket list) still need visual verification in both themes. | Open (verify in continuation) | -| 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. | +| F-RACE-001 | P1 | Games / concurrency | Both partners starting the *same* game within ~1s created **2 divergent active sessions** (different question sets) → no shared reveal; core loop silently defeated. Non-transactional check-then-create in `GameSessionManager.startGameWithCouple`. | Atomic Firestore transaction on a per-couple pointer `couples/{cid}/sessions/_active` (`startSessionAtomically`): reads pointer → `AlreadyActive`→join, else atomically sets session + re-points lock. All 7 games funnel through it. Member-writable `sessions/_active` rule deployed. Files: `QuestionSessionRepository[Impl].kt`, `GameSessionManager.kt`, `firestore.rules`. | **Fixed + verified live (`23dd6a7`):** parallel-tap race → **1 session** (was 2); sequential 2nd start → joins; pointer self-heals on completion; 0 FATAL. **→ Round 8: re-confirm, then delete this row.** | -_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._ +## Resolved & confirmed (archived — full detail in git history) +A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · C-NAV-001 · D-001 · E-001 · E-002 · E-003 · E-OBS · F-OBS — all fixed and re-verified across Rounds 2–6 (commits cited in history). Pruned from the live report per the one-confirmation-round rule. (C-OBS / `outcomes` list / SubscriptionScreen per-user gate = investigated, **not bugs**.) -| 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() { 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. | +## Security cornerstone — clean (Pass D, deep dive, Round 7) +- **D1 at-rest:** chat text + `lastMessagePreview` + all 4 game-answer collections (ToT / How Well / Desire Sync / Wheel, both users) + Memory Lane capsules + date-swipe actions = `enc:v1:`. No plaintext content; only metadata in clear. +- **D2/D3 access:** non-member denied **all** reads/writes (raw Firestore REST → 403); real premium write `users/{uid}/entitlements/premium` denied (server-only → **no self-grant**); cross-couple denied. +- **D4 keys:** 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). +- **Robustness:** malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal) → 0 crash; killed-state cold-start chat deep-link → conversation loads. -**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`. +## Round history (one line each) +- **R7** — multi-angle security/concurrency deep dive → cornerstone fully clean; F-RACE-001 found + fixed + verified. 0 new open. +- **R6** — branding drop + Future.md backlog regression (white-keyhole icons/loader/splash, inclusive gender, copy, rate-limit split, results-push suppression, paywall retry/offline) → 0 new open. +- **R5** — Cloud Functions deployed (E-OBS channel fix, E-003 results routing) + new Pass G (account creation / fake-account abuse) clean → 0 open. +- **R1–R4** — baseline Passes A–F report-only; every P0–P2 found was fixed + verified (see archived IDs). -**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). - -## Pass D — Security & Encryption ✅ clean (no P0/P1 found) -- **D1 at-rest:** all private content is ciphertext — message `text` + `lastMessagePreview` + thread messages = `enc:v1:`; daily answers `encryptedPayload` = `sealed:v1:`; **Memory Lane capsules `title` + `content` = `enc:v1:`** (live-verified R2-B2: admin read the just-created capsule → both fields ciphertext, `status:sealed`, `unlockAt` set, only metadata plaintext). Metadata (dates, types, commitmentHash, ids) plaintext as expected. Chat media bytes = Tink ciphertext (verified prior round + unchanged code path). **No plaintext content leak.** -- **D2 rules:** no catch-all `match /{document=**}`, no blanket `if true`; **`hasPremium` server-only** (client create/update blocked, rules L172/174); entitlements `write:false`; conversations/messages/typing/reactions + entitlement partner-read scoped to members. -- **D4 key exchange:** pairing uses a **wrapped couple key** (`wrappedCoupleKey` + `kdfSalt`/`kdfParams` + `encryptedRecoveryPhrase`); invite code is the KDF seed, never stored raw; strict E2EE (invites without a wrapped key rejected) — confirmed in `acceptInviteCallable`. -- **D5 App Check/secrets:** App Check enforced (`SecurityModule`, `PlayIntegrityChecker`, `FirebaseInitializer`); both service-account JSONs gitignored **and untracked**; `allowBackup=false`. -- **D6 leak vectors:** analytics events carry only metadata (no message/answer content); `allowBackup=false`. - -_Follow-ups (not blockers): live **non-member negative test** (D3) needs a fresh 3rd account (rule logic verified member-scoped); a fresh Storage-bytes spot-check of chat media._ - -| ID | Area | Severity | Description | Status | -|---|---|---|---|---| -| D-001 | Rules — missing subcollection rules | **P1** | `couples/{id}/capsules` and `couples/{id}/challenges` had **no `match` block** → default-deny → **Memory Lane hung on its loading heart** and **Connection Challenges** couldn't load (live `PERMISSION_DENIED` confirmed). Two premium features broken. | Sam premium, QA opens Memory Lane → stuck loading heart; logcat `Listen for Query(.../capsules) failed: PERMISSION_DENIED`. | **FIXED + DEPLOYED** — added member-read + ciphertext-enforcing `capsules` rule (title/content/promptUsed must be `enc:v1:`) and a `challenges` rule (catalog-referenced, progress-only). Re-verified live: Memory Lane shows empty state, Connection Challenges shows the series list, **0 permission errors**. | -| 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) | - -## Pass E — Notifications -- **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. -- **Foundations** (prior round, code present): FCM token registration on sign-in, POST_NOTIFICATIONS, channels. - -| ID | Area | Severity | Description | Status | -|---|---|---|---|---| -| E-001 | Notification routing | **P2** | Type-string mismatch: functions send `daily_question` + `challenge_day_ready`, but client mapped only `daily_question_reminder` + `challenge_waiting` → tapping those did NOT deep-link to Today / Connection Challenges. | **FIXED** `` — added `daily_question`/`challenge_day_ready` to `fromRemoteType` (build green; live tap-verify deferred). | -| 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). | - -_Full live delivery matrix (17 types × foreground/background/killed) deferred — key types verified prior round; routing now code-correct._ +## Operational constants +- **Execution mode:** autonomous run-to-completion — don't stop; fix blockers inline; cycle fix→re-QA until flawless. Don't hand back when context fills — re-read this run-state + coverage after any compaction. 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 without pausing. Only the macOS requirement for iOS (Parts 2/3) is a hard stop. +- **Hardening backlog → Future.md:** App Check not enforced on Firestore; `users/{uid}` update rule allows arbitrary non-`hasPremium` fields (tighten to a field allowlist).