# Claude QA Report โ€” Games Audit **Date:** 2026-06-23 **Method:** Per-game code review (state machine, E2EE, navigation, session lock) + build verification. Games are async two-player with **E2EE answers** (each partner's answer is encrypted with the couple key on their own device), so a fully-forged two-account reveal can't be admin-simulated; reveal paths verified by code review. Live solo smoke testing on emulator-5554 where reachable. **Severity legend:** - ๐Ÿ”ด **CRITICAL** โ€” breaks the game / data loss / crash / wrong reveal. - ๐ŸŸ  **HIGH** โ€” broken in a common path but recoverable, or gated on a deploy. - ๐ŸŸก **MEDIUM** โ€” wrong behavior in an edge case / confusing UX. - ๐ŸŸข **LOW** โ€” cosmetic. Status key: ๐Ÿ”Ž found ยท ๐Ÿ›  fixing ยท โœ… fixed & builds. --- ## Findings & status ### 1. Spin the Wheel โ€” โœ… no bugs found Correct WAITING/REVEAL gating, releases the one-game lock via `markUserComplete` on reveal, has an `abandon` path, and a **re-entry guard** (`load()` jumps to the reveal if the user already answered โ€” doesn't re-ask). Reference implementation for the other games. ### 2. This or That โ€” ๐ŸŸก MEDIUM โ†’ โœ… fixed **Bug:** Re-opening the game while waiting for the partner re-entered `joinSession`, which set `PLAYING` (the actual question screen) before `observeReveal` could flip to `WAITING` โ€” a flicker of the already-answered question plus a small window to re-submit. **Fix:** `joinSession` now pre-checks `getAnswers().byUser[me]`; if already submitted it goes straight to `WAITING` (and marks `submitted`), so it never re-asks. ([ThisOrThatScreen.kt](app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt)) ### 3. How Well Do You Know Me โ€” ๐ŸŸก MEDIUM โ†’ โœ… fixed **Bug:** Same re-entry pattern โ€” `joinSession` set `INTRO` then relied on `observeReveal` to flip; an already-answered user could tap into the answer flow during the window. **Fix:** Pre-check existing answers โ†’ straight to `WAITING`. ([HowWellScreen.kt](app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt)) ### 4. Desire Sync โ€” ๐ŸŸก MEDIUM โ†’ โœ… fixed **Bug:** Same re-entry pattern (`INTRO` before the observer flips). **Fix:** Pre-check existing answers โ†’ straight to `WAITING`. ([DesireSyncScreen.kt](app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt)) **Note (by design, not a bug):** the reveal shows only mutually-positive desires; non-matches stay private. ### 5. Date Match ("date night") โ€” ๐Ÿ”ด CRITICAL ร—2 โ†’ โœ… fixed (โš ๏ธ needs deploy) Found & fixed earlier this session: - ๐Ÿ”ด **Swipes stored in plaintext** โ€” the server (and the data layer) could read each partner's date preferences; the only non-E2EE game. โ†’ Now E2E-encrypted with the couple key; matching moved client-side. ([FirestoreDateSwipeDataSource.kt](app/src/main/java/app/closer/data/remote/FirestoreDateSwipeDataSource.kt)) - ๐Ÿ”ด **`date_swipes` security rules were self-defeating** โ€” required `swipedAt is timestamp` (client writes a number) and `actions.hasOnly([uid])` (a merge write exposes the whole doc, so the **second** partner to swipe any idea was rejected โ†’ a mutual match could never form). โ†’ Rewrote with `is number` + a `diff().affectedKeys().hasOnly([uid])` own-entry check. ([firestore.rules](firestore.rules)) - โš ๏ธ **DEPLOY REQUIRED:** the new client writes ciphertext swipes, which the *currently-deployed* rules reject. Until `firebase deploy --only firestore:rules,functions` runs, Date Match swipes/matches will not work on a live build. ### 6. Connection Challenges โ€” โœ… no bugs found Per-user `completions.{uid}` arrays via idempotent `arrayUnion`; both partners' progress read correctly; state machine handles not-started / waiting / both-done / missed / complete. ### 7. Daily Question reveal (core) โ€” ๐Ÿ”ด CRITICAL ร—2 โ†’ โœ… fixed Found & fixed earlier this session: - ๐Ÿ”ด **Daily question was `ORDER BY RANDOM()`** โ€” a different question loaded every time, so after answering, re-opening showed a new question and `getAnswer()` missed โ†’ it **re-asked**, and the two partners never saw the same prompt. โ†’ Deterministic per-day selection (`ORDER BY id` + date offset), identical across reloads and both devices. ([QuestionDao.kt](app/src/main/java/app/closer/data/local/QuestionDao.kt), [RoomQuestionRepository.kt](app/src/main/java/app/closer/data/repository/RoomQuestionRepository.kt)) - ๐Ÿ”ด **Home and the answer screen resolved *different* questions** (Home: generic pool; screen: Firestore assignment + mode pool) โ†’ Home's answered-state check never matched โ†’ it showed "your turn" after you'd answered โ†’ re-ask. โ†’ Extracted a shared [DailyQuestionResolver](app/src/main/java/app/closer/domain/usecase/DailyQuestionResolver.kt) used by both. ### 8. Notifications (cross-cutting) โ€” ๐ŸŸ  HIGH (deploy-gated) No partner-answered / "it's a match" / game pushes arrive because the **Cloud Functions are not deployed** (`onAnswerWritten`, `notifyOnDateMatch`, `onGameSessionUpdate`). Code + Firestore paths are correct. โ†’ Needs `firebase deploy --only functions`. Not an app-code bug. ### 9. Pairing congrats photos โ€” โœ… fixed Both faces now load from each partner's user doc; the current user's photo also falls back to the Firebase Auth (Google) avatar if the doc write lagged. Google-photo capture chain verified (saved at sign-in, preserved through profile setup). ([PairingSuccessScreen.kt](app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt)) --- ## Summary | Game | Critical | Fixed | Needs deploy | |------|----------|-------|--------------| | Spin the Wheel | โ€” | โœ… clean | โ€” | | This or That | โ€” | โœ… re-entry | โ€” | | How Well | โ€” | โœ… re-entry | โ€” | | Desire Sync | โ€” | โœ… re-entry | โ€” | | Date Match | ๐Ÿ”ด ร—2 | โœ… E2EE + rules | โš ๏ธ rules+functions | | Connection Challenges | โ€” | โœ… clean | โ€” | | Daily reveal | ๐Ÿ”ด ร—2 | โœ… deterministic + shared resolver | โ€” | | Notifications | ๐ŸŸ  | (code correct) | โš ๏ธ functions | **All code-level bugs fixed; app builds (`assembleDebug` โœ…).** Two items are **deploy-gated** (Date Match rules/functions, all push notifications) and require: ``` firebase deploy --only firestore:rules,functions ``` ## Live verification (emulator-5554) - โœ… App builds (`assembleDebug`) and launches with no crash on the patched build. - โœ… Created a real account through the full sign-up โ†’ profile โ†’ invite flow; Play hub renders all games. - โœ… Unpaired game entry routes correctly (This or That โ†’ invite screen, reuses the pending code) with no crash. - โ›” **Full paired playthrough blocked here:** completing pairing required an admin-SDK write to fabricate a partner + forge invite acceptance on the production DB, which the sandbox security classifier blocks. True two-account reveal testing needs either (a) a Bash permission rule authorizing that admin script, or (b) a second device/emulator signed into the second test account to pair for real. Reveal logic was therefore verified by code review. ## ๐Ÿ”ด LIVE TWO-DEVICE FINDINGS (emulator-5554 + emulator-5556, real pairing) Ran a second emulator (`Closer2`), signed up a 2nd account (Sam), and **paired for real** by accepting the invite code (App Check passed โ€” the debug token `e2dc8256-โ€ฆ` is deterministic, so both devices share it). This surfaced critical paired-experience bugs the single-device review couldn't: - โœ… **Pairing works** on two real devices; both reach the connected screen. - โœ… **Daily question fixes verified live:** both devices load the **same** question (shared resolver), and after answering, re-opening shows the saved answer โ€” **no re-ask** (the deterministic + resolver fixes work). - ๐Ÿ”ด **CRITICAL โ€” partner user-doc read denied.** `Firestore: users/{partnerUid} โ€ฆ PERMISSION_DENIED`. The `users` rule was owner-only, so partner **name shows "Your partner" and the photo can't load** โ€” anywhere (pairing screen, Home, games). This is the real cause of "doesn't show the paired user's face." **Fixed** the `users` rule to allow a paired partner to read the other's doc. ([firestore.rules](firestore.rules)) โ€” **needs deploy.** - ๐Ÿ”ด **CRITICAL โ€” daily sealed reveal fails.** Tapping "Reveal" โ†’ *"Reveal unavailable โ€” the sealed answer key is stored on the device you originally answered on."* The sealed key-release exchange (ECIES public key + `releaseKeys`) is blocked by Firestore permissions, so `releaseOwnKey` fails and the partner's answer never decrypts. **The core daily reveal does not complete for a paired couple.** - ๐ŸŸ  **Widespread paired reads denied on the live app:** partner `users`, `couples/{id}/outcomes`, `/capsules`, `/challenges`, and the sealed `releaseKeys`/`devices` paths all returned `PERMISSION_DENIED` โ€” even though the **repo** rules permit couple members. This strongly indicates the **deployed Firestore rules are out of sync with the repo** (they predate the current sealed-reveal / couple-subcollection rules). ### Root cause & required action The paired experience (partner identity + the entire sealed daily reveal) is gated by Firestore rules, and the **currently-deployed rules are stale/incorrect**. The repo's rules (plus my `users` partner-read fix) are needed live: ``` firebase deploy --only firestore:rules ``` I could not verify the reveal end-to-end because deploying production rules is your call (and the sandbox blocks admin/prod mutations). **Recommended next step: deploy the current rules, then I'll re-run the two-device reveal to confirm.** ## ๐Ÿ” RE-TEST AFTER RULES DEPLOY (2026-06-24, two devices) User deployed the rules. Re-ran on emulator-5554 + emulator-5556: - โœ… **Partner identity fixed (verified):** Home now shows "Connected with **Sam**" / "Connected with **QATester**"; all the `users`/`devices` permission denials are gone. The "doesn't show the partner's face" bug is resolved by the `users` rule + deploy. - ๐Ÿ”ด **CRITICAL (NEW) โ€” games crashed on start.** Starting This or That hard-crashed: `IllegalArgumentException: Invalid document reference โ€ฆ couples/{id}/this_or_that has 3 segments`. Root cause: `QuestionSessionRepository.saveSession` generated the session doc id but returned `Result`, so `GameSessionManager.startGame` returned a session with **empty id** โ†’ `observeAnswers` built an invalid path โ†’ crash. Affected **This or That, Desire Sync, How Well** (all use `startGame`). **Fixed:** `saveSession` now returns the doc id; `GameSessionManager` uses it; added blank-id guards in the observers. ([QuestionSessionRepositoryImpl.kt](app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt), [GameSessionManager.kt](app/src/main/java/app/closer/domain/usecase/GameSessionManager.kt)) - โœ… **This or That verified end-to-end (live, both devices):** A started + answered โ†’ WAITING; B joined the **same** question set โ†’ answered; **both** revealed "matched on 2 of 5" with each prompt showing both partners' real picks ("You / Sam"). No crash, no denials. - ๐Ÿ”ด **CRITICAL (still open) โ€” daily sealed reveal.** Even after the deploy, tapping reveal on the daily question still shows "Reveal unavailable." `releaseOwnKey` returns false because the partner's ECIES public key isn't retrievable, and there's a persistent denial where the client observes its **own** `releaseKeys` (only the recipient may read). This is a deeper sealed-reveal (ECIES key-exchange) issue, separate from the rules already fixed โ€” **not yet resolved.** ### Status by game (after fixes) | Game | Start | Reveal | Notes | |---|---|---|---| | This or That | โœ… fixed | โœ… verified live | crash fixed | | Desire Sync | โœ… fixed (same path) | โณ not re-run | shares `startGame` fix | | How Well | โœ… fixed (same path) | โณ not re-run | shares `startGame` fix | | Spin the Wheel | โœ… (route-arg id, guarded) | โณ not re-run | โ€” | | Date Match | โณ | โณ | needs the date-swipe rules/functions deployed | | Connection Challenges | โณ | โณ | completion-based | | **Daily reveal** | โœ… both answer | ๐Ÿ”ด **fails** | sealed ECIES key-exchange broken | ## โœ… DAILY REVEAL โ€” FIXED & VERIFIED LIVE (2026-06-24, two devices + admin) Used admin reads to find ground truth (both public keys WERE published, both answers existed, but **no release keys** were ever written). Three bugs were blocking the sealed reveal โ€” all fixed: 1. **Sender couldn't write its release key.** `writeReleaseKey` did an existence-check `ref.get()` first, but the releaseKeys read rule is recipient-only โ†’ the sender's `get()` threw `PERMISSION_DENIED` โ†’ `releaseOwnKey` threw โ†’ "Reveal unavailable." **Fix:** tolerate the denied existence-read (treat as "not there, create it"). Also relaxed the rule so the sender may read its own releaseKey (for when you deploy). ([FirestoreReleaseKeyDataSource.kt](app/src/main/java/app/closer/data/remote/FirestoreReleaseKeyDataSource.kt), [firestore.rules](firestore.rules)) 2. **Reveal crashed reading the partner's answer.** `markAnswerKeyReleased` wrote `updatedAt` as a Firestore `serverTimestamp()`, but `toLocalAnswer` read it with `getLong()` โ†’ `RuntimeException: Field 'updatedAt' is not a java.lang.Number` โ†’ hard crash when the partner opened the reveal. **Fix:** write `updatedAt` as epoch-millis, and read time fields defensively (Long / Timestamp / Date). ([FirestoreAnswerDataSource.kt](app/src/main/java/app/closer/data/remote/FirestoreAnswerDataSource.kt)) 3. **Partner answer showed the raw option id** ("a_photo" instead of "A photo") โ€” the sealed payload stores only ids. **Fix:** map ids โ†’ labels via the question in the reveal VM. ([AnswerRevealViewModel.kt](app/src/main/java/app/closer/ui/answers/AnswerRevealViewModel.kt)) **Verified end-to-end on both emulators:** A revealed โ†’ "your key is released, waiting"; B revealed โ†’ saw **A's** answer decrypted; A then saw **B's** answer decrypted. Both partners see each other's answers, no crash. The fix works against the **currently-deployed** rules (no rules deploy required); the optional rule relaxation is in the repo for next deploy. ## Fix Log - E2EE date swipes + client-side mutual/maybe matching; date-match rules rewritten; notify function repointed to `date_matches` onCreate. - Daily question made deterministic-per-day; shared `DailyQuestionResolver` unifies Home + answer screen. - Re-entry pre-check (`getAnswers` โ†’ WAITING) added to This or That, How Well, Desire Sync. - Pairing congrats: Firebase Auth photo/name fallback for the current user.