qa(round1): Pass A complete — couple-shared premium gap (A-001, P1)

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 <noreply@anthropic.com>
This commit is contained in:
null 2026-06-24 19:59:04 -05:00
parent 64f0a7e6c8
commit 452aaf787a
2 changed files with 67 additions and 58 deletions

38
ClaudeQACoverage.md Normal file
View File

@ -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 (D1D6)
_todo_
## Pass E — Notifications (17 types × {foreground, background, killed} + tap-to-open)
_todo_

View File

@ -1,71 +1,42 @@
# Claude QA Report — Games & Notifications # Claude QA Report — Full-App QA (living report)
**Updated:** 2026-06-24 > **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.)
**Devices:** emulator-5554 (Device A = QATester) + emulator-5556 (Device B = Sam), paired for real (coupleId `xNd1H2UGUDNqvyrDGgfu`). > Playbook: `ClaudeQAPlan.md`. Coverage matrix: `ClaudeQACoverage.md`. Report-only during passes (no fixes until the fix phase).
**Focus this pass:** every game works end-to-end **and notifications fire correctly on game start + finish.** > Devices: emulator-5554 (QA=`Y05AKO`) + emulator-5556 (Sam=`imDjjO`), paired (coupleId `Xal3Kw3gjSdn0niERYKJ`). Build == HEAD `64f0a7e`.
**Severity:** 🔴 critical · 🟠 high · 🟡 medium · 🟢 low _(Prior games/notifications QA from 2026-06-24 was completed + verified; superseded by this full-app effort.)_
**Status:** 🔎 found · 🛠 fixing · ✅ fixed & builds · ✅✅ verified live · ⚠️ needs deploy
--- ---
## OPEN — current error log ## Severity summary (Round 1)
| Severity | Open | Fixed |
### 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. | P0 | 0 | 0 |
**Fix:** `MainActivity` now observes `authState` and calls `tokenRegistrar.register()` whenever a user is authenticated. ([MainActivity.kt](app/src/main/java/app/closer/MainActivity.kt)) | P1 | 1 | 0 |
**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." | P2 | 0 | 0 |
| P3 | 0 | 0 |
### 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 *"<their own partner-name> 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 ("<name> 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 `"<name> is playing"` even for finish events (shown verbatim when the app is backgrounded). Now type-aware → `"<name> 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. N3N5).
**Action:** `firebase deploy --only functions` (allow it to delete `createDateMatchOnMutualLove`).
--- ---
## 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 | | ID | Area | Screen/Route | Severity | Description | Repro | Status |
|---|---|---|---| |---|---|---|---|---|---|---|
| Spin the Wheel | ✅✅ live (prior) | via `sessions` trigger* | via `sessions` trigger* | | 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 |
| This or That | ✅✅ live (prior) | via `sessions` trigger* | via `sessions` trigger* | | 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 |
| 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 |
\* **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) ## Pass C — Visual (light + dark)
1. `firebase deploy --only functions` — ships N3, N4, N5 (server side) and the `notifyOnDateMatch` rename (N6). _not started_
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).
## Completed earlier (kept for reference, no longer open) ## Pass D — Security & Encryption
- Daily reveal sealed-key exchange (release-key tolerant read, epoch-millis `updatedAt`, id→label mapping) — ✅✅ verified live. _not started_
- 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. ## Pass E — Notifications
- Daily question determinism + shared `DailyQuestionResolver` — ✅✅ verified live. _not started_
- 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).