In-context dark+light verified (Bucket List, Quiet hours, Security, Delete account);
A1/A3 + empty-only states via the debug gallery both themes. Caught + fixed a stale
build on 5556. Baseline intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DeleteAccountScreen gains illustration_account_deletion_goodbye centered above the copy —
a soft box releasing hearts (no alarm imagery), making the goodbye respectful and on-brand.
Verified live on dark; 0 FATAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PairingSuccessScreen replaces the white-keyhole app-icon chip joining the two partner
avatars with the illustration_pairing_success celebration (transparent, tile=false,
keeps the spring + pulse) so the "you're connected" beat shows the mark resolving with a
burst of hearts. SecurityScreen gains the illustration_privacy_recovery scene at the top.
Verified live: Security on dark (warm privacy-lock, not cold vault); A1 confirmed in the
debug gallery (transparent floats cleanly). 0 FATAL. Pairing-success needs a fresh pairing
to see in situ; A1 render proven via gallery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ConnectionChallenges series-list gains the illustration_connection_challenges_header
banner (16:9, BrandIllustration) under the title. Notification settings Quiet-hours
section gains the illustration_quiet_hours scene centered above the toggle. Verified live:
Quiet hours on dark (night-window scene reads beautifully); A3 banner + A1 (transparent,
tile=false) + A2 confirmed in the debug gallery — all crisp + on-brand. 0 FATAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DateMatches empty -> illustration_date_match_empty; the "It is a match!" modal replaces
the heart-icon circle with illustration_date_match_success (celebration). Memory Lane
empty replaces the 📦 emoji with illustration_memory_lane_capsule. ArtPreviewScreen
(debug) now shows all 12 new illustrations via BrandIllustration so they're verifiable on
both themes without needing empty/match data. Verified live (gallery, dark): A10/A11/A12
tiles render crisp + on-brand; 0 FATAL. (Empty/match states need data not present on the
baseline couple; render path proven via the shared tile.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AnswerHistory primary empty swapped from the generic illustration_couple_history to the
purpose-made illustration_answer_history_empty (A2). WheelHistory (the "Past Games"
history screen) empty gains illustration_past_games_empty (A10). Both via the shared
EmptyState (rounded-tile, both-theme verified in Run 2). Empty states need empty data so
not reachable live on the baseline couple; render path proven via the shared component.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EmptyState already supports illustrationResId (rounded-tile clip), so Bucket List just
passes illustration_bucket_list_empty. Messages inbox gained a proper empty state
("Your private conversation starts here") with illustration_messages_empty. Added
BrandIllustration() helper (theme-safe rounded tile / tile=false for transparent art)
for the upcoming header/hero placements. Verified live both themes: rounded illustration
tile reads cleanly on dark (card) and light (white card on blush); 0 FATAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code check: the speculative Pass E types added by the merged playbook (join_game,
partner_joined_game, game_ended, date_plan_update, subscription_entitlement_changed,
...) are not implemented (0 files) -> marked not implemented -> Future.md. Logged the
worthwhile ones to Future.md ## QA (notify free partner on couple premium unlock; join/
end pushes). Round 8 at the flawless bar: 0 open P0-P2 (1 P3 J-OBS); I-001/I-002 fixed+
verified pending Round 9 confirm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
I-001: getOutcomes() did a bare collection list .get() on couples/{cid}/outcomes,
which firestore.rules denies (reads allowed only for dayKey in day_0/30/60/90) ->
always PERMISSION_DENIED, swallowed to emptyList(). Now scopes the query with
whereIn(FieldPath.documentId(), OUTCOME_DAY_KEYS) so it satisfies the rule.
I-002 (found while fixing I-001): toOutcomeScores() cast values to Map<String,Int>,
but Firestore returns integer fields as Long on Android -> ClassCastException ->
scores dropped (same shape submitOutcomeCallable writes, so the real path was broken
too). Now coerces (value as? Number)?.toInt().
Verified live: 0 outcomes PERMISSION_DENIED after relaunch; seeded a day_0 baseline
(int64) -> "Your Progress" shows "Baseline recorded" (was "No baseline yet"). Seed
removed, couple baseline restored (0 outcomes, 0 active sessions). Both pending one
re-QA confirmation round before pruning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cold start 1253ms; core tabs 6.3% janky; conversation/Play-hub scroll ~36ms 90th;
no window/Activity/listener leak (meminfo stable over open/close x6); lazy-load (17),
Coil (11), Room caching all in place. Found I-001 (P1): FirestoreOutcomeDataSource
.getOutcomes() does a bare collection list .get() that firestore.rules:658 denies
(reads allowed only for dayKey in day_0/30/60/90) -> always PERMISSION_DENIED, swallowed
to emptyList() -> "Your Progress" never shows recorded outcomes + re-prompts done days.
Fix (fix phase): whereIn(documentId, [day_0..90]).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 8 chunk 1. Simultaneous mood-tap on both emulators -> exactly 1 active session
(was 2 pre-fix); race-loser hit WaitingForPartner -> "Join the game" -> joined the
winner's session at the same Q1 (shared reveal preserved). Regression smoke clean:
no FATAL, game opens both devices, inbox loads, messages + date_swipes ciphertext at
rest. F-RACE-001 pruned to the archived-ID line per report hygiene; 0 open P0-P3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simultaneous game start by both partners created two divergent active sessions (TOCTOU: a
non-transactional check-then-create in GameSessionManager.startGameWithCouple). Each partner
ended up in a separate session with different questions → no shared reveal.
Fix: QuestionSessionRepository.startSessionAtomically runs a Firestore transaction on a
per-couple pointer doc (couples/{cid}/sessions/_active). It reads the pointer (+ the pointed
session) and either returns AlreadyActive (caller joins the existing session) or atomically
creates the new session and re-points the lock. Concurrent starts contend on the one pointer,
so the loser's transaction retries, sees the now-set pointer, and joins instead of duplicating.
The pointer self-heals (checks the pointed session's status) so no clear-on-finish is needed,
and it carries no status/completedAt so it's invisible to the active/history queries.
GameSessionManager routes all 7 games through it. firestore.rules adds member-write for
sessions/_active (deployed).
Verified live on both emulators: atomic create → 1 session + pointer; sequential 2nd start →
joins (1 session); literal parallel-tap race → 1 session (was 2); 0 FATAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the QA improvement backlog from Future.md:
- Inclusive sex/gender options (Female/Male/Non-binary/Prefer not to say) in onboarding +
Edit Profile; honest copy (Desire Sync is already gender-neutral, no tailoring fallback needed).
- Turn-aware Home "waiting to play" copy ("Your turn to play.").
- Partner-action/results pushes exempt from the weekly promotional rate-limit ceiling
(NotificationRateLimiter); reminders still bound by it. Tests updated.
- Suppress the redundant results / "partner finished" push when the recipient is already on that
game's screen — new ActiveGameSessionMonitor (mirrors ActiveThreadMonitor), wired into the
This or That / How Well / Desire Sync VMs + Wheel results; guarded in PartnerNotificationManager.
- Paywall: retry-with-backoff, offline-aware error copy, Continue hidden until plans load.
- Privacy-message rotator on Sign up + Forgot password (Login already had it).
iOS illustrations were already wired into the Android empty states (no change needed).
Brand-glyph G-set remains in Future.md — blocked on generated art.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pass H: consumer-mindset branding review of every screen; output = ready-to-paste ChatGPT image
prompts; must lock the house style first (read brand docs + open existing illustrations) so all
generated art matches the shipped artwork.
- ClaudeBrandingReview.md: canonical House Style prompt prefix + palette + negatives; screen-by-screen
audit (every route); 12 illustration prompts (A1-A12) + glyph set + pack-art prompt, all reusing the
house style; flags 'wire existing iOS art into Android' vs new generation.
- Future.md QA: non-art branding ideas (wire iOS illustrations to Android, consistent glyphs, rotate
privacy messages on auth screens).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pass B: cover the full depth x round-length matrix (Light/Everyday/Deep/All x 5/10/15), not one combo;
short+long, shallow+deep, every answer type.
- Methodology: THINK AS A CONSUMER (approach from many angles); capture works-but-could-be-better /
feature ideas to Future.md '## QA' (kept separate from the ClaudeReport.md bug log).
- New Future.md seeded with 5 grounded QA improvement ideas (inclusive onboarding options, turn-aware
'waiting to play' copy, rate-limit exemption for high-value pushes, suppress redundant results push,
friendlier paywall error state).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add android.notification.channelId to the 4 senders not covered by the earlier batch:
scheduledOutcomesReminder + dailyQuestionReminder + reengagement -> 'reminders';
sendGentleReminderCallable -> 'partner_activity'. Completes E-OBS so backgrounded pushes land on
their proper channels (importance/sound + per-category toggle) instead of fcm_fallback. tsc green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The paywall ErrorState rendered uiState.error verbatim, surfacing developer-facing billing/RC SDK
text ('There was a credentials issue. Check the underlying error for more details.') to users.
Now always shows friendly copy. Verified live: free user -> paywall -> 'We couldn't load
subscription options right now. Check your connection and tap to try again.' (no raw error).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generic WaitingForPartner screen only exited when the session became null, so a partner who
landed there for an async game they could actually play (every current game is async — both play
on their own device) was stuck waiting forever, recoverable only via Back to Games. Now the screen
resolves the active session's game route and offers a primary 'Join the game' action that drops the
user into the game (which auto-joins the session). Deterministic repro: QA starts How Well, Sam
opens a different game -> one-game lock routes Sam to WaitingForPartner -> 'Join the game' -> How
Well guess intro. Verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>