diff --git a/ClaudeBrandingReview.md b/ClaudeBrandingReview.md index d5856df6..58d9de86 100644 --- a/ClaudeBrandingReview.md +++ b/ClaudeBrandingReview.md @@ -18,7 +18,49 @@ on-brand, in-context, both themes, no clipping/placeholder/off-brand issues** (a bug; none found). The new game-alert surfaces (`GamePromptBanner`, `GameWaitingHeroCard`) use the brand purple gradient + PlayArrow glyph and read as intentional action banners — no illustration warranted. No new art to add this round. -## ✅ WIRED INTO ANDROID (2026-06-26 art drop) — all 11 generated illustrations live +## This or That gameplay brand plan (Codex QA, 2026-06-27) +Live review used a dedicated QA launcher/device (`CloserCodexQA`) with a fresh admin-created test couple +(`codex-this-or-that-*` / `codex-partner-*`). Screenshots checked the mood picker and active gameplay in light and dark +mode. The visual issue is real: light-mode option buttons are directionally good, but dark mode makes the current prompt +backdrop feel like an accidental placeholder. + +**Verdict:** The current `ChoicePromptBackdrop` two-circle + diagonal-line drawing should be treated as a branding defect +for active gameplay. In dark mode the diagonal line cuts through the question, the circles turn muddy, and the whole +motif reads like a technical diagram instead of a warm private ritual for two. + +**Plan** +- Replace the two circles + line with a Closer-native "private choice" motif: two soft sealed answer cards, paired-card + silhouettes, or a subtle C-heart/keyhole accent. Keep it decorative and low-contrast behind the prompt; no line should + cross the question text. +- Keep the light-mode option card shape, spacing, and button feel. Make the colors theme-aware rather than reusing fixed + purple/pink values in both themes. +- Dark-mode option cards need stronger contrast: readable body text, visible borders, and selected states that feel rich + instead of dim. Disabled/other-selected states should still be legible. +- Keep `closerBackgroundBrush()` as the foundation, but consider one very subtle game-local glow or paired-card vignette + in brand colors. The background should support focus, not compete with the question or options. +- Re-check the mood picker after gameplay is fixed. The numbered mood circles are acceptable, but paired-card glyphs or + warmer step badges would feel more branded than plain numbered bubbles. +- Results can reuse `illustration_reveal_celebration` and existing heart/petal particles for high-match moments; the game + surface itself should stay code-native rather than needing a full raster illustration. + +**Implementation map** +- Update `app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt`, especially `ChoicePromptBackdrop`, + `OptionCard`, and `VersusBadge`. +- Add local theme-aware helpers for This-or-That option A/B colors, selected colors, disabled colors, border colors, and + prompt-backdrop alphas. Validate against both `MaterialTheme.colorScheme.background` and `surface`. +- Prefer Compose `Canvas`/shape drawing for the prompt motif. Generate raster art only if the code-native motif cannot + carry the brand; if generated, use transparent PNG art with no readable text. +- Add or update light/dark Compose previews for mood selection, active prompt, selected answer, waiting/disabled, and + results. + +**Acceptance checks** +- Light and dark screenshots show no decorative element crossing or competing with the prompt text. +- Long prompts, long options, selected, unselected, and disabled states stay readable. +- The light-mode cards retain the current friendly button feel. +- Dark mode feels intentionally Closer-branded: aubergine/lavender/pink, soft and private, not a placeholder diagram. +- Logs are checked after opening a game from notification/deep link before assuming the route worked. + +## ✅ WIRED INTO ANDROID (2026-06-26 art drop) — generated illustrations live The generated art (source in gitignored `docs/brand/generated-art/`, copied full-res to `app/src/main/res/drawable-nodpi/`) is now wired into the app via the shared `EmptyState` (rounded-tile, theme-safe) and a new `ui/components/BrandIllustration.kt` helper: @@ -38,8 +80,9 @@ The generated art (source in gitignored `docs/brand/generated-art/`, copied full | A12 `account_deletion_goodbye` | DeleteAccountScreen header | **live dark** | All 12 also live in the debug **Art preview** gallery (Settings → Art preview) for both-theme verification. -**Not done:** A7 pack art = **N/A** (all 10 packs already have `pack_art_*`). **G-set glyphs = still backlog** -(not generated; tracked in `Future.md` `## QA`). Empty/match/pairing states that need empty-or-new data weren't +**Not done:** A7 pack art = **N/A** (all 10 packs already have `pack_art_*`). **G-set glyph art generated +2026-06-27 and ready to add** from `docs/brand/generated-art/glyphs/`; app wiring is still pending. +A1-A12 prompts are complete and should not be regenerated. Empty/match/pairing states that need empty-or-new data weren't reachable on the baseline couple — render path proven via the shared tile + gallery. Commits `077a408`→`5868d06` on `dev`. --- @@ -72,17 +115,17 @@ reachable on the baseline couple — render path proven via the shared tile + ga - Always readable on **both** blush-white (light) and aubergine (dark) surfaces — keep a soft self-contained vignette rather than a hard rectangular background; export PNG with transparency where noted. -**Existing assets (reuse before generating):** `illustration_couple_onboarding`, `_invite`, `_history`, `_paywall`, -`_subscription`, `illustration_tonight_partner_prompt`, `illustration_partner_activation`, -`illustration_reveal_celebration`, `illustration_streak_milestone`, `illustration_together_empty`, -`pack_art_{deep_reflection,family_commitment,home_life,trust_repair,money_values}`, `particle_heart`, `particle_petal`. -NOTE: most live only in iOS / `drawable-nodpi`; several screens below need the **Android copy** wired up, not new art. +**Existing assets (reuse before generating):** the generated A1-A12 illustration set above, `illustration_couple_*`, +`illustration_daily_question`, `illustration_tonight_partner_prompt`, `illustration_partner_activation`, +`illustration_reveal_celebration`, `illustration_streak_milestone`, `illustration_together_empty`, all 10 +`pack_art_*` assets, `particle_heart`, and `particle_petal`. These are Android assets now; only generate new art when a +future QA pass finds a specific missing surface or replacement-worthy defect. --- ## Screen-by-screen audit -Legend: ✅ on-brand / no art needed · ➕ add/þwire art (prompt below) · 🎨 new art to generate · 🔤 brand-copy/color touch +Legend: ✅ on-brand / no art needed · ➕ add/wire art · 🎨 new art to generate · 🔤 brand-copy/color touch | Screen / surface | Current brand state | Opportunity | |---|---|---| @@ -90,113 +133,64 @@ Legend: ✅ on-brand / no art needed · ➕ add/þwire art (prompt below) · | Welcome (Create / I have account) | heart mark + privacy line | ✅ on-brand; could rotate privacy messages 🔤 | | Sign up / Login / Forgot password | plain form | 🔤 add small heart mark + one privacy line above the form (no big art — keep forms clean) | | Create profile — name / sex / photo | plain steps | 🔤 light: small step glyphs; ✅ otherwise (forms stay clean) | -| Pair: invite (create code) | has recovery phrase, clean | ➕ small `illustration_couple_invite` at top (exists on iOS — wire to Android) | -| Pair: accept code / pairing success | minimal | 🎨 **A1** pairing-success celebration | +| Pair: invite (create code) | `illustration_couple_invite` wired | ✅ | +| Pair: accept code / pairing success | `illustration_pairing_success` wired | ✅ | | Home (paired) | cards, warm copy | ✅ good; ensure card icons use brand glyphs 🔤 | | Home (unpaired "bring your person in") | couple art present | ✅ on-brand | | Today / daily question | clean card | ✅; reveal moment is the place for art (below) | -| Answer reveal (mutual) | functional | ➕ use `illustration_reveal_celebration` + petal/heart particles at the reveal beat | -| Answer history | list | ➕ empty state → wire `illustration_couple_history`; 🎨 **A2** if none | +| Answer reveal (mutual) | `illustration_reveal_celebration` wired | ✅ | +| Answer history | `illustration_answer_history_empty` wired | ✅ | | Play hub | card list, game glyphs | ✅; verify each game card has a consistent brand glyph 🔤 | -| This or That (setup/play) | clean | ✅ | +| This or That (setup/play) | light buttons good; dark prompt backdrop off-brand | 🔤 replace two-circle/line motif + theme option colors | | This or That / How Well / Desire Sync **results** | score + rows | ➕ small celebration header art (reuse `reveal_celebration`); particles on high match | | How Well / Desire Sync intro | icon + copy | ✅ | | Spin the Wheel | nice wheel art | ✅ wheel is on-brand | | Wheel complete / results | text reveal | ➕ celebration header (reuse particles) | -| Connection Challenges (series list) | text list | 🎨 **A3** series/“small steps” header + per-series glyphs | +| Connection Challenges (series list) | `illustration_connection_challenges_header` wired | ✅; verify per-series glyph consistency 🔤 | | Connection Challenges (active day) | clean | 🔤 small streak/heart glyph; ✅ otherwise | -| Memory Lane (list) | sealed rows | 🎨 **A4** empty state (no capsules) + sealed-capsule glyph | -| Memory Lane (sealed capsule) | lock + date | ➕ use capsule art (A4) on the sealed card | +| Memory Lane (list) | `illustration_memory_lane_capsule` empty state wired | ✅ | +| Memory Lane (sealed capsule) | lock + date | ➕ optional: reuse capsule art on sealed-card detail, no new art | | Date Match (deck) | clean cards | ✅ | -| Date Match (your matches / empty) | list | 🎨 **A5** “no matches yet” + match celebration | +| Date Match (your matches / empty) | empty + success art wired | ✅ | | Plan Date / Date Builder | form | 🔤 small date-card glyph; ✅ otherwise | -| Bucket List (empty) | list | 🎨 **A6** “save ideas together” empty state | -| Question Packs (library) | `pack_art_*` present | ✅; 🎨 **A7** packs needing art (any pack without `pack_art_*`) | -| Messages (inbox empty) | list | 🎨 **A8** “no messages yet” (two soft bubbles, no text) | -| Conversation | chat | ✅ keep clean; quiet-hours state → A9 | -| Past Games (empty/list) | list | ➕ small replay/heart glyph; 🎨 **A10** if empty state bare | +| Bucket List (empty) | `illustration_bucket_list_empty` wired | ✅ | +| Question Packs (library) | all 10 `pack_art_*` assets present | ✅ | +| Messages (inbox empty) | `illustration_messages_empty` wired | ✅ | +| Conversation | chat | ✅ keep clean; quiet-hours art is in settings | +| Past Games (empty/list) | `illustration_past_games_empty` wired | ✅ | | Your Progress / Activity | stats | 🔤 brand-colored charts; reuse `streak_milestone` for milestones | | Paywall / Subscription | couple art present | ✅ strong (couple illustration + “one subscription for both”) | | WaitingForPartner | category glyph + copy | ✅ on-brand; ensure glyph per game type | | Settings + sub-pages | dense lists | ✅ keep clean — **no illustrations**; brand via section headers/color only 🔤 | -| Security / Recovery phrase | text | 🎨 **A11** privacy-lock illustration (two sealed cards behind a soft lock) | +| Security / Recovery phrase | `illustration_privacy_recovery` wired | ✅ | | Privacy & Terms | text | 🔤 small lock glyph header | -| Delete account | warning copy | 🎨 **A12** calm export/delete privacy scene (no alarm imagery) | -| Quiet hours (settings) | toggle | 🎨 **A9** moon/window/two-muted-phones | -| Notifications (system) | fallback glyph | 🎨 **G-set** monochrome notification glyph (heart/paired-card) | +| Delete account | `illustration_account_deletion_goodbye` wired | ✅ | +| Quiet hours (settings) | `illustration_quiet_hours` wired | ✅ | +| Notifications (system) | G-set glyph art generated, not wired | ➕ wire `docs/brand/generated-art/glyphs/android-vector/glyph_*.xml` | --- -## Art to generate — ChatGPT prompts (prefix the House Style block to each) +## Art to generate — status -Priority order = the moments a user feels most. Each is self-contained after you paste the House Style block first. +A1-A12 were generated and wired into Android. A7 is not needed because all 10 question packs already have `pack_art_*`. +G-set glyphs were generated 2026-06-27 and are ready to add from `docs/brand/generated-art/glyphs/`. There are no active +art-generation prompts right now. -**A1 · Pairing success celebration** — *1:1, transparent bg.* -> Scene: the Closer mark resolving into place — a soft-pink upper C-arc and a lavender lower sweep curving together to -> enclose a heart-shaped space with a small keyhole at its center — with a gentle burst of small floating hearts and -> petals around it. Convey "you're connected now." Calm, joyful, no text. (White keyhole if on a dark background.) +Do not regenerate completed illustration art unless a future QA pass logs a specific defect that requires replacement. +Always clean up completed art items: once an asset is generated, wired, and verified, remove its prompt from this active +backlog and update the audit table/status notes so only unfinished work remains. -**A2 · Answer history empty state** — *1:1.* -> Scene: a small soft journal/photo-album, slightly open, with two paired cards tucked inside and a few faint floating -> hearts; a lavender sprig beside it. Conveys "your shared moments will collect here." Quiet and inviting. - -**A3 · Connection Challenges header ("small steps together")** — *16:9 banner, object-led.* -> Scene: a gentle winding path of soft stepping-stones, each stone a small rounded card, leading toward a glowing -> heart; a tiny lit candle and lavender fronds at the edges. Conveys a shared daily habit, "one small step at a time." - -**A4 · Memory Lane — sealed capsule** — *1:1 (also usable as the sealed-card glyph).* -> Scene: a softly glowing sealed keepsake box/time-capsule with a small heart latch, a calendar/moon hint above it, a -> couple of petals settling. Conveys "sealed until your chosen date." Warm, secret, safe. - -**A5 · Date match — "it's a match" + empty** — *1:1.* -> Scene: two date-idea cards (one pink, one lavender) leaning together to meet in the middle with a small heart where -> they touch; faint calendar/clock and a sun/hill motif behind. Conveys "you both chose this." For the empty variant, -> show the two cards slightly apart with a dotted soft line between them. - -**A6 · Bucket List empty state** — *1:1.* -> Scene: a soft open notebook with a checklist of blank rounded lines, a small star and heart doodle, a pin/map dot -> and a tiny suitcase, lavender sprig accent. Conveys "save ideas to do together." Hopeful, light. - -**A7 · Question pack art (per missing pack)** — *16:9 banner, object-led, match `pack_art_*`.* -> Scene: a fanned deck of paired cards as the hero object, themed to the pack topic with one simple symbol (e.g. -> intimacy = soft flame + petals; communication = two speech shapes; future = a small horizon/sunrise), framed by -> lavender fronds and a soft glow. No people, no readable text. (Generate one per pack that lacks `pack_art_*`.) - -**A8 · Messages empty state** — *1:1.* -> Scene: two soft rounded chat bubbles (one pink, one lavender) gently overlapping with a small heart between them and -> a few floating petals; **no text inside the bubbles.** Conveys "your private conversation starts here." - -**A9 · Quiet hours** — *1:1.* -> Scene: a calm night window with a soft crescent moon and stars, and two muted phones resting face-down on a bedside -> surface with a small lavender sprig. Conveys "gentle, no pressure, paused for the night." Soothing, no alarm imagery. - -**A10 · Past Games empty state** — *1:1.* -> Scene: two paired game cards stacked with a soft replay/loop arc of petals around them and a faint heart. Conveys -> "the games you've played together will live here." Playful but calm. - -**A11 · Privacy / Recovery — privacy-lock** — *1:1 or 4:5.* -> Scene: two sealed cards or closed journals nestled behind a soft, friendly padlock with a small heart on it; a key -> shape made of a lavender sprig; quiet protective glow. Conveys "only the two of you hold the key." Reassuring, warm — -> **not** a cold security/vault look. - -**A12 · Account deletion — calm goodbye** — *1:1.* -> Scene: a soft open box gently releasing a few hearts/petals upward, with a faint door/horizon and a lavender sprig; -> muted, respectful, peaceful. Conveys "your data leaves with you." **No** warning triangles, red, broken hearts, or -> alarm imagery. - -**G-set · Notification + relationship glyphs** — *single-color vector, square, legible at 24 dp.* (One prompt each:) -> Simple single-color flat glyph in the Closer style, no text, no background: **the Closer C-heart-keyhole mark**, **paired sealed -> cards**, **daily card**, **sealed answer (card with small lock)**, **memory capsule**, **date-card with heart**, -> **quiet-hours moon**, **couple-premium (heart + small crown/spark, tasteful)**, **export-data**, **delete-account**. -> Export in lavender for in-app and as high-contrast monochrome for the platform notification glyph. +**Generated G-set files:** `glyph_closer_mark`, `glyph_paired_cards`, `glyph_daily_card`, `glyph_sealed_answer`, +`glyph_memory_capsule`, `glyph_date_card_heart`, `glyph_quiet_hours_moon`, `glyph_couple_premium`, +`glyph_export_data`, `glyph_delete_account`. --- ## Notes for the asset hand-off - Match filenames to the existing scheme: `illustration_.png` (nodpi), `pack_art_.png`, `glyph_.xml`, `particle_.png`. Provide @1x in `drawable-nodpi/` (illustrations) or density buckets where the existing asset has them. -- Several items above are **"wire existing iOS art into Android,"** not new generation — do that first (cheaper): - history, invite, daily-question, together-empty, partner-activation already exist on iOS. +- For the generated G-set, use the source SVGs in `docs/brand/generated-art/glyphs/source-svg/` for review and the + Android-ready vectors in `docs/brand/generated-art/glyphs/android-vector/` for app wiring. - After adding art, re-run Pass C (visual, light + dark) on those screens to confirm contrast + no clipping, and re-export store graphics per `docs/brand/visual-identity.md` if the palette/mark changed. diff --git a/ClaudeQACoverage.md b/ClaudeQACoverage.md index ee32a1a3..f9664513 100644 --- a/ClaudeQACoverage.md +++ b/ClaudeQACoverage.md @@ -10,12 +10,12 @@ ## Status at a glance | Pass | Coverage | Status | |---|---|---| -| A — Couple-shared premium | R12: code audit (all gates→CouplePremiumChecker) + live couple-shared unlock (Sam prem→QA free unlocks Desire Sync setup + Memory Lane badge) | ⚠️ 1 P1 (A-201 Date Match premium ideas ungated — free user liked a ★Premium idea, no paywall); all other gates couple-shared ✅ | +| A — Couple-shared premium | R12: code audit + live couple-shared unlock (Sam prem→QA free unlocks Desire Sync); **A-201 found + FIXED + verified live** (Date Match LOVE/MAYBE on premium idea → Paywall, SKIP passes) | ✅ pass (A-201 fixed pending 1 confirm; all gates now couple-shared incl. Date Match) | | B — Games lifecycle | R12: 4 async games full 2-device end-to-end (ToT Light×5, Wheel mixed-types, How Well asym, Desire Sync shared/private) + start/join/first-finisher/finish/results/back-stack; CC+MemoryLane+DateMatch render/core (full per R10) | ✅ pass (first-finisher nudge + C-NAV-002 + Ready=Start re-verified live; MemoryLane title/preview run-on → Future.md) | | C — Visual (light+dark) | R12: Messages(inbox+conv) both themes, Today, Subscription + organic A/B sweep (Home/Play/all game screens/Security/MemoryLane/DateMatch/Paywall) + R11 decoupled-theme art; back-stack spot-checks OK | ✅ regression-clean vs R10 full sweep · NEW **C-ART-EDGE-002 (P3)** direct-call hero art (daily-question, couple_subscription, etc.) hard edges on dark · C-DARKART-001+C-ART-EDGE-001 (shared helpers) hold | | D — Security & encryption | R12 LIVE: D1 (4 game collections enc:v1:) · D3 raw-API non-member 403×4 + member-scoped + messages not enumerable · D5 self-grant premium PATCH→403; D2/D4/D6/D7 carried R7/R10 (rules unchanged, no deploy) | ✅ clean — cornerstone holds; bounds A-201 (server blocks real premium self-grant) | -| E — Notifications | R10 live: E-GAME-002 confirmed (start push+banner+Join), finish/answer/reveal pushes | ✅ pass · E-GAME-002 pruned · full fg/bg/killed matrix **partial** | -| F — Resilience | R10: concurrency double-start→1 session · process-death→clean+FCM re-register · offline(R9) | ✅ pass · time-travel + deletion-cascade deferred | +| E — Notifications | R12 LIVE: Pass B verified start/first-finisher(`partner_completed_part`)/finish triggers→correct partners+copy; cold-start tap smoke **6/6** (launcher + 5 notif types open & stay) | ✅ pass · splash-crash class clean on fresh APK | +| F — Resilience | R12: concurrency (F-RACE-001 atomic-start code + R8 live) · process-death (smoke `am kill`×5 → push → cold-start recovered each) · offline(R9) | ✅ pass · time-travel + deletion-cascade deferred | | G — Account creation / fake-account | R10: abuse live via D3 (non-member denied, no self-grant) + invite rules; happy/validation R5-clean (unchanged) | ✅ pass | | H — Branding & artwork | R10: existing-art integration clean (0 defects), new game surfaces on-brand | ✅ see `ClaudeBrandingReview.md` | | I — Performance & route efficiency | R10: core-tabs jank 5.53% (R8 6.3%), 90th 32ms, leak proxy bounded | ✅ no regression | @@ -114,6 +114,7 @@ Route smoke-test checklist (re-runnable: `dumpsys gfxinfo closer.app reset` → --- ## Round history (one line each) +- **R12** — FRESH FULL A–J run + fix phase, FLAWLESS (0 open P0–P2): found+fixed **A-201** (P1 Date Match premium bypass — gated via CouplePremiumChecker→Paywall, verified live); 4 async games full 2-device E2E; security cornerstone live-clean (non-member 403 read+write, self-grant 403); smoke 6/6; jank 4.10%; new P3 C-ART-EDGE-002 (hero edges, deferred); C-DARKART-001+C-ART-EDGE-001 held→pruned; Pass A retrospective added (badge≠gate). - **R11** — confirmation round, FLAWLESS (0 open P0–P2): fixed C-DARKART-001 (P2, art follows in-app theme via `LocalAppInDarkTheme` + config-overridden context) + C-ART-EDGE-001 (P3, edge feathering) in shared `BrandIllustration`/`EmptyState`, verified live both decoupled theme directions (system-light+app-Dark→dark art · system-dark+app-Light→light art), 0 FATAL; re-confirmed + pruned the 5 R10 P2 fixes (C-HOME-001/C-NAV-002/C-NAV-003/C-PW-001/C-SEC-001); entrypoint smoke 6/6 green on fresh APK (launcher + 5 notif cold-starts open & stay). Art fixes in working tree; rest committed `2cd0af6`. - **R10** — FULL run A–J + fix phase: 5 P2 found+fixed+verified-live (C-HOME-001 dup card · C-NAV-002 wheel back-stack · C-NAV-003 dup app bar · C-PW-001 dark paywall · C-SEC-001 recovery wrong-store); E-GAME-002 confirmed live (start push+banner+Join) & pruned; concurrency double-start→1 session; security D1–D7 clean; perf/a11y no regression. 0 open P0–P2 (5×P2 pending 1 confirm). - **R7** — security/concurrency deep dive (multi-angle): cornerstone clean; F-RACE-001 found+fixed+verified. 0 new open. diff --git a/ClaudeQAPlan.md b/ClaudeQAPlan.md index 046f8d03..c7dd7058 100644 --- a/ClaudeQAPlan.md +++ b/ClaudeQAPlan.md @@ -12,6 +12,8 @@ For each Pass below, before you start, read the relevant section of [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) — it documents the architecture, the wire-format contracts, the security invariants, and the [Known landmines](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) (bugs that cost real debugging time and are easy to re-introduce). +**This is bidirectional — the manual is a LIVING document, not a read-only reference.** Read it before; **write back to it after.** Whenever a round fixes a bug, changes a contract/flow/gate, or finds the manual stale or missing something, update the manual in the same chunk (see *Where every finding goes*, the *Docs update rule*, and the *MANDATORY retrospective* — all now route durable engineering truth here). Treat it as part of every fix, same as `ClaudeReport.md`/`ClaudeQACoverage.md`. + | Pass | Manual section to read first | |---|---| | A — Couple-shared premium | [Premium-gated features and gate pattern](docs/Engineering_Reference_Manual.md#premium-gated-features-and-gate-pattern) · [Billing](docs/Engineering_Reference_Manual.md#billing) | @@ -34,9 +36,24 @@ For each Pass below, before you start, read the relevant section of [`docs/Engin | **An idea / improvement** — works but could be better, confusing copy, missing affordance, rough-but-not-broken flow, "it'd be great if…", feature idea | **`Future.md`** `## QA` | Short title + what prompted it + suggested improvement | | **New artwork to create** — illustrations, glyphs, image-gen prompts | **`ClaudeBrandingReview.md`** | House-style prompt + placement | | **What got tested + its status** (pass / fail / todo / deferred) | **`ClaudeQACoverage.md`** | Coverage cell (the resume anchor) | +| **Durable engineering knowledge** — a fixed bug's root cause + how it's easy to re-introduce, a new architecture fact / data path / wire-format contract / security invariant / gate pattern, or anything the manual is now stale/missing about | **[`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md)** (esp. [Known landmines and recent fixes](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes)) | New landmine entry (ID + cause + the guard) and/or an updated architecture/gate/flow section | - A branding **defect** (mis-colored, clipped, off-brand, low-contrast art) is a **bug → `ClaudeReport.md`**, not a brand idea — only *new art to create* goes to `ClaudeBrandingReview.md`. +- **ONE canonical home per fact; everywhere else is a pointer (ID/anchor), never a paraphrase.** This is the rule that + keeps the five docs from duplicating each other (and wasting tokens re-stating the same lesson). Route by *purpose*: + the **defect** (repro/severity/status) → `ClaudeReport.md` (transient — prunes to an ID after one confirm); the + **substance** (root cause / why it's fragile / how to not re-introduce it) → the **Engineering Reference Manual** + (permanent, engineer-facing); the **reflex** (how to FIND the class next round) → this `ClaudeQAPlan.md` Pass + (generalized, citing the ID); **coverage status** → `ClaudeQACoverage.md`; **cross-session ops not in the repo** + (accounts, tooling, auth) → `memory/`. State a fact in its home once; elsewhere cite the ID. Don't restate a fix in + four docs. +- **The Engineering Reference Manual is a LIVING document — read it before a pass, write back to it after.** When a + round teaches the codebase something durable (a fixed bug's re-introduction risk, a new/changed architecture fact, + data path, contract, gate, flow, collection/Function/route, or the manual disagreeing with reality), update the manual + in the **same chunk**. **A fix is not complete until its durable substance is in the manual** (see the + MANDATORY-retrospective rule). The report row and the Pass reflex just reference the manual's landmine ID — they don't + re-tell it. - Logging an idea in `Future.md` is **never** a substitute for filing a real defect: if it's broken, it gets an ID in `ClaudeReport.md` too. - Bug lifecycle: filed in `ClaudeReport.md` → fixed → kept **one** confirmation round → pruned to the archived-ID line @@ -172,6 +189,10 @@ surface and reconcile it with `ClaudeQACoverage.md`: - **Docs update rule:** if the inventory finds a page, feature, notification, asset, state, backend path, or edge case missing from the playbook/coverage, update `ClaudeQAPlan.md` and `ClaudeQACoverage.md` before marking the chunk done. If it is product polish, also add it to `Future.md`; if it needs new artwork, add it to `ClaudeBrandingReview.md`. + **And if the discovery is a durable engineering fact (new route/collection/Function/flag/contract, a changed wire + format, a renamed file, a gate/flow that the manual describes wrongly or omits), update + [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) in the same chunk** — the discovery + ritual is exactly when the manual drifts out of date, so reconcile it then, not "later". ## Multi-angle attack mandate (go DEEPER than "does the happy path work") A capability can pass via the UI yet fail when hit directly. Probe each meaningful capability (read/write a private @@ -224,16 +245,27 @@ State lives in **files**, not memory: placement, repeatable bug class, missed edge case, fragile route, confusing state, image/layout failure mode, security angle, or anything else that should be checked every future round — update **this `ClaudeQAPlan.md`** in the relevant pass before ending the chunk. Also add the matching row/cell to `ClaudeQACoverage.md` if it needs recurring - verification. Do this even after the immediate bug is filed/fixed so the lesson or newly discovered surface is not - lost to memory or git history. + verification. **And update [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) when the + discovery is durable engineering truth** (a new architecture fact, data path, contract, gate, flow, or a fixed bug's + re-introduction risk) — the QA plan captures *what to re-test*, the manual captures *what the system is and why it's + fragile*; both are living and both get updated. Do this even after the immediate bug is filed/fixed so the lesson or + newly discovered surface is not lost to memory or git history. - **Learn from every ESCAPED or DEEP bug — MANDATORY retrospective (do this automatically, not only when asked).** Any bug that (a) **escaped a prior round**, (b) needed **non-obvious diagnosis** (a crash, an "opens-and-closes", a "didn't work", an intermittent, a wrong-root-cause first guess), or (c) **recurred** triggers a short retrospective the moment it's fixed — the fix is **not complete** until all four are done: 1. **Add the guard that would have caught it** — a new `qa/` smoke check, a coverage row, or a concrete pass step (e.g. the cold-start bug → `qa/entrypoint_smoke.sh`). If an existing smoke missed it, extend the smoke. - 2. **Record the generalizable inspection lesson** in the relevant pass of this doc AND in `memory/` (how to *find* - this class next time — the reflex, not just the fix). + 2. **Capture the lesson in its ONE canonical home, then link by ID elsewhere — never paraphrase it twice.** Split by + purpose: the **reflex** (how to *find* this class next round) goes in the relevant Pass of **this doc**, written + *generalized* and citing the bug ID as an example (do NOT re-narrate the bug here); the **substance** (root cause + + where it lives now + re-introduction risk + the guard) goes in + [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) → [Known landmines and recent + fixes](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) (and update the matching + architecture/gate/flow section if the fix changed it). The manual is the next engineer's first read; a landmine + that isn't in it will be re-introduced. **Do NOT copy the fix into `memory/`** — per the memory rules, memory holds + only cross-session facts NOT in the repo (emulator↔account map, admin tooling/commands, standing auth, + never-commit); past fixes belong to the manual, so memory just points to the landmine ID if needed. 3. **Name the missing state/angle/entry-point** that let it hide and add it to the multi-angle / state matrices so it's exercised every round (e.g. "real notification tap on an `am kill`'d app", not just `am start`). 4. **Note any wrong turn in diagnosis** so the misstep isn't repeated (e.g. "synthetic test passed while the real @@ -302,6 +334,17 @@ Gated files (for the fix): `ui/play/PlayHubViewModel`, `ui/desiresync/DesireSync `ui/wheel/{CategoryPicker,SpinWheel,WheelHistory}*`, `ui/questions/QuestionPackLibrary*`, `ui/dates/{DateMatch,DateMatches}Screen`, `ui/memorylane/MemoryLaneScreen`, `ui/challenges/ConnectionChallengesScreen`. Also: **any VM/screen calling `EntitlementChecker.isPremium()` directly** (grep for it) is a candidate gate. +- **ENFORCEMENT, not just a checker-usage grep (mandatory — RETROSPECTIVE from A-201, R12).** A feature can carry an + `isPremium` **content flag** + a cosmetic `PremiumBadge` with **NO gate at all** — that's exactly how Date Match + shipped a premium **bypass** (free users could view/like/match ★Premium date ideas; `getDateIdeas()` returned + `DateIdeaSeed.all`, no `CouplePremiumChecker`, badge only). Prior rounds missed it because the audit grepped for + `CouplePremiumChecker` *usages* and found the gated features, never noticing the feature that had **no** checker. + So every round: (1) **grep for `isPremium` / `PremiumBadge` / premium content flags** (`DateIdea.isPremium`, + `category.access=="premium"`, `challenge.isPremium`, …) and for **each** confirm a real enforcement path exists — + a `CouplePremiumChecker` filter OR a paywall-on-interaction — **not just a badge**; (2) **actually TRY TO USE the + premium content as a free user** (like/open/play it), don't just confirm the lock renders — "badge shows" ≠ "gated". + A badge with no enforcement = **premium bypass** (P1+). Inspection lesson: *"shows a Premium badge" is a display + fact, not a gate; prove the gate by using the content while free.* ### Pass B — Games lifecycle (MANDATORY: play each game ONE complete time through ALL different play stayles of the game) Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges, Memory Lane, Spin the Wheel, + Date Match. diff --git a/ClaudeReport.md b/ClaudeReport.md index c3db9b36..e90f7080 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -10,10 +10,10 @@ > to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`. ## Run-state (current) -`R12 (2026-06-27) FRESH FULL ClaudeQAPlan run STARTED (user: "start from the start") | Baseline verified clean: QA free, Sam free (premium revoked), 0 active sessions; build HEAD 2cd0af6 + 3 uncommitted art files installed both emulators (5554=Dark, 5556=Light) | A ✅ (A-201 P1 Date-Match premium bypass) | B ✅ (4 async games full 2-device end-to-end + first-finisher nudge + C-NAV-002 + Ready=Start re-verified live; CC/MemoryLane/DateMatch render-checked; MemoryLane title/preview run-on→Future) | C ✅ (regression-clean vs R10 sweep; Messages/Today/Subscription + organic A/B + R11 decoupled art; NEW C-ART-EDGE-002 P3 direct-call hero hard edges on dark) | D ✅ (LIVE: D1 game answers enc:v1: · D3 non-member 403×4 + member-scoped · D5 self-grant→403; D2/D4/D6/D7 carried R7/R10, rules unchanged) — cornerstone clean | E ▶ in progress (Pass B already verified start/first-finisher/finish triggers→correct partners+copy; cold-start tap smoke running bg bjffibz4v) | F–J todo | Admin: scratchpad/qadmin.js + qa/* + scratchpad/d3neg.js (raw-API). Baseline restored (both free, 0 active). | NEXT: confirm smoke 6/6, then Pass F resilience. Carryover from R11 (still valid): C-DARKART-001 (P2) + C-ART-EDGE-001 (P3) fixed pending 1 confirm; J-OBS (P3) open touch targets.` +`R12 (2026-06-27) FRESH FULL ClaudeQAPlan run STARTED (user: "start from the start") | Baseline verified clean: QA free, Sam free (premium revoked), 0 active sessions; build HEAD 2cd0af6 + 3 uncommitted art files installed both emulators (5554=Dark, 5556=Light) | A ✅ (A-201 P1 Date-Match premium bypass) | B ✅ (4 async games full 2-device end-to-end + first-finisher nudge + C-NAV-002 + Ready=Start re-verified live; CC/MemoryLane/DateMatch render-checked; MemoryLane title/preview run-on→Future) | C ✅ (regression-clean vs R10 sweep; Messages/Today/Subscription + organic A/B + R11 decoupled art; NEW C-ART-EDGE-002 P3 direct-call hero hard edges on dark) | D ✅ (LIVE: D1 game answers enc:v1: · D3 non-member 403×4 + member-scoped · D5 self-grant→403; D2/D4/D6/D7 carried R7/R10, rules unchanged) — cornerstone clean | E ✅ (smoke 6/6 + Pass B triggers) | F ✅ (concurrency+process-death+offline) | G ✅ (security half live) | H (finding C-ART-EDGE-002) | I ✅ (jank 4.10%) | J (J-OBS P3) | **FIX PHASE ✅ — A-201 (P1) FIXED+VERIFIED LIVE** (Date Match LOVE/MAYBE on premium idea → Paywall via CouplePremiumChecker; SKIP passes; 0 FATAL). C-DARKART-001+C-ART-EDGE-001 held → PRUNED. | **R12 COMPLETE — FLAWLESS: 0 open P0–P2.** Remaining: 2 non-blocking P3s (C-ART-EDGE-002 hero edges, J-OBS touch targets). | NEXT (R13): confirm A-201 holds → prune; optional P3 polish. Uncommitted (user commits): R11 art files + `ui/dates/DateMatchViewModel.kt` + `ui/dates/DateMatchScreen.kt` (A-201). Carryover from R11 (still valid): C-DARKART-001 (P2) + C-ART-EDGE-001 (P3) fixed pending 1 confirm; J-OBS (P3) open touch targets.` - **(prior) R11 (2026-06-27) confirmation round COMPLETE — FLAWLESS | Fixed last open P2 C-DARKART-001 (in-app-theme art) + P3 C-ART-EDGE-001 (feathered edges) in shared BrandIllustration/EmptyState; verified live both decoupled theme directions, 0 FATAL | 5 R10 P2 fixes re-confirmed + PRUNED (C-HOME-001/C-NAV-002/C-NAV-003/C-PW-001/C-SEC-001) | entrypoint smoke green on fresh APK | 0 open P0–P2 | Baseline: both FREE, 0 active sessions; build (HEAD 2cd0af6 + 3 uncommitted art files) installed both emulators | NEXT (R12 = next session): confirm C-DARKART-001 + C-ART-EDGE-001 hold → prune; optional J-OBS (P3) touch targets; then declare program-complete for Android. Art fixes in working tree (user commits).` - **Pass B progress (R12):** **1. This or That ✅** — full end-to-end 2-device, NEW style **Light×5 Quick** (R10 was Deep×10): QA started → answered 5 (alt A/B) → first-finisher state; **first-finisher nudge fired** (`partFinishNotifiedAt` set + Sam queue `partner_completed_part` "QA finished their part — your turn to play!"); Sam **joined via Play-hub active state** (at Q1/5, no dup session) → answered all-A → session→completed (0 active); **`partner_finished_game` to BOTH**; reveal **3/5 in sync** symmetric + correct Match/Differ + You/QA attribution on **both** devices (QA dark / Sam light). 0 FATAL. **2. Spin the Wheel ✅** — **Ready=Start session** (R11 change) verified; spun→Stress→10Q; **mixed answer types** (free-text + 1–5 scale) render+accept; Sam **joined active session** via Play hub (Q1, no dup/new spin); both finished→completed (0 active); results "Here's how you each answered" with You/QA free-text + scale; **C-NAV-002 RE-VERIFIED LIVE** — results → BACK → wheel hub (Spin/History), NOT the finished session (the R11-deferred confirm). 0 FATAL. **3. How Well ✅** — QA subject 5·Quick (answered 5 about self), Sam **joined as guesser** ("Predict how QA answered…", asymmetric), guessed 5 → score **5/5 "Perfect read"** + per-Q breakdown (✓ + answer, choice+scale) symmetric both devices; completed (0 active). **4. Desire Sync ✅** (premium, couple-shared via Sam-on) — free QA opened setup (no paywall); QA all-Yes, Sam joined + Y,Y,Y,N,N → reveal **"3 shared desires · 2 kept private"** (only mutual-Yes shown, 2 mismatches "Private") symmetric both devices; completed (0 active); Sam premium restored OFF. **All 4 async session games verified end-to-end.** -- **Uncommitted (user commits):** R11 art fixes only — `app/.../ui/theme/Theme.kt` (LocalAppInDarkTheme CompositionLocal), `app/.../ui/components/BrandIllustration.kt` (theme-correct `-night` variant via config-overridden context + edge feathering), `app/.../ui/components/EmptyState.kt` (routes its illustration through BrandIllustration). Everything else (splash fix, E-GAME-003, foreground banner, qa/ tooling) committed by user in `2cd0af6`. +- **Uncommitted (user commits):** R11 art fixes — `app/.../ui/theme/Theme.kt`, `app/.../ui/components/BrandIllustration.kt`, `app/.../ui/components/EmptyState.kt`; **+ R12 A-201 fix — `app/.../ui/dates/DateMatchViewModel.kt` (CouplePremiumChecker gate + `paywallRequired` event) + `app/.../ui/dates/DateMatchScreen.kt` (navigates to paywall).** Everything else committed in `2cd0af6`. Build installed both emulators. - **Foreground "partner started a game" alert + bold Game Waiting card (R10, user-requested) — DONE+VERIFIED.** When the app is OPEN and a partner starts a game, a prominent **in-app top banner** (" started " + Join) slides in (mirrors chat's in-app surface) instead of the easy-to-miss system notification; **Join → joins the game**. Verified live for all 4 session games: banner name correct (Spin the Wheel / This or That / How Well Do You Know Me / Desire Sync); Join → joins wheel (ToT shows graceful "QA is playing a Wheel game" when types mismatch); **suppressed** when already on that game's screen (added `ActiveGameSessionMonitor.enter/leave` to `WheelSessionViewModel` — the others already had it). Home **"Game waiting"** card redesigned as a **bold purple-gradient hero** (glyph + game name + "Join the game"), promoted to top of "Waiting for you", verified **both themes** → tap **joins the specific game** (not the Play-hub fallback). FCM transport on the emulators is flaky (FcmRetry); the banner was exercised via a data-only high-priority send to the partner token (faithful to the deployed payload). - **Pass C progress (R10):** **Settings family ✅** (dark + Security on light): Settings list, Subscription, Security, Delete account, Notifications all render clean both-relevant-themes; **4 illustrations confirmed in-context** (Security padlock, Delete-account doorway, Quiet-hours moon, + Subscription); back-stack OK (Security/Delete/Notif → BACK → Settings). **Found C-SEC-001 (P2)** — accepter's Recovery phrase disabled + wrong "invite your partner" copy (see Open issues). **Wheel back-stack RE-CHECKED = not a trap:** live wheel session → BACK → spin/setup → BACK → Play hub (2 backs, no dead-end); earlier "stuck" was automation cycling. Leaving mid-wheel leaves a resumable abandoned active session (normal; cleaned). Home ✅ both themes (stale game card gone). - **6. Spin the Wheel ✅** — spun→Physical Intimacy→session; Sam joined at Q1; both answered 10 (A/B + 1–5 scale + free-text); per-Q You/QA breakdown renders; completed, 0 active. (Helper `wheel_drive.py` handles mixed types; free-text Qs hide "Next" behind IME.) @@ -34,30 +34,25 @@ | Severity | Open | Fixed (pending 1 confirm) | |---|---|---| | P0 | 0 | 0 | -| P1 | **1** (A-201) | 0 | -| P2 | **0** | **1** (C-DARKART-001) | -| P3 | **2** (J-OBS, C-ART-EDGE-002) | **1** (C-ART-EDGE-001) | +| P1 | 0 | **1** (A-201) | +| P2 | 0 | 0 | +| P3 | **2** (J-OBS, C-ART-EDGE-002) | 0 | -## Issues — R12 (1 open P1 [A-201 date-match premium bypass] · 0 open P2 · 1×P2 fixed pending 1 confirm [C-DARKART-001] · 1 open P3 [J-OBS] · 1×P3 fixed pending 1 confirm [C-ART-EDGE-001]) -> R11 fixed the two open art issues in the shared `BrandIllustration`/`EmptyState` helpers and verified both live -> on **both** decoupled theme directions, 0 FATAL. The 5 R10 P2 fixes were re-confirmed this round and **pruned** to the -> archived-ID line below (detail in git `9c84c36`). **Fixes for the two art issues are in the working tree (user commits).** -> Fix summary: C-DARKART-001 — `LocalAppInDarkTheme` CompositionLocal (set in `CloserTheme`) drives a config-overridden -> context (`createConfigurationContext` with `UI_MODE_NIGHT_*` from the in-app theme) so `-night` art follows the app's -> own theme, not the system. C-ART-EDGE-001 — tiled art feathers its 4 edges to transparent (`graphicsLayer{Offscreen}` + -> `drawWithContent` `BlendMode.DstIn` linear gradients) instead of a hard `clip` + `border`; `EmptyState` now routes its -> illustration through `BrandIllustration` so both fixes apply everywhere from one place. +## Issues — R12 (0 open P0–P2 = FLAWLESS · A-201 P1 fixed+verified-live pending 1 confirm · 2 open P3 [J-OBS, C-ART-EDGE-002]) +> R12 was a FRESH FULL A–J run. Found A-201 (P1, Date Match premium bypass) — **fixed + verified live this round** (gated +> via `CouplePremiumChecker`). C-DARKART-001 (P2) + C-ART-EDGE-001 (P3), fixed in R11, held through R12's visual sweep and +> are **pruned** to the archived-ID line below (detail in git / working tree). New P3 **C-ART-EDGE-002** (direct-call hero +> hard edges) is deferred polish. Remaining open = 2 non-blocking P3s. **A-201 fix + R11 art fixes are in the working tree +> (user commits).** | ID | Sev | Area | Description | Repro | Suggested fix | Status | |---|---|---|---|---|---|---| -| A-201 | P1 | Premium / Date Match bypass (Pass A, R12) | **Premium date ideas are NOT gated — free users can view, swipe, like and match them with no paywall.** `DateIdea.isPremium` is documented as "requires an active premium entitlement," but `DateMatchRepositoryImpl.getDateIdeas()` returns `DateIdeaSeed.all` (premium ideas included, **no entitlement filter**), `DateMatchViewModel` loads them with no `CouplePremiumChecker`, and `DateMatchScreen` only renders a cosmetic `PremiumBadge()` — no lock overlay, no paywall on like/super-like. So the premium tier for Date Match is unenforced. **Escaped prior Pass A rounds** (which asserted "all gates use CouplePremiumChecker" — Date Match has NO gate). (Plan buckets "premium bypass" as P0; filed P1 as it's a content-tier subset, no security/data impact.) | 5554 (QA **free**): Play → Date Match → reject through deck → reach a **★ Premium** idea ("night camping getaway") → Like (heart) → **no paywall**, swipe accepted, deck advanced to the next premium idea ("Zipline canopy tour"); 0 FATAL, no `PERMISSION_DENIED`. | Gate premium date ideas through `CouplePremiumChecker`: either filter `isPremium` ideas out of a free couple's deck, OR intercept like/super-like on a premium idea → route to Paywall when neither partner is premium (couple-shared). Mirror the Desire Sync / Question-pack gate. | **Open (P1)** | -| C-DARKART-001 | P2 | Theme / dark-mode art (Pass C) | **Dark-mode illustrations didn't follow the IN-APP theme switch — only the system dark mode.** The in-app toggle (Settings → Appearance → Dark) swapped Compose colors via `CloserTheme(darkTheme=…)` but had no config `uiMode` override, so `painterResource` + the `drawable-night-nodpi/` variants resolved off the **system** `uiMode` → app-Dark on a light-mode phone showed **dark UI + light illustrations**. Affected all 12 `-night` illustrations. | 5554: `cmd uimode night no` (system light) → Settings → Appearance → **Dark** → before fix Security showed the **light** padlock tile on a dark screen. | **DONE:** `BrandIllustration` loads the drawable through `context.createConfigurationContext(cfg)` with `UI_MODE_NIGHT_*` set from `LocalAppInDarkTheme` (provided in `CloserTheme`). **Verified live R11 both directions:** 5554 system-light + app-Dark → **dark aubergine** art on dark screen (Security + Art-preview gallery); 5556 system-dark + app-Light → **light pastel** art on light screen; 0 FATAL, both apps alive. | **Fixed + verified live R11 (working tree; user commits)** | -| C-ART-EDGE-001 | P3 | Art / edge treatment (Pass C+H) | **Displayed illustrations had hard edges instead of fading into the screen** — `BrandIllustration` hard-`clip`ped art to `RoundedCornerShape` + a hairline `border`, and `EmptyState` rendered raw `painterResource`, so the near-white tile read as a crisp rounded-rectangle boundary (esp. on dark). | Any art screen: hard tile edge/outline instead of feathering. | **DONE:** tiled art now feathers its 4 edges to transparent (`graphicsLayer{compositingStrategy=Offscreen}` + `drawWithContent` `BlendMode.DstIn` linear gradients, ~14% inset); `clip`+`border` removed; `EmptyState` routes through `BrandIllustration`. **Verified live R11:** Art-preview gallery + Security padlock melt softly into the surface on both themes; transparent art (`tile=false`) unaffected. | **Fixed + verified live R11 (working tree; user commits)** | +| A-201 | P1 | Premium / Date Match bypass (Pass A, R12) | **Premium date ideas are NOT gated — free users can view, swipe, like and match them with no paywall.** `DateIdea.isPremium` is documented as "requires an active premium entitlement," but `DateMatchRepositoryImpl.getDateIdeas()` returns `DateIdeaSeed.all` (premium ideas included, **no entitlement filter**), `DateMatchViewModel` loads them with no `CouplePremiumChecker`, and `DateMatchScreen` only renders a cosmetic `PremiumBadge()` — no lock overlay, no paywall on like/super-like. So the premium tier for Date Match is unenforced. **Escaped prior Pass A rounds** (which asserted "all gates use CouplePremiumChecker" — Date Match has NO gate). (Plan buckets "premium bypass" as P0; filed P1 as it's a content-tier subset, no security/data impact.) | 5554 (QA **free**): Play → Date Match → reject through deck → reach a **★ Premium** idea ("night camping getaway") → Like (heart) → **no paywall**, swipe accepted, deck advanced to the next premium idea ("Zipline canopy tour"); 0 FATAL, no `PERMISSION_DENIED`. | Gate premium date ideas through `CouplePremiumChecker`: either filter `isPremium` ideas out of a free couple's deck, OR intercept like/super-like on a premium idea → route to Paywall when neither partner is premium (couple-shared). Mirror the Desire Sync / Question-pack gate. | **Fixed + verified live R12 (working tree; user commits)** — `DateMatchViewModel` injects `CouplePremiumChecker`; `swipeCurrent` intercepts LOVE/MAYBE on a premium idea when neither partner is premium → emits `paywallRequired` → `DateMatchScreen` navigates to Paywall; SKIP still passes; deck stays on the card. **Verified:** free QA Love on ★Premium "night camping" → Paywall ("Go deeper together"), no swipe; SKIP → advances, no paywall; 0 FATAL. (Server already blocked real self-grant per D5 — so no entitlement was ever exposed.) | | C-ART-EDGE-002 | P3 | Art / hard edges on direct-call heroes (Pass C, R12) | **Hero illustrations rendered via direct `painterResource` (not the shared `BrandIllustration`) still show hard edges on dark theme** — the R11 C-ART-EDGE-001 feather fix only covered `BrandIllustration`/`EmptyState`. The Today "Weekend Side Quest" daily-question hero (light/pink art) renders as a **bright rounded-rect block with a hard bottom edge on the dark screen**. These direct-call heroes have **no `-night` variant** either. Likely same class: paywall couple art, onboarding, Home tonight-prompt/partner-activation, spin-wheel hero, pack art (all direct `painterResource(illustration_*)`). Matches the user's "all images should fade into the screen" request (only partially satisfied by C-ART-EDGE-001). | 5554 (dark): Today tab → daily question → "Weekend Side Quest" hero shows a hard bright edge against the dark card. | Route these heroes through `BrandIllustration` (gains feather + theme-variant), OR apply the same `featherEdges()` treatment at each call site; consider `tile`/`hero` variants. Verify each direct `painterResource(R.drawable.illustration_*)` site listed in the R12 grep. | **Open (P3)** | | J-OBS | P3 | A11y / touch targets | A few conversation icon-buttons measure **~42–45dp wide** (48dp tall) — single-axis marginal miss of the 48dp target; fully operable. Most controls are 48dp. | Pass J: uiautomator bounds on conversation → 2–3 clickables `<126px` wide. | Bump those icon-buttons to 48dp min (e.g. `Modifier.minimumInteractiveComponentSize()` / `size(48.dp)`). | **Open (P3, non-blocking)** | ## Resolved & confirmed (archived — full detail in git history) -A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · **C-HOME-001** · **C-NAV-001** · **C-NAV-002** · **C-NAV-003** · **C-PW-001** · **C-SEC-001** · D-001 · E-001 · E-002 · E-003 · **E-GAME-002** · **E-GAME-003** · E-OBS · F-OBS · F-RACE-001 · **I-001** · **I-002** — all fixed and re-verified (R11 pruned the 5 R10 P2 fixes — C-HOME-001 single Home card · C-NAV-002 `popUpTo(WHEEL_SESSION){inclusive}` present + R10-live · C-NAV-003 single app bar re-confirmed live · C-PW-001 dark paywall pills legible re-confirmed live · C-SEC-001 recovery row active for accepter re-confirmed live — all committed in `9c84c36`; E-GAME-003 `onGamePartFinished` deployed + committed `2cd0af6`) (E-GAME-002 confirmed live R10: `startNotifiedAt` set + partner_started_game queued to right partner + foreground banner + Join→joined active ToT at same Q1; commits 6e79cd9/38fdc6d) (commits in history; F-RACE-001 re-confirmed R8; **I-001** query→`whereIn(dayKeys)` + **I-002** Long-score→`Number.toInt()`, fixed `ab29f6b`, re-confirmed live R9: 0 outcomes denials/CCE). Pruned per the one-confirmation-round rule. (C-OBS / `outcomes` list / SubscriptionScreen per-user gate = investigated, **not bugs**.) +A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · **C-DARKART-001** · C-DS-001 · **C-ART-EDGE-001** · **C-HOME-001** · **C-NAV-001** · **C-NAV-002** · **C-NAV-003** · **C-PW-001** · **C-SEC-001** · D-001 · E-001 · E-002 · E-003 · **E-GAME-002** · **E-GAME-003** · E-OBS · F-OBS · F-RACE-001 · **I-001** · **I-002** — all fixed and re-verified (R12 pruned C-DARKART-001 [in-app-theme `-night` art] + C-ART-EDGE-001 [feathered edges] — fixed R11, held through R12 visual sweep; in working tree) (R11 pruned the 5 R10 P2 fixes — C-HOME-001 single Home card · C-NAV-002 `popUpTo(WHEEL_SESSION){inclusive}` present + R10-live · C-NAV-003 single app bar re-confirmed live · C-PW-001 dark paywall pills legible re-confirmed live · C-SEC-001 recovery row active for accepter re-confirmed live — all committed in `9c84c36`; E-GAME-003 `onGamePartFinished` deployed + committed `2cd0af6`) (E-GAME-002 confirmed live R10: `startNotifiedAt` set + partner_started_game queued to right partner + foreground banner + Join→joined active ToT at same Q1; commits 6e79cd9/38fdc6d) (commits in history; F-RACE-001 re-confirmed R8; **I-001** query→`whereIn(dayKeys)` + **I-002** Long-score→`Number.toInt()`, fixed `ab29f6b`, re-confirmed live R9: 0 outcomes denials/CCE). Pruned per the one-confirmation-round rule. (C-OBS / `outcomes` list / SubscriptionScreen per-user gate = investigated, **not bugs**.) ## Security cornerstone — clean (Pass D, deep dive, Round 7) - **D1 at-rest:** chat text + `lastMessagePreview` + all 4 game-answer collections (ToT / How Well / Desire Sync / Wheel, both users) + Memory Lane capsules + date-swipe actions = `enc:v1:`. No plaintext content; only metadata in clear. @@ -100,6 +95,14 @@ also set an `ACTION_VIEW` + `closer://` **data Uri** on the notification intent: path (no Uri) loads results; **warm tap from Settings now routes** (was the stuck case). ## Round history (one line each) +- **R12 (2026-06-27) — FRESH FULL A–J run + fix phase, FLAWLESS (0 open P0–P2).** Found **A-201 (P1): Date Match + premium ideas ungated** (free users could like/match ★Premium ideas — `getDateIdeas()=all`, no checker, badge only; + escaped prior Pass A rounds) → **fixed + verified live** (gated LOVE/MAYBE via `CouplePremiumChecker`→Paywall, SKIP + passes). Pass B: all 4 async games full 2-device E2E (ToT/Wheel/HowWell/DesireSync) + first-finisher nudge + C-NAV-002 + + Ready=Start re-verified live. Pass D LIVE clean: non-member 403 (read+write), self-grant→403, game answers enc:v1:. + Pass E smoke 6/6. Pass I jank 4.10% (art change perf-safe). New P3 C-ART-EDGE-002 (direct-call hero hard edges, + deferred). C-DARKART-001+C-ART-EDGE-001 (R11) held → pruned. Retrospective added to Pass A (badge≠gate; try to USE + premium content as a free user). Fixes in working tree (user commits). - **R11 (2026-06-27) — confirmation round, FLAWLESS (0 open P0–P2).** Fixed the last open P2 **C-DARKART-001** (dark-mode art now follows the in-app theme: `LocalAppInDarkTheme` CompositionLocal in `CloserTheme` → `BrandIllustration` loads the `-night` drawable via a `createConfigurationContext` whose `UI_MODE_NIGHT_*` comes from the app theme, not the system) and diff --git a/Future.md b/Future.md index 031657f2..9827edb5 100644 --- a/Future.md +++ b/Future.md @@ -7,6 +7,11 @@ Non-blocking ideas: things that work today but could be better, plus feature ide Improvement & feature ideas surfaced while QA-testing as a consumer (each works today — none are defects). +- **Memory Lane capsule list: separate title from body preview.** (R12 Pass B) In the capsule list each row runs + the title straight into the body preview with no separator/space/style break (e.g. "QA Memory Test" + "Remember this + QA round…" renders as one run-on string). Give the preview its own line / muted style, or a separator, so title and + body read distinctly. (Partly confounded by R12 adb-typed test titles — re-check with clean data; cosmetic only.) + - **Consistent brand glyphs across game cards + waiting/notification surfaces.** _(Blocked: needs the generated G-set art — image generation is the user's step per `ClaudeBrandingReview.md`.)_ Game cards (Play hub), the WaitingForPartner screen, and notifications mix Material icons with brand art. A small diff --git a/app/src/main/java/app/closer/ui/components/BrandIllustration.kt b/app/src/main/java/app/closer/ui/components/BrandIllustration.kt index f9c66787..e65765e6 100644 --- a/app/src/main/java/app/closer/ui/components/BrandIllustration.kt +++ b/app/src/main/java/app/closer/ui/components/BrandIllustration.kt @@ -1,28 +1,46 @@ package app.closer.ui.components +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Canvas as AndroidCanvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.compose.foundation.Image -import androidx.compose.foundation.border -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import app.closer.ui.theme.LocalAppInDarkTheme /** * Brand illustration that reads on BOTH the light and dark themes. * - * Most of the generated empty-state / header illustrations ship with a soft - * near-white background; rendered raw on the dark (aubergine) theme that background - * would float as a pale block. Clipping to a generous rounded tile with a hairline - * outline turns it into an intentional, modern illustration card on either surface. + * Two things the raw `painterResource` path got wrong, both fixed here once for every screen: * - * Transparent art (e.g. the pairing-success celebration) should pass [tile] = false - * so it floats freely with no card edge. + * 1. **Theme-correct variant (C-DARKART-001).** `painterResource` resolves `-night` drawables off the + * Android *system* `uiMode`, but the app has its own in-app theme toggle (Settings → Appearance). + * When the two disagree (app set to Dark on a light-mode phone) the art used to stay light on a dark + * screen. We load the drawable through a config-overridden context driven by [LocalAppInDarkTheme] + * so the `-night` variant follows the IN-APP theme. + * + * 2. **Soft edges (C-ART-EDGE-001).** The generated illustrations ship with a soft near-white tile + * background; rendered as a hard-clipped rounded rectangle with a border they read as a floating + * card with a crisp edge. Tiled art now feathers its four edges to transparent so it melts into the + * surface on either theme. + * + * Transparent art (e.g. the pairing-success celebration) should pass [tile] = false so it floats + * freely with no feathering. */ @Composable fun BrandIllustration( @@ -30,20 +48,61 @@ fun BrandIllustration( contentDescription: String?, modifier: Modifier = Modifier, tile: Boolean = true, - cornerRadius: Dp = 28.dp, + contentScale: ContentScale = ContentScale.Fit, ) { - val shape = RoundedCornerShape(cornerRadius) - val shaped = if (tile) { - modifier - .clip(shape) - .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.10f), shape) - } else { - modifier + val context = LocalContext.current + val dark = LocalAppInDarkTheme.current + // Resolve the drawable through a config-overridden context so the -night variant follows the + // IN-APP theme (Settings → Appearance), not the system uiMode (C-DARKART-001). + val painter = remember(res, dark) { + val cfg = Configuration(context.resources.configuration).apply { + uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or + if (dark) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO + } + val themed = context.createConfigurationContext(cfg) + val bitmap = ContextCompat.getDrawable(themed, res)?.toImageBitmap() + ?: Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888).asImageBitmap() + BitmapPainter(bitmap) } + val shaped = if (tile) modifier.featherEdges() else modifier Image( - painter = painterResource(res), + painter = painter, contentDescription = contentDescription, - contentScale = ContentScale.Fit, + contentScale = contentScale, modifier = shaped, ) } + +/** + * Fade the four edges of tiled art to transparent so the illustration melts into the surface instead + * of showing a hard rounded-rectangle tile edge / border (C-ART-EDGE-001). Rendered offscreen so the + * `DstIn` alpha gradients blend against the art, not the screen. + */ +private fun Modifier.featherEdges(): Modifier = this + .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } + .drawWithContent { + drawContent() + val fx = size.width * 0.14f + val fy = size.height * 0.14f + drawRect(Brush.horizontalGradient(listOf(Color.Transparent, Color.White), 0f, fx), blendMode = BlendMode.DstIn) + drawRect( + Brush.horizontalGradient(listOf(Color.White, Color.Transparent), size.width - fx, size.width), + blendMode = BlendMode.DstIn, + ) + drawRect(Brush.verticalGradient(listOf(Color.Transparent, Color.White), 0f, fy), blendMode = BlendMode.DstIn) + drawRect( + Brush.verticalGradient(listOf(Color.White, Color.Transparent), size.height - fy, size.height), + blendMode = BlendMode.DstIn, + ) + } + +private fun Drawable.toImageBitmap() = run { + if (this is BitmapDrawable) bitmap?.let { return@run it.asImageBitmap() } + val w = intrinsicWidth.coerceAtLeast(1) + val h = intrinsicHeight.coerceAtLeast(1) + val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) + val canvas = AndroidCanvas(bmp) + setBounds(0, 0, canvas.width, canvas.height) + draw(canvas) + bmp.asImageBitmap() +} diff --git a/app/src/main/java/app/closer/ui/components/EmptyState.kt b/app/src/main/java/app/closer/ui/components/EmptyState.kt index d9496e4a..bc6649c3 100644 --- a/app/src/main/java/app/closer/ui/components/EmptyState.kt +++ b/app/src/main/java/app/closer/ui/components/EmptyState.kt @@ -2,20 +2,16 @@ package app.closer.ui.components import app.closer.ui.theme.closerCardColor import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -39,13 +35,11 @@ fun EmptyState( verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md) ) { illustrationResId?.let { resId -> - Image( - painter = painterResource(resId), + BrandIllustration( + res = resId, contentDescription = null, + modifier = Modifier.size(illustrationSize), contentScale = ContentScale.Crop, - modifier = Modifier - .size(illustrationSize) - .clip(RoundedCornerShape(CloserRadii.Tile)) ) } Text( diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt index 01d965fc..0ccf3ebf 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -77,6 +78,11 @@ fun DateMatchScreen( ) { val state by viewModel.uiState.collectAsState() + // A-201: a free couple expressing interest in a premium idea routes to the paywall (couple-shared). + LaunchedEffect(Unit) { + viewModel.paywallRequired.collect { onNavigate("paywall") } + } + DateMatchContent( state = state, onLove = viewModel::loveCurrent, diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchViewModel.kt b/app/src/main/java/app/closer/ui/dates/DateMatchViewModel.kt index 90c9de6e..665d9aa2 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchViewModel.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchViewModel.kt @@ -3,6 +3,7 @@ package app.closer.ui.dates import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.closer.core.billing.CouplePremiumChecker import app.closer.domain.model.DateIdea import app.closer.domain.model.DateMatch import app.closer.domain.model.DateSwipe @@ -14,8 +15,11 @@ import app.closer.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine @@ -32,7 +36,8 @@ data class DateMatchUiState( val ownSwipes: Map = emptyMap(), val matches: List = emptyList(), val currentIndex: Int = 0, - val justMatched: DateMatch? = null + val justMatched: DateMatch? = null, + val hasPremium: Boolean = false ) { val currentIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex) val nextIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex + 1) @@ -46,14 +51,25 @@ class DateMatchViewModel @Inject constructor( private val repository: DateMatchRepository, private val authRepository: AuthRepository, private val coupleRepository: CoupleRepository, - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val premiumChecker: CouplePremiumChecker ) : ViewModel() { private val _uiState = MutableStateFlow(DateMatchUiState()) val uiState: StateFlow = _uiState.asStateFlow() + // One-shot: emitted when a free couple tries to LOVE/MAYBE a premium date idea, so the screen + // routes to the paywall instead of recording the swipe (A-201 — couple-shared premium gate). + private val _paywallRequired = MutableSharedFlow(extraBufferCapacity = 1) + val paywallRequired: SharedFlow = _paywallRequired.asSharedFlow() + init { loadDateMatch() + viewModelScope.launch { + premiumChecker.isPremium().collect { premium -> + _uiState.update { it.copy(hasPremium = premium) } + } + } } private fun loadDateMatch() { @@ -121,6 +137,17 @@ class DateMatchViewModel @Inject constructor( val userId = _uiState.value.currentUserId ?: return viewModelScope.launch { + // Couple-shared premium gate (A-201): expressing positive interest (LOVE/MAYBE) in a + // premium idea requires premium for EITHER partner; SKIP always passes. Checked at decision + // time so a freshly-loaded deck can't race the entitlement flow. The deck does NOT advance + // when gated — the user lands on the paywall and the same premium card is still there on return. + if (action != SwipeAction.SKIP && current.isPremium) { + val premium = runCatching { premiumChecker.hasPremium() }.getOrDefault(false) + if (!premium) { + _paywallRequired.tryEmit(Unit) + return@launch + } + } _uiState.update { it.copy(justMatched = null) } val result = repository.recordSwipe(coupleId, userId, current.id, action) result.fold( diff --git a/app/src/main/java/app/closer/ui/theme/Theme.kt b/app/src/main/java/app/closer/ui/theme/Theme.kt index 2b661d21..dccdebea 100644 --- a/app/src/main/java/app/closer/ui/theme/Theme.kt +++ b/app/src/main/java/app/closer/ui/theme/Theme.kt @@ -6,8 +6,19 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color +/** + * The app's OWN dark-theme state (from Settings → Appearance), independent of the Android system + * `uiMode`. `CloserTheme(darkTheme)` only swaps the Compose color scheme, while `painterResource` + * and the `drawable-night*` variants resolve off the system `uiMode` — so when the in-app theme and + * the system disagree, art mismatches the UI. Art helpers read this to load the correct `-night` + * variant per the app's own setting (C-DARKART-001). + */ +val LocalAppInDarkTheme = staticCompositionLocalOf { false } + @Composable fun CloserTheme( darkTheme: Boolean = isSystemInDarkTheme(), @@ -15,11 +26,13 @@ fun CloserTheme( ) { val colors = if (darkTheme) darkColors else lightColors - MaterialTheme( - colorScheme = colors, - typography = Typography, - content = content - ) + CompositionLocalProvider(LocalAppInDarkTheme provides darkTheme) { + MaterialTheme( + colorScheme = colors, + typography = Typography, + content = content + ) + } } // Brand palette. Keep accent color in containers and controls; large surfaces stay quiet.