From 452aaf787a76cbcddcd53f372f92a3f633e1a7a9 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 24 Jun 2026 19:59:04 -0500 Subject: [PATCH] =?UTF-8?q?qa(round1):=20Pass=20A=20complete=20=E2=80=94?= =?UTF-8?q?=20couple-shared=20premium=20gap=20(A-001,=20P1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only chat uses CouplePremiumChecker; all other gates are per-user → a free partner of a premium user stays locked. Confirmed live (Sam premium, QA still locked on Desire Sync + Memory Lane). Report-only. Co-Authored-By: Claude Opus 4.8 --- ClaudeQACoverage.md | 38 ++++++++++++++++++++ ClaudeReport.md | 87 +++++++++++++++------------------------------ 2 files changed, 67 insertions(+), 58 deletions(-) create mode 100644 ClaudeQACoverage.md diff --git a/ClaudeQACoverage.md b/ClaudeQACoverage.md new file mode 100644 index 00000000..9e218065 --- /dev/null +++ b/ClaudeQACoverage.md @@ -0,0 +1,38 @@ +# Claude QA Coverage Matrix + +> Resume anchor. Status: `todo | pass | fail(→id) | n/a`. See `ClaudeReport.md` run-state header for current position. +> Round 1 in progress. + +## Pass A — Couple-shared premium (states: 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) | + +Pass A: **complete** (1 systemic P1). + +## Pass B — Games lifecycle (start / play / finish + results) +| Game | starts | plays | finishes/results | no crash | Status | +|---|---|---|---|---|---| +| This or That | todo | todo | todo | todo | todo | +| How Well Do You Know Me | todo | todo | todo | todo | todo | +| Desire Sync | todo | todo | todo | todo | todo | +| Connection Challenges | todo | todo | todo | todo | todo | +| Memory Lane | todo | todo | todo | todo | todo | +| Spin the Wheel | todo | todo | todo | todo | todo | +| Date Match | todo | todo | todo | todo | todo | + +## Pass C — Visual (light + dark), all ~50 routes +_todo — enumerate from AppRoute.kt; 5554=Dark, 5556=Light._ + +## Pass D — Security & Encryption (D1–D6) +_todo_ + +## Pass E — Notifications (17 types × {foreground, background, killed} + tap-to-open) +_todo_ diff --git a/ClaudeReport.md b/ClaudeReport.md index d219f3f6..38732bb9 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -1,71 +1,42 @@ -# Claude QA Report — Games & Notifications +# Claude QA Report — Full-App QA (living report) -**Updated:** 2026-06-24 -**Devices:** emulator-5554 (Device A = QATester) + emulator-5556 (Device B = Sam), paired for real (coupleId `xNd1H2UGUDNqvyrDGgfu`). -**Focus this pass:** every game works end-to-end **and notifications fire correctly on game start + finish.** +> **RUN-STATE: Round 1 | Pass B (games lifecycle) | NEXT ACTION: drive each game start→finish on both devices, log issues.** (Pass A done: A-001 P1.) +> 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`. -**Severity:** 🔴 critical · 🟠 high · 🟡 medium · 🟢 low -**Status:** 🔎 found · 🛠 fixing · ✅ fixed & builds · ✅✅ verified live · ⚠️ needs deploy +_(Prior games/notifications QA from 2026-06-24 was completed + verified; superseded by this full-app effort.)_ --- -## OPEN — current error log - -### N1. 🔴 FCM token was NEVER registered → no push notifications worked at all — ✅✅ FIXED & VERIFIED -`TokenRegistrar.register()` (which fetches `messaging.token` and stores it) was **never called anywhere**. The only other path, `AppMessagingService.onNewToken`, bails with `currentUserId ?: return` — and FCM generates the token at **install, before sign-in**, so `onNewToken` ran with no uid and stored nothing; it never fires again afterwards. Result: **`users/{uid}.fcmToken` was empty for every account**, so no game/message/daily push could ever be delivered. -**Fix:** `MainActivity` now observes `authState` and calls `tokenRegistrar.register()` whenever a user is authenticated. ([MainActivity.kt](app/src/main/java/app/closer/MainActivity.kt)) -**Verified live:** after the fix both A and B have a stored `fcmToken`; a direct push to A rendered the heads-up "Your partner started a game — Tap to join them." - -### N2. 🔴 POST_NOTIFICATIONS permission was never requested → notifications can't display on Android 13+ — ✅ FIXED -`NotificationPermissionHelper` existed but had **no caller**. On API 33+ notifications are silently dropped without the runtime grant. -**Fix:** `MainActivity` requests `POST_NOTIFICATIONS` on launch via an Activity Result launcher. ([MainActivity.kt](app/src/main/java/app/closer/MainActivity.kt)) - -### N3. 🟠 Game-START notification named the WRONG person — ✅ FIXED ⚠️ needs functions deploy -`onGameSessionUpdate` passed the **recipient's** name into the body, so the partner saw *" has started a game"* (live: B/Sam received "Sam has started a game"). It should name the **starter**. -**Fix:** use `startedByUserId` → starter's name + avatar for both title and body. ([onGameSessionUpdate.ts](functions/src/games/onGameSessionUpdate.ts)) - -### N4. 🟠 Game-FINISH notification only reached one partner — ✅ FIXED ⚠️ needs functions deploy -Completion branch gated the "notify both" path on `currentData.partnerCompletedAt`, **a field the client never writes** (the client tracks `completedByUsers`). So when both finished, the partner who'd been waiting often got nothing (or a "tap to continue playing" that no longer applied). -**Fix:** on `active → completed` (both partners done = reveal ready) notify **both** partners, each naming the other (" finished — tap to see your results!"). ([onGameSessionUpdate.ts](functions/src/games/onGameSessionUpdate.ts)) - -### N5. 🟡 Finish copy was wrong (title + client mapping) — ✅✅ FIXED & VERIFIED -- FCM title was hard-coded `" is playing"` even for finish events (shown verbatim when the app is backgrounded). Now type-aware → `" finished the game"`. ([onGameSessionUpdate.ts](functions/src/games/onGameSessionUpdate.ts)) -- Client mapped `partner_finished_game` → `PARTNER_COMPLETED_PART` ("finished their part, open yours when ready") — wrong once **both** are done. Added a dedicated `GAME_RESULTS_READY` type ("Your game results are ready! You both finished — tap to see how you compare."). ([PartnerNotificationManager.kt](app/src/main/java/app/closer/notifications/PartnerNotificationManager.kt)) - -**N5 verified live:** pushed both types to A — start rendered "Your partner started a game — Tap to join them."; finish rendered "Your game results are ready! You both finished…". The client renders the correct copy for each type. - -### N6. 🟠 Deployed functions are STALE for Date Match — ⚠️ needs functions deploy -Source exports `notifyOnDateMatch`, but the **deployed** function is still the old `createDateMatchOnMutualLove`. `onMessageWritten` is current; the rest predate recent edits (incl. N3–N5). -**Action:** `firebase deploy --only functions` (allow it to delete `createDateMatchOnMutualLove`). +## Severity summary (Round 1) +| Severity | Open | Fixed | +|---|---|---| +| P0 | 0 | 0 | +| P1 | 1 | 0 | +| P2 | 0 | 0 | +| P3 | 0 | 0 | --- -## Per-game status +## 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. -| Game | Functional | Start notif | Finish notif | -|---|---|---|---| -| Spin the Wheel | ✅✅ live (prior) | via `sessions` trigger* | via `sessions` trigger* | -| This or That | ✅✅ live (prior) | via `sessions` trigger* | via `sessions` trigger* | -| How Well | ✅ fixed (prior) | via `sessions` trigger* | via `sessions` trigger* | -| Desire Sync | ✅ fixed (prior) | via `sessions` trigger* | via `sessions` trigger* | -| Connection Challenges | ✅ clean (prior) | n/a (completion-based) | n/a | -| Date Match | ✅ E2EE + rules (prior) | ⚠️ `notifyOnDateMatch` not deployed (N6) | — | -| Daily reveal | ✅✅ live (prior) | `onAnswerWritten` / `sendPartnerAnsweredNotification` | reveal-ready | +| ID | Area | Screen/Route | Severity | Description | Repro | Status | +|---|---|---|---|---|---|---| +| A-001 | Premium gating | PlayHubViewModel, DesireSyncScreen, MemoryLaneScreen, ConnectionChallengesScreen, QuestionPackLibraryViewModel, wheel CategoryPicker/SpinWheel/WheelHistory(VMs) | **P1** | These gate on per-user `EntitlementChecker.isPremium()` instead of couple-shared `CouplePremiumChecker.coupleHasPremium(partnerId)`. A free partner of a premium user stays locked — violates couple-shared requirement. | Set Sam premium, QA free → QA Play hub still shows 🔒 Premium on Desire Sync + Memory Lane (confirmed live). Code: only `ConversationViewModel` uses CouplePremiumChecker. | Open | +| 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 | -\* **All games share one notification trigger:** start writes `couples/{id}/sessions/{sessionId}.status="active"`, finish writes `"completed"`, and `onGameSessionUpdate` fires on that single doc. So N3/N4/N5 fix start+finish notifications for **every** game at once. Pipeline is proven (function writes `notification_queue` + sends FCM; FCM delivery verified in N1); the start-name/finish-both fixes take effect after the deploy in N6. +**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 +_not started_ -## DEPLOY CHECKLIST (your call — prod deploys/admin writes are blocked for the agent) -1. `firebase deploy --only functions` — ships N3, N4, N5 (server side) and the `notifyOnDateMatch` rename (N6). -2. `firebase deploy --only firestore:rules` — Date Match (`date_swipes`/`date_matches`) + sealed `releaseKeys` sender-read, if not already live. -3. Install the refreshed APK (`Closer-v0.1.0-debug-2026-06-24.apk`) — ships N1, N2, N5 (client) + the in-app message bubble. -4. A leftover **active `this_or_that` session** is in `couples/{id}/sessions` from prior testing; it blocks starting a new game until that game is finished (or the doc is removed — admin delete needs your authorization). +## Pass C — Visual (light + dark) +_not started_ -## Completed earlier (kept for reference, no longer open) -- Daily reveal sealed-key exchange (release-key tolerant read, epoch-millis `updatedAt`, id→label mapping) — ✅✅ verified live. -- Game-start crash (`saveSession` empty id → invalid path) — ✅✅ fixed; This or That verified live. -- Game re-entry flicker/re-submit (This or That, How Well, Desire Sync) — ✅ pre-check → WAITING. -- Daily question determinism + shared `DailyQuestionResolver` — ✅✅ verified live. -- Partner identity (users partner-read rule) — ✅✅ verified live ("Connected with Sam/QATester"). -- Date Match E2EE + rules rewrite — ✅ (⚠️ still needs the deploy in checklist #1/#2). +## Pass D — Security & Encryption +_not started_ + +## Pass E — Notifications +_not started_