8.8 KiB
Claude QA Report — Full-App QA (living report)
RUN-STATE: Round 2 (re-QA + deferred coverage) NEXT | NEXT ACTION: re-verify A-001 + E-001 fixes; play each game ONE complete time through on both devices (Pass B was launch-only — full playthroughs still owed); then Pass C deep/stateful screens (reveal, wheel session, dates, bucket list, auth/onboarding) in both themes, full live notification matrix, D3 live non-member test. 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. 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 (Round 1)
| Severity | Open | Fixed |
|---|---|---|
| P0 | 0 | 0 |
| P1 | 0 | 1 |
| P2 | 0 | 1 |
| P3 | 3 | 0 |
Round 1: all P0–P2 found were FIXED (A-001 premium P1, E-001 notif-routing P2). Open = P3 cosmetics only (A-003 badge, B-001 stale-session guard, E-002 informational notif routing). Deferred for a clean "flawless" certification: exhaustive deep/stateful screens (Pass C), full live notification matrix + D3 live non-member test.
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. | Open (deferred — thread hasPremium into the card composables) |
| 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 | P3 (observe) | A stale active wheel session (startedBy QA, createdAt missing/undefined) blocked all games ("Waiting for Sam"). In-app "End their game" recovery works (session→completed, games unblocked). Likely a prior-test artifact, but: sessions appearing without a timestamp + one stale session blocking every game is worth a guard. |
Open This or That → "Waiting for Sam… Sam is playing a Wheel game"; tap End their game → unblocked. | Open |
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).
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) |
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.
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 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:. 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.
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. |
Open |
Full live delivery matrix (17 types × foreground/background/killed) deferred — key types verified prior round; routing now code-correct.