38 KiB
Claude QA Report — Full-App QA (living report)
RUN-STATE: Round 3 (full re-QA A–F) COMPLETE — 2026-06-25, build
ce7fc2e. RESULT: all 12 prior fixes RE-VERIFIED HOLDING LIVE; deeper play-as-user testing surfaced 5 NEW issues — 2×P2 (B-004 intermittent guesser-stuck on WaitingForPartner; E-003 game notifications deep-link to Play hub not the game), 3×P3 (A-OBS paywall raw error copy [env]; E-OBS bg pushes use fallback channel; C-OBS RESOLVED = debug menu IS BuildConfig.DEBUG-gated, not a bug). NO P0/P1, NO security/encryption findings (Pass D clean, E2EE holds at-rest + UI). NOT yet "flawless" (2 open P2 + Pass E has E-003) → NEXT: a FIX PHASE for E-003 (client routeFor + likely server payload gameType, mirrors B-002) + a deterministic B-004 repro before fixing, then Round 4 re-QA. Baseline restored: both free, 0 active sessions. E-003/B-004 are report-only (logged, not fixed mid-round per plan). Round 2 result (carried): FIX PHASE COMPLETE — P1×2 (B-001 session rules, C-NAV-001 back-stack), P2×4 (A-001, B-002, C-CC-001, C-DS-001), P3×4 (A-003, B-003, E-002, F-OBS) all FIXED; D-001 (P1 rules) + E-001 (P2 routing) fixed earlier. All verified LIVE except E-002/F-OBS (code+build; live-trigger deferred). Rules deployed. 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): mayfirebase 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 (coupleIdXal3Kw3gjSdn0niERYKJ). Build == HEAD64f0a7e.
(Prior games/notifications QA from 2026-06-24 was completed + verified; superseded by this full-app effort.)
Severity summary (current — after Round 3 re-QA)
| Severity | Open (new in R3) | Fixed (verified holding) |
|---|---|---|
| P0 | 0 | 0 |
| P1 | 0 | 4 |
| P2 | 2 (B-004, E-003) | 4 |
| P3 | 2 (A-OBS, E-OBS) | 4 |
Round 3 result: all 12 prior fixes RE-VERIFIED HOLDING LIVE (C-NAV-001, C-CC-001, A-001, A-003, B-001 across 4 game types, B-002, B-003, C-DS-001, D-001 + E-001/E-002 code + F-OBS indirect). No P0/P1, no security/encryption findings (Pass D clean; E2EE holds at-rest + in UI). Deeper play-as-user testing found 5 new issues: 2×P2 (B-004, E-003), 3×P3 (A-OBS, E-OBS, and C-OBS which RESOLVED to not-a-bug — debug menu is BuildConfig.DEBUG-gated). Not yet "flawless" (def: 0 open P0–P2 + Passes D/E clean) — 2 open P2 remain → fix phase + Round 4 re-QA needed.
New R3 issues (report-only — logged, to fix next phase):
- B-004 (P2, intermittent): How Well guesser can get stuck on the generic WaitingForPartner screen during a rapid game-to-game transition (screen only exits on session end; needs deterministic repro before fix; escalate to P1 if deterministic).
- E-003 (P2): game notifications (
partner_started_game/game_results_ready/partner_completed_part) deep-link to the generic Play hub, not the specific game/results, despite "Tap to join!" (fix: extend HomeViewModel.gameRouteFor resolver to notification routing; likely needs server payload gameType). - A-OBS (P3): paywall plan-load shows raw "credentials issue" error (emulator has no RevenueCat sandbox; copy should be friendlier in prod).
- E-OBS (P3): backgrounded pushes use
fcm_fallback_notification_channel, bypassing code-defined channels (CHANNEL_GAMES/chat) — server sends "notification" not data-only messages. - C-OBS (RESOLVED, not a bug): Settings "Art preview/Paired home (debug)" entries ARE
BuildConfig.DEBUG-gated (SettingsScreen.kt:469) — won't ship in release.
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 (
Backdesc 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 stalewaitingGameRoute/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 blanketif true; sessions update (B-001 fix present): only['status','completedAt','completedByUsers'],startedByUserIdimmutable, 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: 0enc: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 (
isCouplesMembergate 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_PARTallrouteFor → 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 onfcm_fallback_notification_channeleven though the code assignsCHANNEL_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 setandroid.notification.channel_idin 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 |
|---|---|---|---|---|---|
| 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. |
| 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. |
Deep/stateful screens (answer reveal, wheel session/complete, date match/builder/matches, bucket list, memory capsule, history, paywall, auth/onboarding/pairing) need their states set up — pending next chunk.
| C-NAV-001 | Nav / back-stack — onboarding+auth not popped after login | MainActivity AppNavigation (start/auth/onboarding graph) | P1 | The auth + onboarding destinations are never popped from the nav back stack after login, so pressing system Back from Home walks BACKWARD into onboarding → the welcome/login screen instead of exiting the app. Confirmed with a CLEAN reproduction (no scripted pollution): cold start → land on Home (authenticated, "Connected with Sam") → press system Back once → lands on the "Answer honestly" onboarding carousel (still inside closer.app/app.closer.MainActivity, so it's in-app nav, not a separate task). Tapping the carousel's Skip then reaches "Closer — Create account / I already have an account" (the pre-auth welcome) — i.e., a logged-in user pressing Back appears to be logged out. Not data loss (cold start returns to Home; Firebase auth persists), but it's a core, every-user nav defect and very alarming UX. Fix: on successful auth/onboarding completion, navigate to Home with popUpTo(<auth/onboarding graph or start route>) { inclusive = true } (and launchSingleTop) so Home is the back-stack root and Back from Home exits the app. | Cold start (logged in) → Home → press system Back → onboarding carousel appears instead of the app closing. | FIXED — in AppNavigation.navigateRoute, navigating to HOME from an entry route (ONBOARDING/CREATE_PROFILE/PAIR_PROMPT/LOGIN/SIGN_UP/FORGOT_PASSWORD) now does navigate(HOME){ popUpTo(0){inclusive=true}; launchSingleTop }, wiping the entire pre-app flow so HOME is the back-stack root. Normal tab-switch semantics (selectTab) untouched. Re-verified LIVE: cold start (logged in) → Home → system Back → focused activity is the launcher (NexusLauncherActivity), app exits cleanly — onboarding no longer resurfaces. |
Pass B requirement (updated): each game must be played one complete time through on both devices (start → every step → finish/reveal/results), not just launched. Round 1 did launch-only → full playthroughs owed in Round 2 for all 7 (premium games need a premium toggle). A launch-only result = partial, not pass.
Pass C requirement (added): navigation from every entry point (each screen reached from all its links — e.g. conversation from inbox/Discuss/notification; game from Play/notification; paywall from each gate) + back-stack / "double-back" (system back AND in-app back return to the right place from each entry; no dead-ends, no exit-app surprise, no screen needing two backs/duplicate stack entries; deep-link/notification entries land with a sane back stack). Owed in Round 2. Wrong/double back or dead-end = P2 (P1 if it traps the user).
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 answersencryptedPayload=sealed:v1:; Memory Lane capsulestitle+content=enc:v1:(live-verified R2-B2: admin read the just-created capsule → both fields ciphertext,status:sealed,unlockAtset, 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 blanketif true;hasPremiumserver-only (client create/update blocked, rules L172/174); entitlementswrite: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 inacceptInviteCallable. - 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. |
| 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). |
| (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. |
— |
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 <pending-commit> — 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.