chore: R12 working tree — QA docs, brand illustration updates, date-match paywall routing, theme tweaks
This commit is contained in:
parent
361eff18e3
commit
9f09ebbc67
|
|
@ -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
|
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.
|
+ 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
|
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`
|
`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:
|
(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** |
|
| 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.
|
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 done:** A7 pack art = **N/A** (all 10 packs already have `pack_art_*`). **G-set glyph art generated
|
||||||
(not generated; tracked in `Future.md` `## QA`). Empty/match/pairing states that need empty-or-new data weren't
|
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`.
|
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
|
- 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.
|
rather than a hard rectangular background; export PNG with transparency where noted.
|
||||||
|
|
||||||
**Existing assets (reuse before generating):** `illustration_couple_onboarding`, `_invite`, `_history`, `_paywall`,
|
**Existing assets (reuse before generating):** the generated A1-A12 illustration set above, `illustration_couple_*`,
|
||||||
`_subscription`, `illustration_tonight_partner_prompt`, `illustration_partner_activation`,
|
`illustration_daily_question`, `illustration_tonight_partner_prompt`, `illustration_partner_activation`,
|
||||||
`illustration_reveal_celebration`, `illustration_streak_milestone`, `illustration_together_empty`,
|
`illustration_reveal_celebration`, `illustration_streak_milestone`, `illustration_together_empty`, all 10
|
||||||
`pack_art_{deep_reflection,family_commitment,home_life,trust_repair,money_values}`, `particle_heart`, `particle_petal`.
|
`pack_art_*` assets, `particle_heart`, and `particle_petal`. These are Android assets now; only generate new art when a
|
||||||
NOTE: most live only in iOS / `drawable-nodpi`; several screens below need the **Android copy** wired up, not new art.
|
future QA pass finds a specific missing surface or replacement-worthy defect.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Screen-by-screen audit
|
## 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 |
|
| 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 🔤 |
|
| 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) |
|
| 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) |
|
| 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: invite (create code) | `illustration_couple_invite` wired | ✅ |
|
||||||
| Pair: accept code / pairing success | minimal | 🎨 **A1** pairing-success celebration |
|
| Pair: accept code / pairing success | `illustration_pairing_success` wired | ✅ |
|
||||||
| Home (paired) | cards, warm copy | ✅ good; ensure card icons use brand glyphs 🔤 |
|
| Home (paired) | cards, warm copy | ✅ good; ensure card icons use brand glyphs 🔤 |
|
||||||
| Home (unpaired "bring your person in") | couple art present | ✅ on-brand |
|
| Home (unpaired "bring your person in") | couple art present | ✅ on-brand |
|
||||||
| Today / daily question | clean card | ✅; reveal moment is the place for art (below) |
|
| 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 reveal (mutual) | `illustration_reveal_celebration` wired | ✅ |
|
||||||
| Answer history | list | ➕ empty state → wire `illustration_couple_history`; 🎨 **A2** if none |
|
| Answer history | `illustration_answer_history_empty` wired | ✅ |
|
||||||
| Play hub | card list, game glyphs | ✅; verify each game card has a consistent brand glyph 🔤 |
|
| 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 |
|
| 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 | ✅ |
|
| How Well / Desire Sync intro | icon + copy | ✅ |
|
||||||
| Spin the Wheel | nice wheel art | ✅ wheel is on-brand |
|
| Spin the Wheel | nice wheel art | ✅ wheel is on-brand |
|
||||||
| Wheel complete / results | text reveal | ➕ celebration header (reuse particles) |
|
| 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 |
|
| 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 (list) | `illustration_memory_lane_capsule` empty state wired | ✅ |
|
||||||
| Memory Lane (sealed capsule) | lock + date | ➕ use capsule art (A4) on the sealed card |
|
| Memory Lane (sealed capsule) | lock + date | ➕ optional: reuse capsule art on sealed-card detail, no new art |
|
||||||
| Date Match (deck) | clean cards | ✅ |
|
| 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 |
|
| Plan Date / Date Builder | form | 🔤 small date-card glyph; ✅ otherwise |
|
||||||
| Bucket List (empty) | list | 🎨 **A6** “save ideas together” empty state |
|
| Bucket List (empty) | `illustration_bucket_list_empty` wired | ✅ |
|
||||||
| Question Packs (library) | `pack_art_*` present | ✅; 🎨 **A7** packs needing art (any pack without `pack_art_*`) |
|
| Question Packs (library) | all 10 `pack_art_*` assets present | ✅ |
|
||||||
| Messages (inbox empty) | list | 🎨 **A8** “no messages yet” (two soft bubbles, no text) |
|
| Messages (inbox empty) | `illustration_messages_empty` wired | ✅ |
|
||||||
| Conversation | chat | ✅ keep clean; quiet-hours state → A9 |
|
| Conversation | chat | ✅ keep clean; quiet-hours art is in settings |
|
||||||
| Past Games (empty/list) | list | ➕ small replay/heart glyph; 🎨 **A10** if empty state bare |
|
| Past Games (empty/list) | `illustration_past_games_empty` wired | ✅ |
|
||||||
| Your Progress / Activity | stats | 🔤 brand-colored charts; reuse `streak_milestone` for milestones |
|
| Your Progress / Activity | stats | 🔤 brand-colored charts; reuse `streak_milestone` for milestones |
|
||||||
| Paywall / Subscription | couple art present | ✅ strong (couple illustration + “one subscription for both”) |
|
| Paywall / Subscription | couple art present | ✅ strong (couple illustration + “one subscription for both”) |
|
||||||
| WaitingForPartner | category glyph + copy | ✅ on-brand; ensure glyph per game type |
|
| 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 🔤 |
|
| 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 |
|
| Privacy & Terms | text | 🔤 small lock glyph header |
|
||||||
| Delete account | warning copy | 🎨 **A12** calm export/delete privacy scene (no alarm imagery) |
|
| Delete account | `illustration_account_deletion_goodbye` wired | ✅ |
|
||||||
| Quiet hours (settings) | toggle | 🎨 **A9** moon/window/two-muted-phones |
|
| Quiet hours (settings) | `illustration_quiet_hours` wired | ✅ |
|
||||||
| Notifications (system) | fallback glyph | 🎨 **G-set** monochrome notification glyph (heart/paired-card) |
|
| 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.*
|
Do not regenerate completed illustration art unless a future QA pass logs a specific defect that requires replacement.
|
||||||
> Scene: the Closer mark resolving into place — a soft-pink upper C-arc and a lavender lower sweep curving together to
|
Always clean up completed art items: once an asset is generated, wired, and verified, remove its prompt from this active
|
||||||
> enclose a heart-shaped space with a small keyhole at its center — with a gentle burst of small floating hearts and
|
backlog and update the audit table/status notes so only unfinished work remains.
|
||||||
> petals around it. Convey "you're connected now." Calm, joyful, no text. (White keyhole if on a dark background.)
|
|
||||||
|
|
||||||
**A2 · Answer history empty state** — *1:1.*
|
**Generated G-set files:** `glyph_closer_mark`, `glyph_paired_cards`, `glyph_daily_card`, `glyph_sealed_answer`,
|
||||||
> Scene: a small soft journal/photo-album, slightly open, with two paired cards tucked inside and a few faint floating
|
`glyph_memory_capsule`, `glyph_date_card_heart`, `glyph_quiet_hours_moon`, `glyph_couple_premium`,
|
||||||
> hearts; a lavender sprig beside it. Conveys "your shared moments will collect here." Quiet and inviting.
|
`glyph_export_data`, `glyph_delete_account`.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes for the asset hand-off
|
## Notes for the asset hand-off
|
||||||
- Match filenames to the existing scheme: `illustration_<name>.png` (nodpi), `pack_art_<pack>.png`, `glyph_<name>.xml`,
|
- Match filenames to the existing scheme: `illustration_<name>.png` (nodpi), `pack_art_<pack>.png`, `glyph_<name>.xml`,
|
||||||
`particle_<name>.png`. Provide @1x in `drawable-nodpi/` (illustrations) or density buckets where the existing asset has them.
|
`particle_<name>.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):
|
- For the generated G-set, use the source SVGs in `docs/brand/generated-art/glyphs/source-svg/` for review and the
|
||||||
history, invite, daily-question, together-empty, partner-activation already exist on iOS.
|
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
|
- 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.
|
re-export store graphics per `docs/brand/visual-identity.md` if the palette/mark changed.
|
||||||
</content>
|
</content>
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@
|
||||||
## Status at a glance
|
## Status at a glance
|
||||||
| Pass | Coverage | Status |
|
| 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) |
|
| 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 |
|
| 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) |
|
| 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** |
|
| 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 | R10: concurrency double-start→1 session · process-death→clean+FCM re-register · offline(R9) | ✅ pass · time-travel + deletion-cascade deferred |
|
| 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 |
|
| 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` |
|
| 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 |
|
| 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)
|
## 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`.
|
- **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).
|
- **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.
|
- **R7** — security/concurrency deep dive (multi-angle): cornerstone clean; F-RACE-001 found+fixed+verified. 0 new open.
|
||||||
|
|
|
||||||
|
|
@ -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).
|
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 |
|
| 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) |
|
| 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 |
|
| **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 |
|
| **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) |
|
| **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
|
- 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`.
|
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
|
- 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.
|
`ClaudeReport.md` too.
|
||||||
- Bug lifecycle: filed in `ClaudeReport.md` → fixed → kept **one** confirmation round → pruned to the archived-ID line
|
- 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
|
- **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.
|
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`.
|
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")
|
## 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
|
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,
|
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
|
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
|
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
|
verification. **And update [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) when the
|
||||||
lost to memory or git history.
|
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).**
|
- **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",
|
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
|
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:
|
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
|
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.
|
(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*
|
2. **Capture the lesson in its ONE canonical home, then link by ID elsewhere — never paraphrase it twice.** Split by
|
||||||
this class next time — the reflex, not just the fix).
|
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
|
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`).
|
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
|
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/wheel/{CategoryPicker,SpinWheel,WheelHistory}*`, `ui/questions/QuestionPackLibrary*`,
|
||||||
`ui/dates/{DateMatch,DateMatches}Screen`, `ui/memorylane/MemoryLaneScreen`, `ui/challenges/ConnectionChallengesScreen`.
|
`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.
|
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)
|
### 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.
|
Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges, Memory Lane, Spin the Wheel, + Date Match.
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@
|
||||||
> to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`.
|
> to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`.
|
||||||
|
|
||||||
## Run-state (current)
|
## 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).`
|
- **(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.**
|
- **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** ("<partner> started <Game>" + 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).
|
- **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** ("<partner> started <Game>" + 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).
|
- **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.)
|
- **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) |
|
| Severity | Open | Fixed (pending 1 confirm) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| P0 | 0 | 0 |
|
| P0 | 0 | 0 |
|
||||||
| P1 | **1** (A-201) | 0 |
|
| P1 | 0 | **1** (A-201) |
|
||||||
| P2 | **0** | **1** (C-DARKART-001) |
|
| P2 | 0 | 0 |
|
||||||
| P3 | **2** (J-OBS, C-ART-EDGE-002) | **1** (C-ART-EDGE-001) |
|
| 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])
|
## 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])
|
||||||
> R11 fixed the two open art issues in the shared `BrandIllustration`/`EmptyState` helpers and verified both live
|
> R12 was a FRESH FULL A–J run. Found A-201 (P1, Date Match premium bypass) — **fixed + verified live this round** (gated
|
||||||
> on **both** decoupled theme directions, 0 FATAL. The 5 R10 P2 fixes were re-confirmed this round and **pruned** to the
|
> via `CouplePremiumChecker`). C-DARKART-001 (P2) + C-ART-EDGE-001 (P3), fixed in R11, held through R12's visual sweep and
|
||||||
> archived-ID line below (detail in git `9c84c36`). **Fixes for the two art issues are in the working tree (user commits).**
|
> are **pruned** to the archived-ID line below (detail in git / working tree). New P3 **C-ART-EDGE-002** (direct-call hero
|
||||||
> Fix summary: C-DARKART-001 — `LocalAppInDarkTheme` CompositionLocal (set in `CloserTheme`) drives a config-overridden
|
> hard edges) is deferred polish. Remaining open = 2 non-blocking P3s. **A-201 fix + R11 art fixes are in the working tree
|
||||||
> context (`createConfigurationContext` with `UI_MODE_NIGHT_*` from the in-app theme) so `-night` art follows the app's
|
> (user commits).**
|
||||||
> 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.
|
|
||||||
|
|
||||||
| ID | Sev | Area | Description | Repro | Suggested fix | Status |
|
| 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)** |
|
| 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-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)** |
|
|
||||||
| 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)** |
|
| 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)** |
|
| 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)
|
## 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)
|
## 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.
|
- **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).
|
path (no Uri) loads results; **warm tap from Settings now routes** (was the stuck case).
|
||||||
|
|
||||||
## Round history (one line each)
|
## 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
|
- **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
|
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
|
`-night` drawable via a `createConfigurationContext` whose `UI_MODE_NIGHT_*` comes from the app theme, not the system) and
|
||||||
|
|
|
||||||
|
|
@ -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).
|
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
|
- **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
|
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
|
(Play hub), the WaitingForPartner screen, and notifications mix Material icons with brand art. A small
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,46 @@
|
||||||
package app.closer.ui.components
|
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.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Image
|
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.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.ui.unit.dp
|
import app.closer.ui.theme.LocalAppInDarkTheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Brand illustration that reads on BOTH the light and dark themes.
|
* Brand illustration that reads on BOTH the light and dark themes.
|
||||||
*
|
*
|
||||||
* Most of the generated empty-state / header illustrations ship with a soft
|
* Two things the raw `painterResource` path got wrong, both fixed here once for every screen:
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* Transparent art (e.g. the pairing-success celebration) should pass [tile] = false
|
* 1. **Theme-correct variant (C-DARKART-001).** `painterResource` resolves `-night` drawables off the
|
||||||
* so it floats freely with no card edge.
|
* 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
|
@Composable
|
||||||
fun BrandIllustration(
|
fun BrandIllustration(
|
||||||
|
|
@ -30,20 +48,61 @@ fun BrandIllustration(
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
tile: Boolean = true,
|
tile: Boolean = true,
|
||||||
cornerRadius: Dp = 28.dp,
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
) {
|
) {
|
||||||
val shape = RoundedCornerShape(cornerRadius)
|
val context = LocalContext.current
|
||||||
val shaped = if (tile) {
|
val dark = LocalAppInDarkTheme.current
|
||||||
modifier
|
// Resolve the drawable through a config-overridden context so the -night variant follows the
|
||||||
.clip(shape)
|
// IN-APP theme (Settings → Appearance), not the system uiMode (C-DARKART-001).
|
||||||
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.10f), shape)
|
val painter = remember(res, dark) {
|
||||||
} else {
|
val cfg = Configuration(context.resources.configuration).apply {
|
||||||
modifier
|
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(
|
Image(
|
||||||
painter = painterResource(res),
|
painter = painter,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = contentScale,
|
||||||
modifier = shaped,
|
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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,16 @@ package app.closer.ui.components
|
||||||
|
|
||||||
import app.closer.ui.theme.closerCardColor
|
import app.closer.ui.theme.closerCardColor
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -39,13 +35,11 @@ fun EmptyState(
|
||||||
verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md)
|
verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md)
|
||||||
) {
|
) {
|
||||||
illustrationResId?.let { resId ->
|
illustrationResId?.let { resId ->
|
||||||
Image(
|
BrandIllustration(
|
||||||
painter = painterResource(resId),
|
res = resId,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(illustrationSize),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
|
||||||
.size(illustrationSize)
|
|
||||||
.clip(RoundedCornerShape(CloserRadii.Tile))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
|
@ -77,6 +78,11 @@ fun DateMatchScreen(
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
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(
|
DateMatchContent(
|
||||||
state = state,
|
state = state,
|
||||||
onLove = viewModel::loveCurrent,
|
onLove = viewModel::loveCurrent,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package app.closer.ui.dates
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.closer.core.billing.CouplePremiumChecker
|
||||||
import app.closer.domain.model.DateIdea
|
import app.closer.domain.model.DateIdea
|
||||||
import app.closer.domain.model.DateMatch
|
import app.closer.domain.model.DateMatch
|
||||||
import app.closer.domain.model.DateSwipe
|
import app.closer.domain.model.DateSwipe
|
||||||
|
|
@ -14,8 +15,11 @@ import app.closer.domain.repository.UserRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -32,7 +36,8 @@ data class DateMatchUiState(
|
||||||
val ownSwipes: Map<String, SwipeAction> = emptyMap(),
|
val ownSwipes: Map<String, SwipeAction> = emptyMap(),
|
||||||
val matches: List<DateMatch> = emptyList(),
|
val matches: List<DateMatch> = emptyList(),
|
||||||
val currentIndex: Int = 0,
|
val currentIndex: Int = 0,
|
||||||
val justMatched: DateMatch? = null
|
val justMatched: DateMatch? = null,
|
||||||
|
val hasPremium: Boolean = false
|
||||||
) {
|
) {
|
||||||
val currentIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex)
|
val currentIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex)
|
||||||
val nextIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex + 1)
|
val nextIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex + 1)
|
||||||
|
|
@ -46,14 +51,25 @@ class DateMatchViewModel @Inject constructor(
|
||||||
private val repository: DateMatchRepository,
|
private val repository: DateMatchRepository,
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val coupleRepository: CoupleRepository,
|
private val coupleRepository: CoupleRepository,
|
||||||
private val userRepository: UserRepository
|
private val userRepository: UserRepository,
|
||||||
|
private val premiumChecker: CouplePremiumChecker
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(DateMatchUiState())
|
private val _uiState = MutableStateFlow(DateMatchUiState())
|
||||||
val uiState: StateFlow<DateMatchUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<DateMatchUiState> = _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<Unit>(extraBufferCapacity = 1)
|
||||||
|
val paywallRequired: SharedFlow<Unit> = _paywallRequired.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadDateMatch()
|
loadDateMatch()
|
||||||
|
viewModelScope.launch {
|
||||||
|
premiumChecker.isPremium().collect { premium ->
|
||||||
|
_uiState.update { it.copy(hasPremium = premium) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDateMatch() {
|
private fun loadDateMatch() {
|
||||||
|
|
@ -121,6 +137,17 @@ class DateMatchViewModel @Inject constructor(
|
||||||
val userId = _uiState.value.currentUserId ?: return
|
val userId = _uiState.value.currentUserId ?: return
|
||||||
|
|
||||||
viewModelScope.launch {
|
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) }
|
_uiState.update { it.copy(justMatched = null) }
|
||||||
val result = repository.recordSwipe(coupleId, userId, current.id, action)
|
val result = repository.recordSwipe(coupleId, userId, current.id, action)
|
||||||
result.fold(
|
result.fold(
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,19 @@ import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.graphics.Color
|
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
|
@Composable
|
||||||
fun CloserTheme(
|
fun CloserTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
|
@ -15,11 +26,13 @@ fun CloserTheme(
|
||||||
) {
|
) {
|
||||||
val colors = if (darkTheme) darkColors else lightColors
|
val colors = if (darkTheme) darkColors else lightColors
|
||||||
|
|
||||||
MaterialTheme(
|
CompositionLocalProvider(LocalAppInDarkTheme provides darkTheme) {
|
||||||
colorScheme = colors,
|
MaterialTheme(
|
||||||
typography = Typography,
|
colorScheme = colors,
|
||||||
content = content
|
typography = Typography,
|
||||||
)
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brand palette. Keep accent color in containers and controls; large surfaces stay quiet.
|
// Brand palette. Keep accent color in containers and controls; large surfaces stay quiet.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue