diff --git a/ClaudeBrandingReview.md b/ClaudeBrandingReview.md index 7cec8c33..1ddf91b3 100644 --- a/ClaudeBrandingReview.md +++ b/ClaudeBrandingReview.md @@ -3,14 +3,93 @@ Living status document for Closer's brand artwork pass. It records which illustrations/glyphs are live, which surfaces should reuse existing assets, and which brand issues belong in implementation or QA rather than image generation. -**Current state:** no active image-generation prompts remain, and the last two implementation items are now **done -(R13, 2026-06-27)**: the **This-or-That gameplay brand redesign** (C-DARK-UI-001) and the **one-time Premium-unlock -modal** (A13) are both implemented + verified live in both themes. A1-A13 illustrations and the G/G2 glyph sets exist -and are wired. No open branding implementation work remains — only ongoing QA verification. +**Asset ownership:** Codex is responsible for making all needed images, dark variants, and custom glyphs. Do not leave +image work as user-generated prompt handoff; backlog items here are Codex-owned assets to generate, add to the repo, and +verify. -> Branding **defects** (off-brand color, clipped/low-contrast art) → `ClaudeReport.md`. Pure "could be warmer / feature" -> ideas → `Future.md` `## QA`. Only add new prompts here when a future QA pass proves an existing/code-native treatment -> cannot carry the brand. +**Current state (2026-06-27 audit):** the This-or-That redesign (C-DARK-UI-001) + Premium-unlock modal (A13) are done. +**But two large brand backlogs are now OPEN** (found by the 2026-06-27 asset audit): (1) **most illustrations are +light-only** — only 12 of ~25 have a dark variant, and **all 10 `pack_art_*` banners + all `illustration_couple_*` +heroes are light-only**, so they show light/pink art on a dark screen; (2) **~60 distinct generic Material icons** are +used across ~201 call sites (generic hearts, Person, Lock, Star…) — **brand rule: every icon must be a custom Closer +glyph.** See **Brand standards** + the two backlog tables below. + +> Branding **defects** (off-brand color, clipped/low-contrast art, a **light image on a dark screen**, a **generic +> Material icon**) → `ClaudeReport.md`. Pure "could be warmer / feature" ideas → `Future.md` `## QA`. **New art/glyphs to +> create** (dark variants, custom glyphs) → logged HERE as Codex-owned assets to make and verify. + +--- + +## Brand standards — MUST hold every QA round (Pass C visual + Pass H branding own these) +Two non-negotiable brand rules. A violation of either is a **bug** (`ClaudeReport.md`) **and** the asset to create is +logged here as Codex-owned image/glyph work: +1. **Every image has a LIGHT and a DARK variant that matches the IN-APP theme.** No light/pink art on a dark screen, no + dark art on a light screen. `drawable-nodpi/` = light, `drawable-night-nodpi/` = dark (auto-selected by + `BrandIllustration` via the in-app-theme config override). Transparent/celebration art that genuinely reads on **both** + themes is the only exemption — **verify it, don't assume.** A surface missing its variant = bug + add the asset below. +2. **Every icon/glyph is a custom Closer glyph — NO generic Material icons, no generic hearts.** Material `Icons.*` + (ArrowBack, Favorite, Person, Lock, Star, PlayArrow, …) are placeholders, not brand. Each in use is replaced by a + bespoke glyph in the Closer house style, logged in the icon backlog below for Codex to make + add (`glyph_*`). + +**Image style lock:** all generated imagery must match the existing shipped flat 2D vector assets, especially +`illustration_couple_paywall`, `illustration_couple_subscription`, `illustration_couple_onboarding`, +`illustration_daily_question`, and the `pack_art_*` banners. Keep shapes simple and graphic, with pastel fills, minimal +facial/detail rendering, clean vector-like edges, and gentle gradients only. **Reject** painterly/storybook rendering, +3D-ish lighting, realistic texture, detailed hair/skin shading, dramatic cinematic glow, hard rectangular backdrops, or +anything that looks materially richer than the existing asset family. For any new batch, generate and review **one sample +first** against the existing contact sheet before saving more assets. + +## Image theme-variant coverage (light + dark per surface) — backlog: make the missing DARK variants +> Goal: every image row has BOTH a light and a dark asset. Audited 2026-06-27. + +| Illustration | Light | Dark | Action | +|---|---|---|---| +| account_deletion_goodbye · answer_history_empty · bucket_list_empty · connection_challenges_header · date_match_empty · date_match_success · memory_lane_capsule · messages_empty · pairing_success · past_games_empty · privacy_recovery · quiet_hours | ✅ | ✅ | none — both exist | +| **illustration_couple_paywall** (Paywall) | ✅ | ❌ | **MAKE DARK** (`drawable-night-nodpi/`) | +| **illustration_couple_subscription** (Subscription) | ✅ | ❌ | **MAKE DARK** | +| **illustration_couple_onboarding** (Onboarding) | ✅ | ❌ | **MAKE DARK** | +| **illustration_couple_invite** (Pairing/invite) | ✅ | ❌ | **MAKE DARK** | +| **illustration_couple_history** | ✅ | ❌ | **MAKE DARK** (or confirm unused) | +| **illustration_daily_question** (Today hero) | ✅ | ❌ | **MAKE DARK** | +| **illustration_partner_activation** (Home) | ✅ | ❌ | **MAKE DARK** | +| **illustration_tonight_partner_prompt** (Home) | ✅ | ❌ | **MAKE DARK** | +| **illustration_together_empty** (Activity) | ✅ | ❌ | **MAKE DARK** | +| **pack_art_*** ×10 (communication, deep_reflection, desire, family_commitment, fun_date, future_goals, home_life, intimacy, money_values, trust_repair) | ✅ | ❌ | **MAKE DARK** for each pack banner (or prove the banner reads correctly on dark) | +| reveal_celebration · streak_milestone · premium_unlock · spin_wheel | ✅ (transparent) | n/a | transparent/celebration — **verify reads on both themes**; add dark only if it doesn't | + +**Prompt for the missing dark variants:** regenerate each light asset above in the **dark/aubergine** house palette +(deep aubergine `#24122F` ground, lavender `#B98AF4` / soft-pink `#F7C8E4` accents, blush highlights) so it reads on a +dark surface, **same composition + flat 2D vector style + transparent/feathered edges** as the light version, exported to +`drawable-night-nodpi/` with the **identical filename**. Re-run Pass C's decoupled-theme check after adding each. + +## Icon/glyph audit — generic Material icons to replace with custom Closer glyphs (backlog: make these) +> Audited 2026-06-27: **~60 distinct Material icons across ~201 call sites.** Brand rule #2 — each becomes a bespoke +> `glyph_*` in the house style. Existing custom glyphs (reuse/extend, don't regress): the G/G2 set +> (`glyph_paired_cards`, `glyph_how_well`, `glyph_sealed_answer`, `glyph_connection_challenge`, `glyph_memory_capsule`, +> `glyph_date_card_heart`, `glyph_question_packs`, `glyph_bucket_list`, `glyph_past_games`, `glyph_spin_wheel`, +> `glyph_couple_premium`, `glyph_privacy_lock`, `glyph_delete_account`, `glyph_how_well`, + closer_mark/daily_card/ +> quiet_hours_moon/export_data). **To make** (high-traffic first): + +| Generic icon (≈uses) | Used for | Make custom glyph | +|---|---|---| +| `ArrowBack` (31) | every top-bar back | `glyph_back` (brand chevron) | +| `Favorite` / `FavoriteBorder` (17/5) | **generic hearts** — likes, love, daily | `glyph_heart` (the Closer two-equal-halves heart, filled + outline) | +| `Lock` / `LockOpen` (18/2) | premium-locked, security | `glyph_lock` / `glyph_lock_open` (keyhole motif from the mark) | +| `Person` / `People` (16/1) | avatars/partner fallback | `glyph_person` / `glyph_couple` | +| `Check` / `Done` / `CheckCircle` (11/1/1) | confirm/selected/sent | `glyph_check` | +| `ArrowForward` / `ArrowForwardIos` (10/4) | row chevrons, next | `glyph_forward` | +| `PlayArrow` (8) | Play tab / start | `glyph_play` | +| `Close` (7) | dismiss/close | `glyph_close` | +| `Star` (6) | premium/★ ideas | `glyph_star` (brand sparkle) | +| `Visibility` / `VisibilityOff` (5/2) | password reveal | `glyph_eye` / `glyph_eye_off` | +| `ContentCopy` (4) | copy invite code | `glyph_copy` | +| `Sync` (3) · `Image`/`PhotoLibrary`/`PhotoCamera`/`AddAPhoto` (3/2/2/2) · `Send` (3) · `Chat` (3) · `Delete` (3) | retry · media pickers · send · messages · delete | `glyph_sync` · `glyph_photo`/`glyph_camera` · `glyph_send` · `glyph_chat` · `glyph_trash` | +| `Home` (2) · `Settings` (1) · `Notifications`/`NotificationsNone` (2) | bottom nav + settings | `glyph_home` · `glyph_settings` · `glyph_bell` | +| `LocalFireDepartment` (2) | **streak flame** (generic) | `glyph_streak` (brand flame/spark) | +| `Mic`/`Pause` · `Timeline`/`TrendingUp`/`Psychology` · `Fingerprint`/`Key`/`Shield` · `Edit`/`Add`/`Share`/`Refresh`/`Warning`/`CalendarToday`/`Cake`/`CardGiftcard`/`AttachMoney`/`HourglassEmpty`/`QuestionAnswer`/`Palette`/`OpenInNew` (1 each) | voice · progress · security · misc | one bespoke `glyph_*` each, in house style | + +Replace each `Icons.*` call site with `ImageVector.vectorResource(R.drawable.glyph_*)` + `Icon(tint=…)` (the existing +wiring pattern). Until a glyph exists, the Material icon is a **placeholder = a logged brand defect**, not acceptable for ship. --- @@ -218,8 +297,14 @@ Legend: ✅ on-brand / no art needed · ➕ reuse/wire existing art · 🔤 bran ## Generation Backlog -**None.** Do not regenerate completed illustration or glyph art unless a future QA pass logs a specific defect that -requires replacement. +**Two OPEN backlogs (2026-06-27 audit) — see the tables up top:** +1. **Dark illustration variants** — make `drawable-night-nodpi/` versions of every light-only image: all + `illustration_couple_*` heroes, `daily_question`, `partner_activation`, `tonight_partner_prompt`, `together_empty`, + and all 10 `pack_art_*` banners (see **Image theme-variant coverage**). +2. **Custom glyphs to replace ~60 generic Material icons** (see **Icon/glyph audit**) — every icon must be a bespoke + `glyph_*`, starting with the highest-traffic (back, heart, lock, person, check, forward, play, close, star). + +Do not regenerate **completed** illustration/glyph art unless a QA pass logs a specific defect; the above are net-new. **Generated glyph files:** `glyph_closer_mark`, `glyph_paired_cards`, `glyph_daily_card`, `glyph_sealed_answer`, `glyph_memory_capsule`, `glyph_date_card_heart`, `glyph_quiet_hours_moon`, `glyph_couple_premium`, diff --git a/ClaudeQACoverage.md b/ClaudeQACoverage.md index 88c30d1b..c94d931b 100644 --- a/ClaudeQACoverage.md +++ b/ClaudeQACoverage.md @@ -1,7 +1,9 @@ # Claude QA Coverage Matrix > **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`. -> Build `2cd0af6` + R11/R12/R13 working-tree changes (rebuilt + installed both emulators). Position + verdict: see `ClaudeReport.md` run-state. **Verdict: R13 = open-backlog fix pass + full fresh A–J — FLAWLESS, 0 open P0–P3.** Fixed all 5 carried/open UI items (C-DARK-UI-001 ToT dark redesign · C-DARK-UI-002 check-in label · C-DARK-UI-003 bottom insets · C-ART-EDGE-002 8 opaque heroes feathered · J-OBS 48dp), confirmed **A-201** live → pruned, shipped the **Premium-unlock modal** (one-time, both partners). Pass D cornerstone re-verified LIVE. All app changes in the working tree (user commits); diff is UI-only (no rules/functions/crypto). +> Build HEAD `c31eea2` + **R15 working-tree changes** (functions + rules **deployed to prod**; client rebuilt+installed both emulators). Position + verdict: see `ClaudeReport.md` run-state. **Verdict: R15 = gap-closing round (Passes L/M/N/P + smoke) — found & FIXED M-001 (P2 quiet hours).** Quiet hours didn't suppress backgrounded/killed partner pushes (local-only); fixed via server-side fail-open suppression + client window/tz sync + rules allowlist — verified live (fn log suppress vs notify). L (chat E2E render + decrypt + receipts + reactions + at-rest), P (UI copy + 6103-Q bank, clean), N (daily-Q/reveal gate), smoke 6/6 GREEN. **0 open P0–P2** (M-001 fixed, pending 1 confirm); 2 P3 brand backlogs open. 0 FATAL. App+functions+rules changes in working tree (user commits); functions+rules already deployed. +> +> **Scope expanded (plan review):** the playbook now has first-class passes **K–O** (billing money-path · messaging/chat E2E · functional settings · daily-Q/outcomes/interactive · release-build/store-readiness). These surface **coverage GAPS, not defects** — the recurring defect bar is clean, but **K (real purchase/restore/cancel path), L (full chat), M (settings take-effect), N (outcomes/Bucket-List/Date-Builder), O (minified release + App Check + store)** are `todo`/`partial`/`blocked→needs-device`. **Next-priority work = close these (start L + M on-emulator; K + O need a real device / pre-ship).** > > **📖 Architecture reference:** see [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) — contains architecture, security model, data model, and the [Known landmines](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) section that backs every fix-and-pruned ID below. > Hygiene: this is a *current-status* matrix, not a per-round changelog — `fail→id` flips to `pass` once a fix is @@ -12,16 +14,22 @@ |---|---|---| | A — Couple-shared premium | R13: **A-201 confirmed live → pruned** (free QA → Date Match Love ★Premium → Paywall, deck didn't advance) + Desire Sync free→Paywall re-verified; couple-shared unlock holds | ✅ pass (all gates couple-shared incl. Date Match) | | B — Games lifecycle | R12: 4 async games full 2-device end-to-end (ToT Light×5, Wheel mixed-types, How Well asym, Desire Sync shared/private) + start/join/first-finisher/finish/results/back-stack; CC+MemoryLane+DateMatch render/core (full per R10) | ✅ pass (first-finisher nudge + C-NAV-002 + Ready=Start re-verified live; MemoryLane title/preview run-on → Future.md) | -| C — Visual (light+dark) | R13: ToT setup+gameplay both themes, Play/Home/Paywall insets, Today/Paywall heroes, conversation, Premium modal both themes — all verified live | ✅ **C-DARK-UI-001/002/003 + C-ART-EDGE-002 all FIXED + verified live R13** (ToT theme-aware redesign · check-in label weight · bottom clearance · 8 opaque heroes feathered); pending 1 confirm | +| C — Visual (light+dark) | R13: ToT setup+gameplay both themes, Play/Home/Paywall insets, Today/Paywall heroes, conversation, Premium modal both themes — all verified live | ✅ R13 fixes pruned · ⚠️ **BRAND-DARK-COVERAGE (P3) open** — 2026-06-27 audit: many illustrations light-only (no dark variant); see `ClaudeBrandingReview.md` | | D — Security & encryption | R13 LIVE (rules/functions unchanged this session): D3 non-member GET couple+messages → 403; D5 self-grant entitlement PATCH → 403; member GET own couple → 200; D1 chat at-rest `enc:v1:`. D2/D4/D6/D7 carried R7/R10 | ✅ clean — cornerstone holds | | E — Notifications | R12 LIVE: Pass B verified start/first-finisher(`partner_completed_part`)/finish triggers→correct partners+copy; cold-start tap smoke **6/6** (launcher + 5 notif types open & stay) | ✅ pass · splash-crash class clean on fresh APK | | F — Resilience | R12: concurrency (F-RACE-001 atomic-start code + R8 live) · process-death (smoke `am kill`×5 → push → cold-start recovered each) · offline(R9) | ✅ pass · time-travel + deletion-cascade deferred | | G — Account creation / fake-account | R10: abuse live via D3 (non-member denied, no self-grant) + invite rules; happy/validation R5-clean (unchanged) | ✅ pass | -| H — Branding & artwork | R13: ToT gameplay brand redesign + Premium-unlock modal (A13) both implemented + verified live; 8 opaque heroes feathered | ✅ see `ClaudeBrandingReview.md` (no open branding implementation work remains) | +| H — Branding & artwork | R13: ToT redesign + Premium-unlock modal done. **2026-06-27 brand audit opened 2 backlogs.** | ⚠️ **BRAND-DARK-COVERAGE** (light-only illustrations need dark variants) + **BRAND-ICON-CUSTOM** (~60 generic Material icons → bespoke `glyph_*`) — full asset lists in `ClaudeBrandingReview.md` | | I — Performance & route efficiency | R10: core-tabs jank 5.53% (R8 6.3%), 90th 32ms, leak proxy bounded | ✅ no regression | | J — Accessibility | R13: **J-OBS FIXED + verified live** (composer media/voice/retry buttons → 48dp; measured 126px=48dp both axes); font scale 2.0 reflows + reduce-motion×7 (R10) hold | ✅ done · J-OBS fixed (pending 1 confirm) | +| K — Billing & subscription lifecycle | Gate (couple-shared unlock) verified via admin toggle (Pass A) + Premium-unlock modal + `onEntitlementChanged` push live (R13/R14). **Real money path (purchase/restore/cancel→expiry-relock/refund/plan-switch) NOT tested** — needs a real device + Play sandbox. | ⚠️ **todo** — money path `blocked→needs-device`; gate ✅ | +| L — Messaging & chat (E2E) | R15: conversation render driven live — decrypt **both dirs**, attribution, timestamps, **Seen** receipt, ❤️ **reaction**, ordering, day-separators, voice-note + image bubbles, E2E composer lock glyphs; inbox decrypted previews **no `enc:` leak**; live QA→Sam send delivered; at-rest `enc:v1:`. Remaining: failed-send/offline retry, delete-message, fresh image/voice send, Discuss-thread live send. | ✅ **pass (core)** — text/decrypt/receipts/reactions/inbox/at-rest verified; 4 sub-items carry | +| M — Settings & account management | R15: **M-001 (quiet hours) FIXED + verified live** (server-side fail-open suppression); per-type notif toggle take-effect confirmed live (server-enforced; field flips in Firestore; toggle-off → 0 delivery); theme/DataStore persistence across relaunch ✅; biometric lock code-sound (cold-start re-lock; background-resume observation → Future.md). Remaining: edit-profile persist, unpair/delete-cascade (disruptive — deferred). | ✅ **pass (core)** — M-001 fixed (pending 1 confirm); unpair/delete deferred | +| N — Daily Q / reveal / check-ins / interactive | R15: daily-Q + **reveal both-answered gate** render confirmed live (privacy copy exemplary). Outcomes loop / Bucket List CRUD / Date Builder save / Activity feed render-clean (prior rounds) — not re-driven this round. | ⚠️ **partial** (core render-clean; CRUD loops carry) | +| O — Release build & store readiness | **Not started.** All QA to date is on the **debug** APK. Minified release build, signing/AAB, App Check enforcement, i18n/RTL, App-Links, Play Data-Safety = pre-ship gate, not yet run. | ❌ **todo (pre-ship gate)** | +| P — Content, copy & language | R15: UI-microcopy swept (warm/inclusive; debug rows `BuildConfig.DEBUG`-gated; friendly error fallbacks; on-brand privacy copy) + **question-bank audit live: 6103 Qs — 0 empty, 0 exact dupes, 0 placeholder tokens, complete/mutually-exclusive answer configs, good type variety, consent-framed sensitive content.** No typos/off-voice/non-inclusive copy found. | ✅ **pass** — copy + question bank clean | -**Archived issue IDs (fixed + confirmed, detail in git):** A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · C-NAV-001 · D-001 · E-001 · E-002 · E-003 · E-OBS · F-OBS. Pending one confirm: **F-RACE-001**. +**Archived issue IDs (fixed + confirmed, detail in git):** A-001 · A-003 · A-201 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DARKART-001 · C-DARK-UI-001 · C-DARK-UI-002 · C-DARK-UI-003 · C-DS-001 · C-ART-EDGE-001 · C-ART-EDGE-002 · 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 · J-OBS. **0 open P0–P2; 1 fixed pending confirm (M-001 quiet hours); 2 open P3 brand backlogs.** --- @@ -63,7 +71,7 @@ Note: exit each game via "Back to Play" between games so the session closes (B-0 - **D2 rules:** no catch-all, no blanket `if true`; sessions update allowlist + immutable `startedByUserId` + monotonic status; `hasPremium` + entitlements server-only; ciphertext enforced on private fields; capsules/challenges member-scoped. - **D3 raw-API negative (LIVE):** non-member ID token → Firestore REST on couple doc/conversation/messages/answers/session/capsules/partner-profile = **all 403**; non-member writes incl. real `users/{uid}/entitlements/premium` = **all 403 → no self-grant**. Member token reads 200 → **App Check not enforced on Firestore; rules are the sole gate and hold**. - **D4/D5/D6:** wrapped couple key + KDF; App Check (client), gitignored SA JSONs, `allowBackup=false`; analytics metadata-only. Unchanged, hold. -- Two hardening notes → `Future.md` (App Check off on Firestore; `users/{uid}` update rule allows arbitrary non-`hasPremium` fields). +- One hardening note → `Future.md` (App Check off on Firestore). _(R15 correction: `users/{uid}` update rule enforces a **field allowlist** — not arbitrary; extended R15 for `quietHours*`+`timezone`.)_ ## Pass E — Notifications (type × {foreground / background / killed} + tap-to-open, both clients) Full live two-device run (games + messages): @@ -114,6 +122,8 @@ Route smoke-test checklist (re-runnable: `dumpsys gfxinfo closer.app reset` → --- ## Round history (one line each) +- **R15** — gap-closing round (Passes L/M/N/P + regression smoke); **found + FIXED M-001 (P2 quiet hours)** — local-only window didn't suppress backgrounded/killed partner pushes; fixed via server-side fail-open `recipientInQuietHours()` in the 4 partner-action senders + client window/tz sync to `users/{uid}` + rules allowlist; verified live (fn log suppress vs notify; positive control delivers); deployed prod. L chat-core, P copy+question-bank (6103 Qs), N daily-Q/reveal verified clean; smoke 6/6 GREEN. Corrected stale "users/{uid} allows arbitrary fields" claim (there's an allowlist). +- **R14** — full fresh A–J ClaudeQAPlan run (pure QA, no code), FLAWLESS, 0 new findings: confirmation round on the R13 build — premium enforcement + couple-shared unlock + entitlement push (live); Desire Sync/How Well/Spin-the-Wheel full 2-device + first-finisher nudge; Memory Lane create+seal, CC resume, Date Match deck; decoupled-theme-art mandate; cornerstone live (403s + enc:v1:); offline + process-death; jank 5.25%; J-OBS 48dp holds. The 5 R13 fixes held → pruned (archived line). - **R13** — open-backlog fix pass + full fresh A–J, FLAWLESS (0 open P0–P3): fixed C-DARK-UI-001 (ToT dark redesign), C-DARK-UI-002 (check-in label), C-DARK-UI-003 (bottom insets), C-ART-EDGE-002 (8 opaque heroes feathered), J-OBS (48dp targets); confirmed A-201 live→pruned; shipped Premium-unlock modal (one-time, both partners, couple-shared, verified live). Pass D cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, at-rest enc:v1:). Diff UI-only → E/F/G carried. 0 FATAL both emulators. - **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`. diff --git a/ClaudeQAPlan.md b/ClaudeQAPlan.md index c7dd7058..cd03eb55 100644 --- a/ClaudeQAPlan.md +++ b/ClaudeQAPlan.md @@ -26,6 +26,12 @@ For each Pass below, before you start, read the relevant section of [`docs/Engin | H — Branding & artwork | `ClaudeBrandingReview.md` (this repo) · `docs/brand/visual-identity.md` | | I — Performance | [Engineering conventions](docs/Engineering_Reference_Manual.md#engineering-conventions) · [Where to look first](docs/Engineering_Reference_Manual.md#where-to-look-first) | | J — Accessibility | [CloserTheme](docs/Engineering_Reference_Manual.md#ios-specific-notes) · [Engineering conventions](docs/Engineering_Reference_Manual.md#engineering-conventions) | +| K — Billing & subscription lifecycle | [Billing](docs/Engineering_Reference_Manual.md#billing) · [Premium-gated features and gate pattern](docs/Engineering_Reference_Manual.md#premium-gated-features-and-gate-pattern) | +| L — Messaging & chat (E2E) | [End-to-end encryption model](docs/Engineering_Reference_Manual.md#end-to-end-encryption-model) · [Notifications](docs/Engineering_Reference_Manual.md#notifications) | +| M — Settings & account management | [Authentication and pairing flow](docs/Engineering_Reference_Manual.md#authentication-and-pairing-flow) · [Notifications](docs/Engineering_Reference_Manual.md#notifications) | +| N — Daily question & interactive features | [Daily question lifecycle](docs/Engineering_Reference_Manual.md#daily-question-lifecycle) | +| O — Release build & store readiness | [Firestore security rules](docs/Engineering_Reference_Manual.md#firestore-security-rules) · [Engineering conventions](docs/Engineering_Reference_Manual.md#engineering-conventions) | +| P — Content, copy & language | `docs/brand/visual-identity.md` (Store voice) · `seed/questions/QUESTION_CONTENT_GUIDE.md` (v3) | **If you find a bug that LOOKS like it might be a re-introduction of a known landmine** (above table or [Known landmines](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes)), stop and verify the fix is still in place before filing a new ID — it may be a regression on a known issue, not a new bug. @@ -40,6 +46,10 @@ For each Pass below, before you start, read the relevant section of [`docs/Engin - 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`. +- **WRONG LANGUAGE IS A BUG (not a Future.md idea).** A typo, grammar/punctuation error, off-brand or cold/salesy voice, + non-inclusive/assumptive wording, leaked placeholder/dev/raw-error text, copy that doesn't match behavior, or a + broken/duplicate/off-guide question → **`ClaudeReport.md`** (see **Pass P**). Only genuinely-working copy that *could be + warmer/clearer* (a rewording for delight) goes to `Future.md`. "Confusing copy" that actually misleads the user is a bug. - **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 @@ -60,7 +70,7 @@ For each Pass below, before you start, read the relevant section of [`docs/Engin (detail lives in git). `Future.md` ideas sit in the backlog until built. (See **Report hygiene** under Reporting.) ## Context -Drive the real app on both emulators, verify each thing live, report, fix, re-verify. Five QA dimensions: +Drive the real app on both emulators, verify each thing live, report, fix, re-verify. Core QA dimensions (cornerstones): 1. **Couple-shared premium** — if EITHER partner is premium, **all** premium features unlock for **both**. 2. **Games** — each starts, plays, **joins, resumes**, finishes, **and reopens results** correctly on both devices. 3. **Full visual pass, light + dark** — every screen, text readable, nothing clipped/invisible. @@ -70,6 +80,17 @@ Drive the real app on both emulators, verify each thing live, report, fix, re-ve deep-links correctly, opens the right destination on **both clients**, covers all **game/join-game** flows, handles stale notifications, and leaks no private content. +These five are the original cornerstones; the playbook has since grown to cover the rest of the app as first-class +passes (see **QA passes** below): **K Billing & subscription lifecycle** (the real purchase/restore/cancel/expiry money +path, not just the admin entitlement toggle), **L Messaging & chat** (E2E send/receive/react/media, both clients), +**M Settings & account management** (every toggle persists + takes effect; biometric lock, quiet hours, unpair/delete), +**N Daily-question/reveal/check-ins + Bucket List/Date Builder/Activity** (the interactive non-game features), and +**O Release build & store readiness** (the **minified release** build, signing/AAB, App Check, i18n, deep/App-Links, +Play Data-Safety — everything else runs on the debug APK), and **P Content, copy & language** (typos/grammar, brand +voice, inclusive language, and the **question-bank** content — *wrong language is a bug, not a Future.md idea*). Plus the +existing **F resilience**, **G account/abuse**, **H branding**, **I performance**, **J accessibility**. **Pass letters are stable IDs — never renumber** (issue IDs and +coverage rows reference them; note D/E/G are not in strict alphabetical position for that reason). + Scope decisions: **exhaustive** visual pass (all ~50 screens, both modes); **full scope incl. pre-pairing** flows (fresh throwaway account); **couple-shared everywhere** — per-user gates are bugs, fixed by routing through `core/billing/CouplePremiumChecker.kt`; **full notification suite** — every type, game + join-game pushes, deep-links, @@ -80,7 +101,7 @@ stale-notification handling, and all in-app paths into joining/resuming/results, confirms + enumerates this; the fix phase applies couple-shared everywhere. ## Execution mode — run to completion (autonomous; do NOT stop) -- **Do not stop to check in or ask for approval.** Run all passes (A–J) → the fix phase → re-QA rounds **continuously +- **Do not stop to check in or ask for approval.** Run all passes (A–P — recurring set A–N + P each round; K's real-money path and O's release/store gates run when a sandbox device / pre-ship is in scope) → the fix phase → re-QA rounds **continuously until a flawless round** (zero open P0–P2, Passes D + E clean, every game fully played through, all notification routes verified, navigation/back-stack verified). Don't hand control back early. - **Unblock yourself:** if anything **blocks progress** (a stale/blocking session, a crash, a build break, a missing @@ -304,6 +325,12 @@ fully inspect screenshots, tap every CTA, vary app states, update files accurate | H Branding | **one small route family per chunk** (~2–3 screens/states) consumer brand walk + ready-to-paste art prompts + existing-image integration verdict | 8–14 | | I Performance | **one route-group per chunk** — gfxinfo/jank + read-count instrumentation (build the route smoke checklist) | ~3 | | J Accessibility | **one a11y setting per chunk** (font scale · TalkBack · contrast · targets · keyboard · reduce-motion) | ~5 | +| K Billing | **one money-path per chunk** (purchase · restore · plan-switch · cancel→expiry-relock · refund · webhook auth) — needs a real device/sandbox | ~6 | +| L Messaging | **one chat dimension per chunk** (send-types both dirs · reactions/receipts/typing · failed-send/offline · media perms · inbox/entry-points · delete/moderation) | ~6 | +| M Settings | **one settings group per chunk** (appearance · notif toggles · quiet hours · biometric lock · edit profile · unpair/delete · security/recovery) | ~6 | +| N Interactive features | **one feature per chunk** (daily-question loop · outcomes/check-ins · Bucket List · Date Builder · Activity feed) | ~5 | +| O Release/store | **one gate per chunk** (minified release smoke · signing/AAB · App Check (staging) · deep/App-Links · permissions/manifest · i18n · Data-Safety/store) — pre-ship, not per-round | ~6 | +| P Content/language | **one surface per chunk** (UI microcopy of a route family · voice/tone sweep · inclusive-language sweep · question-bank by category/depth · legal/store copy) | ~5 | Context-cost tips: prefer **code/admin-read audits** (cheap) before live UI sweeps; **montage** screenshots (dark|light pairs) to review many at once; keep one chunk = one TodoWrite focus. @@ -447,6 +474,24 @@ Account); Paywall; Your Progress/Activity; Recovery. re-run the decoupled state and confirm it still holds, including any newly added `-night` art.) Fix pattern (if it regresses): drive the resource `uiMode` from the in-app theme as above, or `AppCompatDelegate.setDefaultNightMode`/config override, so `painterResource` picks `-night` per the app's own setting. +- **EVERY image needs BOTH a light AND a dark variant matching the theme (mandatory — audit every image-bearing page).** + It is not enough that the `-night` mechanism works — the dark asset must **exist**. Go page by page through every screen + that shows an illustration / hero / banner / empty-state / pack art and confirm there is a real **dark variant** in + `drawable-night-nodpi/` (and a light one in `drawable-nodpi/`) that **matches the in-app theme** — a light/pink image + shown on a dark screen (because only the light asset exists) is a **bug**, even with feathered edges. Cross-check each + page against the **Image theme-variant coverage** table in `ClaudeBrandingReview.md`; a missing variant is filed as a + bug in `ClaudeReport.md` **and** the dark/light asset to create is logged in `ClaudeBrandingReview.md` as a prompt to + be made. (2026-06-27 audit found all `illustration_couple_*` heroes, `daily_question`, `partner_activation`, + `tonight_partner_prompt`, `together_empty`, and all 10 `pack_art_*` are **light-only** — these still need dark variants.) + Only genuinely theme-agnostic transparent/celebration art is exempt, and only after you **verify** it reads on both. +- **EVERY icon/glyph must be a CUSTOM Closer glyph — no generic Material icons, no generic hearts (mandatory).** On every + screen, inspect each icon: any generic Material icon (`Icons.Filled.*`/`Icons.AutoMirrored.*`/`Icons.Default.*` — + ArrowBack, Favorite/FavoriteBorder, Person, Lock, Star, PlayArrow, Check, Close, Send, …) is a **placeholder, not + brand** → a finding. File it as a brand defect in `ClaudeReport.md` and log the **custom `glyph_*` to make** in + `ClaudeBrandingReview.md` (see its **Icon/glyph audit**). Reflex grep to find them: + `grep -rE "Icons\.(Filled|Outlined|Rounded|Default|AutoMirrored)\." app/src/main/java/app/closer/` — every hit is a + generic icon that needs a bespoke Closer glyph (`ImageVector.vectorResource(R.drawable.glyph_*)` + `Icon(tint=…)`). + (2026-06-27 audit: ~60 distinct Material icons across ~201 call sites still to replace.) - **States, not just happy path:** empty / loading / error / not-paired / locked-premium / signed-out / stale-or-deleted-target / populated-with-many where they exist; many need data setup (seeding is user-gated) — note unreachable states in coverage rather than skipping silently. @@ -496,6 +541,14 @@ Account); Paywall; Your Progress/Activity; Recovery. hierarchy should feel intentional and consistent. Awkward or out-of-place UI such as a Settings relationship row where **"Connected with ..."** looks visually odd, cramped, misaligned, or unlike the rest of Settings is a finding: file as a bug if it looks broken/inconsistent; log to `Future.md` only if it is purely a product/content improvement. +### Pass D — Security & encryption (cornerstone; findings default to P0) +> Read first: manual's [E2EE model](docs/Engineering_Reference_Manual.md#end-to-end-encryption-model) · +> [Firestore rules](docs/Engineering_Reference_Manual.md#firestore-security-rules) · +> [Encryption versions](docs/Engineering_Reference_Manual.md#encryption-versions). The cornerstone: every private field +> is ciphertext at rest, rules hold against non-members, keys/recovery are sound. **D3 (live negative raw-API) is +> MANDATORY every round** — never deferred to "only 2 emulators" (mint a non-member token via admin → Identity Toolkit +> `signInWithCustomToken` → Firestore REST). Run all of D1–D7: + - **D1 At-rest coverage:** admin-read RAW docs/objects, assert ciphertext for every private type — chat text + `lastMessagePreview` (`enc:v1:`), chat media bytes (Tink `01 69 59 51 f0…`), answers (`sealed:v1:`/`enc:v1:`), date plans + `date_swipes`, Memory Lane capsules, Bucket List. Also: **wrappedCoupleKey** + recovery material never @@ -703,10 +756,20 @@ open**. No duplicates; rate limiter (20/day, 100/week) doesn't drop legit ones. - **Crash reporting:** confirm crashes/ANRs are actually captured (Crashlytics) so field issues surface. ### Pass H — Branding & artwork (every screen: could it carry more of the brand? where would art help?) -A consumer-mindset pass focused on **brand presence and delight**, not defects. Walk **every screen and surface** and -ask: *does this feel like Closer (private, warm, equal, intentional — a ritual for two)? Could brand color, the heart -mark, a brand message, or an illustration make it warmer or clearer without clutter?* Output is **artwork descriptions -written as ready-to-paste ChatGPT image-generation prompts** — the user generates the images; we only describe them. +**Branding review is a MANDATORY part of QA every round** (not an optional polish pass) — its findings + the assets to +create are logged in `ClaudeBrandingReview.md`. A consumer-mindset pass focused on **brand presence and delight** AND +two hard brand standards. Walk **every screen and surface** and ask: *does this feel like Closer (private, warm, equal, +intentional — a ritual for two)? Could brand color, the heart mark, a brand message, or an illustration make it warmer +or clearer without clutter?* Output is **artwork descriptions written as ready-to-paste ChatGPT image-generation +prompts** — the user generates the images; we only describe them. +- **MANDATE 1 — every image has a light AND a dark variant (theme-matched).** Cross-check every image-bearing page + against the **Image theme-variant coverage** table in `ClaudeBrandingReview.md`; a light-only image shown on dark (or + vice-versa) is a **bug → `ClaudeReport.md`** and the missing variant is a **prompt to make → `ClaudeBrandingReview.md`**. + (Shares the per-page audit with Pass C; H owns producing the prompts + tracking the coverage table.) +- **MANDATE 2 — every icon/glyph is a CUSTOM Closer glyph (no generic Material icons / generic hearts).** Audit all + icons in use (`grep -rE "Icons\.(Filled|Outlined|Rounded|Default|AutoMirrored)\."`); each generic icon is a brand + defect → `ClaudeReport.md` + a **custom `glyph_*` to make → `ClaudeBrandingReview.md`** (the **Icon/glyph audit** + table). The bar for ship: **zero generic Material icons** — every icon is bespoke and on-brand. - **Existing art integration check:** judge the art as part of the whole page, not as a standalone asset. Confirm each image supports the screen's job, aligns with the surrounding typography/actions, has enough breathing room, and uses the right light/dark treatment. Art that looks generic, unfinished, randomly placed, or visually disconnected is a @@ -786,9 +849,172 @@ This is the deep home for a11y; the Pass C contrast/font spot-checks feed into i - Findings: missing label / clipped-at-large-font / sub-48dp / failing contrast = bug → `ClaudeReport.md` (**P2**; **P1** if it blocks a primary flow for assistive-tech users); polish → `Future.md` `## QA`. +### Pass K — Billing & subscription lifecycle (the REAL money path, not the admin toggle) +**Pass A tests the GATE (couple-shared unlock via an admin entitlement toggle); Pass K tests how the entitlement is +actually earned, kept, and lost.** This is the revenue path and it is almost entirely unexercised by the admin toggle. +Read the manual's [Billing](docs/Engineering_Reference_Manual.md#billing) section first. **Needs real services** +(Google Play Billing sandbox + a Play **license tester** + RevenueCat) — emulators can't do real IAP, so run on a +**physical device** with a sandbox account, or mark each money-path row `blocked→needs-device` (the admin toggle is +**not** a substitute for these). +- **Purchase, end to end:** Paywall → select a plan → Play billing sheet → buy as a sandbox tester → RevenueCat → + `revenueCatWebhook`/`syncEntitlement` → `users/{uid}/entitlements/premium` flips active → features unlock for **both** + partners (couple-shared, Pass A) → `onEntitlementChanged` fires the partner push → the one-time **Premium-unlock modal** + (`PremiumUnlockOverlay`) shows once for **each** partner. +- **Restore purchases:** Paywall "Restore" on a reinstall / second device / after sign-out→in → entitlement restored, + no double-charge, features unlock. +- **Plan switching:** monthly ↔ annual upgrade / downgrade / crossgrade → correct proration + entitlement continuity. +- **Trial / intro pricing** (if configured); **price + currency are displayed from the store, localized, never + hardcoded**; plan list + benefits render; offline/SDK-error paywall is friendly (A-OBS), Continue hidden until plans load. +- **Cancel → expiry → RE-LOCK:** cancel keeps access until period end (`expiresAt`); at expiry, `CouplePremiumChecker` + reports inactive → premium features **re-lock for BOTH** and the premium-unlock "celebrated" flag re-arms. Test the + `expiresAt` boundary (admin: set it just-past) — the couple-shared checker must treat a lapsed entitlement as inactive. +- **Billing retry / grace period / account hold / pause** (Play states) → entitlement + UI reflect the state; no hard + crash, clear messaging. +- **Refund / revocation:** RevenueCat `CANCELLATION`/`EXPIRATION`/refund webhook → entitlement removed promptly → re-lock. +- **Security (overlaps D3/D5):** server-only entitlement writes (client self-grant → 403); **webhook authenticity** + (forged/replayed RevenueCat webhook rejected); no client-trusted entitlement; receipt validated server-side. +- **Error/abuse:** cancel the billing sheet mid-flow, kill network mid-purchase, double-tap buy, rapid unlock taps → + no false unlock, no duplicate purchase, retry recovers. +- **Settings → Subscription** reflects the live status; "Manage subscription" deep-links to Play. +- Done = purchase + restore + switch + cancel→expiry-relock + refund all verified on a real device (or each explicitly + `blocked→needs-device` with the admin-toggle gate covered in Pass A). + +### Pass L — Messaging & chat (E2E, both clients, the whole feature) +Chat is a core couple feature with no functional home until now (Pass C covers its visuals, Pass E its +`chat_message` push). Drive the **main couple conversation AND the per-question "Discuss" threads** QA↔Sam, both +directions. Read the manual's [E2EE model](docs/Engineering_Reference_Manual.md#end-to-end-encryption-model). +- **Send every type, both directions:** text, emoji, **image (gallery + camera)**, **voice note** → arrives on the + partner's device **decrypted**, correct attribution/timestamp/ordering, day separators. +- **E2E at rest (overlaps D1):** every sent item is ciphertext (`enc:v1:` / Tink media bytes), `lastMessagePreview` + encrypted, decrypts only on member devices; raw-API read by a non-member = 403. +- **Interactions:** reactions (add / change / remove), read receipts ("Seen"), typing indicator, message ordering under + rapid exchange. +- **Failed send & offline:** airplane mid-send → failed-message row → **retry / dismiss** (the 48dp controls), offline + queue flushes on reconnect, **no duplicate on retry** (idempotency, overlaps F); double/triple-tap send guarded. +- **Delete / moderation:** delete a message (own / both) + deleted-message rendering; block/report a partner if such a + flow exists. +- **Media:** gallery + camera + mic permission granted **and denied** → graceful; premium-gated media is couple-shared + (Pass A); oversized image handled; image viewer opens/zoom/back. +- **Inbox:** conversation list, unread badge, decrypted last-message preview, recency sort; open a conversation from + inbox **and** from "Discuss" **and** from a notification (Pass C/E) — all reach the same thread with a sane back stack. +- **Foreground chat-head bubble** for an incoming message while the app is open but that thread isn't on screen + (`MessageBubbleOverlay`) → tap opens it (Pass E overlap). +- **Realtime + perf (overlaps I):** snapshot listener detaches on leaving the conversation; long-history scroll pages, + no jank/leak. **Quiet hours** suppress the chat push (Pass M). Long/emoji/multiline/RTL text renders without clipping. + +### Pass M — Settings & account management (functional: settings PERSIST and TAKE EFFECT) +Pass C checks Settings **looks** right; Pass M checks each control **does** something, persists across relaunch, and +takes real effect. Read [Authentication and pairing flow](docs/Engineering_Reference_Manual.md#authentication-and-pairing-flow). +- **Appearance theme** (Light / Dark / Device) → applies app-wide immediately, **persists across process death + + relaunch**, and the decoupled-art behavior holds (Pass C). +- **Notification toggles** (daily reminder · partner answered · chat · streak) → toggling one **OFF actually suppresses + that push** (verify by triggering it), ON re-enables; survives relaunch. +- **Quiet hours** → set a window covering "now" → partner-triggered pushes are suppressed/deferred during it + and deliver outside it; the partner-action vs promotional rate-limit split holds. **MUST test with the recipient + BACKGROUNDED/KILLED, not just foreground (RETROSPECTIVE — M-001):** a partner push carries a `notification` block + the OS renders directly when the app isn't foreground, so any client-side `QuietHoursManager.isInQuietHours` check + (which only runs in `onMessageReceived`, foreground-only) is bypassed exactly when quiet hours matters. Verdict bar: + with QH on + recipient backgrounded, send a real partner action (chat/answer/game) → assert **0** notification in the + shade AND the Cloud Function log says it suppressed (`recipientInQuietHours`); then QH off → same action delivers. + Generalize: **any "don't notify when X" setting (quiet hours, snooze, DND, per-type opt-out) must be enforced + server-side where the push is SENT** — verify the setting reaches Firestore and the sender honors it, not just the + client. (Reminder: the `users/{uid}` update rule is a **field allowlist** — a newly-synced pref field is silently + denied until added to it; confirm the write actually lands via an admin read, not just the UI toggle.) +- **Biometric app-lock** → enable → background-return / cold-start prompts for biometric; correct unlock proceeds, + cancel keeps it locked, disable removes the lock. (Security-relevant: no bypass.) +- **Edit profile** → name, sex/gender (inclusive options), photo upload → persists, reflects on the **partner's** side, + ciphertext/storage correct at rest. +- **Relationship / unpair** → unpair returns **both** to the unpaired state, **revokes decrypt** (D4), notifies the + partner (`partner_left`), makes couple data inaccessible; re-pair works cleanly. +- **Delete account** → confirmation → account + couple data cascade (`onUserDelete`), partner unpaired + notified, + re-create with the same email is a clean slate (overlaps G). +- **Security** → recovery-phrase reveal for **both** accepter and inviter (C-SEC-001), server-blind (D4); regenerate if + supported. **Subscription** → "Manage subscription" → Play (Pass K). **Privacy & Terms / data export** links open. +- Every toggle survives **process death + reinstall-with-data** (overlaps F). + +### Pass N — Daily question, reveal, check-ins & the other interactive features +The non-game interactive surfaces that have no functional home (Pass B is games only). Read +[Daily question lifecycle](docs/Engineering_Reference_Manual.md#daily-question-lifecycle). +- **Daily-question loop (the core daily ritual):** assignment (6 PM CST, `assignDailyQuestion`) → answer (each answer + type) → **both-answered gate** (neither sees the other's answer until both submit) → **mutual reveal** → per-question + **Discuss** thread (Pass L) → **Answer History** → **streak** increment + milestone celebration (`streak_milestone`) + → reveal `isRevealed` retry (the `onAnswerRevealed` push). Verify the premium daily-question fallback + (`DailyQuestionResolver` per-user) does **not** desync the couple's shared daily Q. +- **Relationship check-ins / Your Progress (outcomes):** baseline check-in (gated to show once), 30/60/90-day + follow-ups, slider inputs persist (`submitOutcomeCallable`), the progress view renders patterns/milestones, + `scheduledOutcomesReminder` fires, "No baseline yet" → check-in dialog (C-DARK-UI-002 area). Submit + Skip both work. +- **Bucket List:** add / check-complete / edit / delete an item; empty state; both-device sync; at rest encrypted (D1); + premium state if applicable (A). +- **Plan a Date / Date Builder:** build a plan (shape/steps) → save → **persists + the partner sees it**; date plan + + `date_swipes` ciphertext at rest (D1); submit-outcome path. +- **Activity / Together feed:** shared activity entries render + sort, unread count, navigation in/out. +- Each feature: empty / loading / error / not-paired states, two-device realtime sync, no stuck/orphaned state. + +### Pass O — Release build, store readiness & pre-launch security +**Everything above runs on the DEBUG build; the shippable artifact is the minified RELEASE build — test THAT.** This is +a pre-ship gate, not a per-round pass (run it before any store push and after build-config / dependency / keep-rule +changes). +- **Release/minified build (R8 + resource-shrink):** build the **release** APK/AAB and run `qa/entrypoint_smoke.sh` + + a representative slice of A–N on it. R8 can strip/obfuscate classes that **Firebase/Firestore/Tink/RevenueCat/Gson/ + kotlinx-serialization/Compose** need via reflection → crashes that never appear in debug. Verify keep-rules; 0 FATAL + on launch + each core flow; **upload the ProGuard mapping to Crashlytics** so release crashes deobfuscate. +- **Signing & packaging:** release signing config + upload key; build the **App Bundle**; install the signed AAB via + bundletool / Play internal-app-sharing and smoke it; 64-bit + target-SDK compliance. +- **App Check enforcement (pre-launch — currently OFF in dev per standing instruction; do NOT enable the dev project):** + in **staging**, enable enforcement on Firestore + Functions → a valid-token app works, a raw/no-token request → **403** + (extends D3/D5 beyond rules-only); confirm Play Integrity on a real device vs the debug provider. +- **Deep links / Android App Links:** `closer://` **and** any `https` App Links (`assetlinks.json`) open the correct + screen with auth/membership re-checked (overlaps E). +- **Permissions & manifest:** the manifest declares only what's used; runtime prompts (POST_NOTIFICATIONS, camera, mic, + Android-13+ photo picker / `READ_MEDIA_IMAGES`) appear and degrade gracefully when denied; `allowBackup=false` holds (D6). +- **Localization & formats (i18n):** strings are externalized (no hardcoded user-facing text), the longest translations + don't clip (overlaps C/J), **RTL** mirrors correctly, dates/numbers/**subscription prices+currency** format per locale + (overlaps K). Even if English-only today, confirm there's no layout that assumes English length. +- **Play Store readiness:** the **Data Safety** form matches the actual data flows + E2E encryption; privacy-policy URL + live; version code/name bumped; store listing/screenshots are the brand pass (H); min/target-SDK **device matrix** + (Methodology) covered. + +### Pass P — Content, copy & language quality (voice, grammar, inclusivity, the question bank) +**Wrong language is a BUG, not a "nice-to-have."** Typos, grammar/punctuation errors, off-brand or cold/salesy voice, +non-inclusive or assumptive wording, leaked placeholder/dev text, raw SDK/Firebase/RevenueCat errors shown to users, +copy that doesn't match behavior, and broken/duplicate/low-quality questions are all **defects → `ClaudeReport.md`**. +Only genuinely-working copy that *could be warmer/clearer* goes to `Future.md`. **Read first:** +`docs/brand/visual-identity.md` (**Store voice**) and `seed/questions/QUESTION_CONTENT_GUIDE.md` (**v3** — readability +test, no-AI-writing, duplicate prevention, variety, fun/relationship-first/premium rules). This is a recurring pass. +- **UI microcopy audit (every screen + state):** read ALL visible text — titles, labels, button verbs, helper text, + empty states, dialogs/confirmations, toasts, loading + **error** copy, and notification text — for: typos, grammar, + punctuation, capitalization/casing consistency, consistent terminology (feature names, "partner"/the partner's name, + "couple"), and copy that **matches the actual action/state** (a button says what it does; "Day N of 7" matches the real + state; correct names/counts/attribution). A label that misstates its destination or effect is a bug (overlaps C's CTA check). +- **No raw / placeholder / dev text ever reaches a user:** no Lorem, "TODO", debug strings, untranslated keys, or raw + exception/Firebase/RevenueCat error text surfaced to users (the A-OBS class) — always friendly copy. +- **Brand voice & tone (against `visual-identity.md` Store voice):** copy is **warm, quiet, equal, calm, specific** — a + private ritual for two. **Off-voice = finding:** cold/clinical, salesy/hype, urgent/alarmist, guilt- or streak-shaming, + competitive, surveillance-y, or "we'll FIX your relationship" promises. Scrutinize paywall, notification, and streak copy. +- **Inclusive & non-assumptive language:** no heteronormative or relationship-structure assumptions, no assumption about + who initiates or about bodies/ability/culture; gender-neutral where the design calls for it (the de-gendering effort — + `seed/degender_*.py`); sensitive topics (Desire Sync, intimacy) phrased with care + a consent framing, never crude or + clinical. Assumptive/exclusionary wording = bug. +- **Question-bank content QA (against `QUESTION_CONTENT_GUIDE.md` v3):** spot-check questions across **every category, + depth, and answer type** as the user SEES them (live-rendered, not just the DB) for: passes the **readability test**; + no **AI-writing tells**; **no duplicates / near-duplicates**; sensible **variety** + emotional mix; **answer options + complete + mutually exclusive + sensible** (no overlapping, joke-only, or placeholder options); the **answer type fits** + the prompt; **fun-rule / relationship-first** tone; and **no broken/empty/garbled/offensive/unsafe** prompts (cf. + `seed/fix_depth5_grammar.py`, `seed/validate_question_variety.py`). A shipped question that's broken, duplicated, + off-guide, or unsafe is a bug. +- **Legal / store / monetary copy accuracy:** paywall benefit claims are truthful (no over-promise); subscription terms + + renewal/price wording present and accurate (overlaps K); Privacy & Terms links resolve; store-voice rules hold. +- **Localization correctness (overlaps O):** no clipped/awkward strings from concatenation, correct pluralization, dates/ + numbers/currency per locale, RTL grammar intact — even if English-only today, flag any English-length/grammar assumption. +- **Method:** harvest strings from `res/values/strings.xml` + in-code literals AND read them **in context on-device** (a + string can be fine in isolation yet wrong/cramped/ambiguous in place). Routing: incorrect/off-voice/non-inclusive/ + placeholder/inaccurate/unsafe = **bug → `ClaudeReport.md`** (P2 default; **P1** if it misleads, blocks, leaks a raw + error, or is offensive/unsafe; P3 for pure nits); "could be warmer/clearer" → `Future.md`; new copy/voice work that's + out of scope → note it. + ## Reporting → ClaudeReport.md (living QA report) - Header: date, build, devices, round number + run-state header. -- One section per pass (A–J), each a table: **ID | Area | Screen/Route | Mode | Severity | Description | Repro +- One section per pass (A–P), each a table: **ID | Area | Screen/Route | Mode | Severity | Description | Repro | Evidence | Suggested fix | Status**. - Summary: counts by severity. Report only during passes — no fixes recorded until the fix phase. @@ -849,14 +1075,18 @@ Optimize every QA doc for a reader who has **5 seconds** to find the current sta - **New issues found while fixing** are logged (new ID), not silently fixed beyond scope — next re-QA round catches them. **Definition of done:** a **pass** is done when every coverage row is `pass`/`fail→id`/`not implemented→Future.md`/ -`blocked→id`; a **round** is done when all passes (A–J) are done; **flawless** = one full round with **zero open P0–P2 -and Passes D + E fully clean** (no open P0/P1 in I/J), **every game fully played through, every notification type -verified or explicitly `not implemented→Future.md`, all join-game navigation paths and all back-stack checks -verified**, **and `qa/entrypoint_smoke.sh` GREEN on both emulators (0 FAIL — every entry-point cold-start opens and -stays)**. Then stop (P3s optional). Don't re-open a clean pass within the same round. +`blocked→id`; a **round** is done when all **recurring** passes (A–N + P) are done; **flawless** = one full round with +**zero open P0–P2 and Passes D + E + L + P fully clean** (no open P0/P1 in I/J), **every game fully played through, +every notification type verified or explicitly `not implemented→Future.md`, chat (L) + the couple-shared premium gate +(A) + settings-take-effect (M) + content/language (P: no typos/off-voice/non-inclusive copy, question bank on-guide) +verified, all join-game navigation paths and all back-stack checks verified**, **and `qa/entrypoint_smoke.sh` GREEN on +both emulators (0 FAIL — every entry-point cold-start opens and stays)**. Then stop (P3s optional). **Pass O (release +build + store readiness) and Pass K's real-money path are pre-ship / real-device gates** — they don't block a per-round +"flawless" but **must be GREEN before any store submission**. Don't re-open a clean pass within the same round. ## Re-QA loop (until flawless) -After the fix phase, re-run Pass A–J (regression + confirm fixes). Repeat **fix → re-QA** rounds until a full +After the fix phase, re-run Passes A–N + P (regression + confirm fixes; Pass K money-path when a sandbox device is +available, Pass O when prepping a release). Repeat **fix → re-QA** rounds until a full round yields zero P0–P2 and Passes D+E fully clean. - **Prune on confirmation (Report hygiene):** the moment a re-QA round re-verifies a `Fixed` issue, **delete its row** from `ClaudeReport.md` (move its ID to the compact `Resolved & confirmed (archived — detail in git)` line) and diff --git a/ClaudeReport.md b/ClaudeReport.md index 83dbce76..a0ebfd20 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -1,5 +1,9 @@ # Claude QA Report — Full-App QA (living report) +> **Verdict (2026-06-27): R15 = gap-closing round on Passes L/M/N/P + smoke — found & FIXED M-001 (P2 quiet hours).** Targeted the previously-uncovered gaps (the new "flawless" bar needs L + P clean + M take-effect). **Found M-001 (P2):** "Quiet hours — 10 PM–8 AM, no notifications" did **not** suppress partner pushes when the recipient app was backgrounded/killed (the main case) — quiet hours was **local-only** (never synced server-side) and the OS shows the FCM `notification` block directly without running app code. **Fixed + verified live:** client now mirrors the window+timezone to `users/{uid}`; the 4 partner-action senders (`onMessageWritten`/`onAnswerWritten`/`onAnswerRevealed`/`onGameSessionUpdate`) suppress server-side via a **fail-open** `recipientInQuietHours()`; rules allowlist extended for the new fields. Live: QH ON → function logs `…is in quiet hours — suppressing`, 0 delivery; QH OFF → `notified partner`, delivery resumes; per-type chat toggle still suppresses (server-enforced). **Clean passes:** L (chat decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no `enc:` leak, at-rest `enc:v1:`); P (UI copy warm/inclusive, debug rows `BuildConfig.DEBUG`-gated, friendly error fallbacks; **question bank 6103 Qs — 0 empty, 0 dupes, 0 placeholders, complete answer configs, on-guide tone**); N (daily-Q + reveal both-answered gate render); smoke **6/6 GREEN both emulators**. 2 P3 brand-asset backlogs still open. **0 FATAL.** +> +> **Verdict (2026-06-27): R14 = full fresh A–J ClaudeQAPlan run — 0 open P0–P2, 0 new functional findings.** A pure-QA confirmation round (no code changes) on the R13 build. _(A follow-up 2026-06-27 brand-standards audit then opened **2 P3 brand-asset backlogs** — every image needs a dark variant; every icon must be custom — see the Issues section + `ClaudeBrandingReview.md`.)_ The 5 R13 fixes (C-DARK-UI-001/002/003, C-ART-EDGE-002, J-OBS) **held through R14's sweep → pruned**; the Premium-unlock modal held + re-verified. **Live results:** Pass A — premium ENFORCEMENT audited (all 6 gated features have a real gate, not just a badge; A-201 class closed) + free→Paywall confirmed for Date Match / Desire Sync / Question Packs + couple-shared unlock (Sam→QA) with modal + `subscription_entitlement_changed` push delivered live. Pass B — Desire Sync / How Well / Spin-the-Wheel full 2-device (mixed answer types incl free-text + multi-select + skipped-answer reveal), **first-finisher `partner_completed_part` nudge confirmed live**, Memory Lane create+seal (premium), Connection Challenges resume, Date Match deck (ToT carried from R13). Pass C — broad both-theme sweep + **decoupled-theme-art mandate** (system-light+app-Dark → dark UI + correctly-themed feathered art). Pass D — cornerstone LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest `enc:v1:`). Pass E — all triggers fired live with content-free copy to the right partner. Pass F — offline cache render + process-death recovery, both 0 FATAL. Pass I — jank 5.25%. Pass J — J-OBS 48dp holds. **0 FATAL across the whole run, both emulators.** + > **Verdict (2026-06-27): R13 = fixed the entire open backlog + full fresh A–J — FLAWLESS (0 open P0–P3).** Took over and **fixed all 3 open Codex dark-mode findings** (C-DARK-UI-001 P2 This-or-That redesign; C-DARK-UI-002 P3 check-in label/value; C-DARK-UI-003 P3 bottom-inset clipping) plus the 2 carried P3s (C-ART-EDGE-002 direct-call hero feathering; J-OBS 48dp touch targets), and **confirmed A-201 (P1) live → pruned**. Also shipped the **branding Premium-unlock modal** (`illustration_premium_unlock`, one-time, shown to BOTH partners on couple-shared activation). All verified live on both emulators (5554 dark / 5556 light), **0 FATAL**. Full fresh A–J run clean: Pass D security cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, chat at-rest `enc:v1:`); A premium gates → Paywall (Date Match + Desire Sync); B ToT full both themes + Wheel launch; I jank 6.43% (perf-safe); J 48dp confirmed. Diff is **UI-only** (no rules/functions/crypto change) → E/F/G carried. All app changes in the working tree — user commits. > **Verdict addendum (2026-06-27): ad hoc DARK-MODE UI/brand review on dedicated Codex emulator COMPLETE.** Built + installed the current debug APK on my own `CloserCodexQA` emulator (`emulator-5558`), forced system dark mode, created a fresh real paired couple through the app invite flow, and swept profile/onboarding, unpaired invite, paired Home, Play, This-or-That, Settings, Notifications, Paywall, Messages, and Today. **Button text is generally readable** across profile/Home/Settings/Notifications/Messages/Paywall, but the sweep found **1 open P2**: This-or-That active gameplay has low-contrast dark option text and an off-brand diagonal/circle backdrop crossing the prompt. Also found **2 open P3s**: first-launch check-in modal label/value collision and recurring bottom-inset clipping on scroll content near nav/gesture areas. Logs checked after navigation/game entry: **0 app FATAL/ANR/force-finish**; only uiautomator/system noise plus a non-crashing BillingClient unbind warning. @@ -14,6 +18,8 @@ > to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`. ## Run-state (current) +- **R15 (2026-06-27) — gap-closing round (Passes L/M/N/P + regression smoke) — found & FIXED M-001 (P2).** Build current (HEAD `c31eea2` + R15 working-tree changes rebuilt+installed both emulators); baseline both FREE, 0 active sessions. **Smoke** ✅ 6/6 GREEN both (launcher + 5 notif cold-starts). **M (settings take-effect)** — **M-001 (P2) quiet hours didn't suppress backgrounded/killed partner pushes** (local-only window; OS shows `notification` block w/o app code). **FIXED + verified live:** client mirrors window+tz → `users/{uid}`; 4 partner-action senders suppress via fail-open `recipientInQuietHours()`; rules allowlist extended. Live: QH ON → fn log `is in quiet hours — suppressing`, 0 delivery; QH OFF → `notified partner`. Per-type chat toggle re-confirmed server-enforced (toggle off → 0 delivery; field flips in Firestore). Theme/DataStore persistence across relaunch ✅. Biometric lock code-sound (no compose bypass; observation: re-locks on cold-start, not plain background→resume → `Future.md`). **L (chat E2E)** ✅ decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no `enc:` leak, at-rest `enc:v1:`. **N** ✅ daily-Q + reveal both-answered gate render (outcomes/bucket-list/date-builder carried — render-clean prior rounds). **P (content/language)** ✅ UI copy warm/inclusive, debug rows `BuildConfig.DEBUG`-gated, friendly error fallbacks; **question bank 6103 Qs: 0 empty/0 dupes/0 placeholders/complete answer configs/on-guide tone.** **D1 at-rest** ✅ messages/preview/capsules `enc:v1:`. **0 FATAL.** Uncommitted (user commits): functions (`quietHours.ts` + 4 senders), `firestore.rules`, client (`FirestoreUserDataSource`/`UserRepository(+Impl)`/`NotificationSettingsScreen`). **Functions + rules DEPLOYED to prod (standing auth).** NEXT (R16): confirm M-001 holds → prune; close remaining N/L sub-items (failed-send/offline retry, delete-msg, outcomes loop) + the 2 P3 brand backlogs. +- **R14 (2026-06-27) — full fresh A–J ClaudeQAPlan run (pure QA, no code changes) — FLAWLESS, 0 open P0–P3, 0 new findings.** Baseline both FREE, 0 active sessions; R13 build on both emulators (5554 dark / 5556 light). **A** ✅ premium enforcement audited (6/6 features gated, not just badged; A-201 class closed) + free→Paywall (Date Match / Desire Sync / Question Packs) + couple-shared unlock (Sam prem→QA free unlocks Desire Sync + a premium pack; modal + `subscription_entitlement_changed` push delivered live to QA). **B** ✅ Desire Sync + How Well + Spin-the-Wheel full 2-device (True/False+Yes/No+multi-select+free-text answer types; skipped-answer reveal; **first-finisher `partner_completed_part` nudge confirmed in Sam's queue**), Memory Lane create+seal (premium), Connection Challenges resume (Day 4 · 🔥2), Date Match deck; ToT carried R13. **C** ✅ broad both-theme + **decoupled-theme-art mandate** (system-light+app-Dark → dark UI + correctly-themed feathered Today hero); no nav dead-ends (back-from-Home exits = correct). **D** ✅ LIVE non-member 403 ×2 · self-grant 403 · member 200 · chat at-rest `enc:v1:` (game/capsule at-rest carried R10/R12, crypto unchanged). **E** ✅ all triggers fired live, content-free copy to right partner (started/completed_part/finished + entitlement). **F** ✅ offline Today-from-cache + `am kill` recovery, 0 FATAL. **I** ✅ jank 5.25%. **J** ✅ J-OBS 48dp holds. **0 FATAL whole run.** The 5 R13 fixes held → pruned. Uncommitted (user commits): R13's 16 modified + `PremiumUnlockOverlay.kt` + `illustration_premium_unlock.png` (R14 added no code). - **R13 (2026-06-27) — backlog fix pass + full fresh A–J — FLAWLESS (0 open P0–P3).** Took over the open Codex dark-mode backlog and shipped it all (working tree, both emulators rebuilt+installed; baseline both FREE, 0 active sessions): **C-DARK-UI-001** (ToT dark redesign — theme-aware backdrop/options/chips/versus/progress/pills) · **C-DARK-UI-002** (check-in label/value weight) · **C-DARK-UI-003** (Play/Home/Paywall bottom clearance) · **C-ART-EDGE-002** (8 opaque heroes routed through `BrandIllustration` feather) · **J-OBS** (composer/voice/retry buttons → 48dp). Confirmed **A-201** live → pruned. Shipped the **Premium-unlock modal** (`ui/components/PremiumUnlockOverlay.kt`, hosted in `AppNavigation`; driven off `CouplePremiumChecker`, one-time via a new `premiumUnlockCelebrated` `SettingsRepository` flag) — verified live on BOTH purchaser (5554) and partner (5556) + one-time gate (dismiss→relaunch no re-show). **A–J:** A ✓ (Date Match + Desire Sync gates → Paywall) · B ✓ (ToT full both themes; Wheel launch clean) · C ✓ (extensive both-theme sweep) · D ✓ LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest `enc:v1:`) · E/F/G carried (diff is UI-only; no rules/functions/crypto/auth change) · H ✓ (ToT brand + modal + heroes) · I ✓ (jank 6.43%) · J ✓ (48dp). **0 FATAL both devices.** Uncommitted (user commits): 16 modified + `ui/components/PremiumUnlockOverlay.kt` + `res/drawable-nodpi/illustration_premium_unlock.png`. - **Ad hoc dark-mode UI/brand sweep (2026-06-27, Codex-owned emulator `emulator-5558`):** current debug APK installed, dark mode forced, fresh real paired users created through invite flow (`Codex Dark` + `River Dark`). Swept profile, invite, paired Home, Play, This-or-That, Settings, Notifications, Paywall, Messages, Today. **0 app FATAL/ANR/force-finish in logcat.** Findings added below: C-DARK-UI-001 (P2), C-DARK-UI-002 (P3), C-DARK-UI-003 (P3). Screenshots captured at `/tmp/closer-dark-04-after-permission.png` through `/tmp/closer-dark-25-today.png`. `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.` @@ -41,35 +47,26 @@ |---|---|---| | P0 | 0 | 0 | | P1 | 0 | 0 | -| P2 | **0** | **1** (C-DARK-UI-001) | -| P3 | **0** | **4** (J-OBS, C-ART-EDGE-002, C-DARK-UI-002, C-DARK-UI-003) | +| P2 | **0** | **1** (M-001 quiet hours) | +| P3 | **2** (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM) | **0** | -_R13: A-201 (P1) confirmed live → pruned to the archived line. The 5 fixes above are verified-live this round; they live one confirmation round, then prune._ +_R15: found + FIXED **M-001** (P2 quiet hours — fixed + verified live, pending 1 confirmation round). **0 open P0–P2.** 2 P3 +brand-asset backlogs still open (every image needs a dark variant; every icon must be custom) — full asset lists in +`ClaudeBrandingReview.md`._ -## Issues — ad hoc dark-mode UI/brand review (2026-06-27) -> Dedicated emulator: `CloserCodexQA` / `emulator-5558`; fresh paired users created through the real invite flow; system dark mode forced. This pass focused on brand fit, image integration, dark-mode button/text visibility, route integrity, and logs after navigation/game launch. **Clean surfaces:** profile creation, invite, paired Home, Settings ("Connected with Codex Dark"), Notifications, Messages, and Paywall primary content/buttons are readable and on-brand in dark mode. **Known C-ART-EDGE-002 reproduced:** Today's direct-call hero art still appears as a bright pasted light tile on dark, already tracked below. +## Issues — open (brand-asset backlogs, P3) +> Surfaced by the 2026-06-27 brand standards audit (new Pass H/Pass C mandates). Both are **brand-quality defects** +> (the app functions): light-only art shows on dark; generic icons aren't on-brand. Asset lists + prompts to make live +> in `ClaudeBrandingReview.md` (Image theme-variant coverage · Icon/glyph audit). -| ID | Sev | Area | Description | Evidence / repro | Suggested fix | Status | -|---|---|---|---|---|---|---| -| C-DARK-UI-001 | P2 | This or That / dark active gameplay | **Dark-mode gameplay still looks off-brand and weakly legible.** The diagonal line + two-circle backdrop cuts through the question and reads like a forgotten placeholder, especially in dark mode. A/B option labels and option text are too dim on the dark cards; mood-picker duration chips are also low-contrast. This is the one screen where dark button/choice text does not meet the rest of the app's standard. | `emulator-5558`: Play → This or That → choose mood/count → active question. Screenshot: `/tmp/closer-dark-20-this-or-that-game.png`; setup chip contrast: `/tmp/closer-dark-19-this-or-that-mood.png`. Logcat after launch: 0 app FATAL/ANR/force-finish. | Replace the placeholder-like line/circle motif with branded game art or a subtle surface treatment; use theme tokens with high-contrast option text for dark; verify A/B cards, selected/pressed/disabled states, and all mood/count chips in both themes. | **Fixed + verified live R13 (working tree)** — `ThisOrThatScreen.kt`: `ChoicePromptBackdrop` redrawn as a soft theme-aware glow + two faint paired-card silhouettes low in the frame (no line/diagram, never crosses the prompt); `OptionCard` A/B now map to `colorScheme.primary`/`secondary` with high-contrast `onSurface` body text + visible accent borders + rich filled selected state; `VersusBadge`/progress/pills/mood number-circle/`TotLengthChips` all theme-aware. Verified both themes (5554 dark / 5556 light): backdrop subtle, options/chips legible, 0 FATAL. Added light+dark previews. | -| C-DARK-UI-002 | P3 | Paired Home / first-launch check-in modal | **Slider label and value collide in dark mode.** The second check-in row reads visually like `communicate?5` because the label runs into the numeric value. Button text ("Submit", "Skip for now") is readable, but the modal needs spacing/layout polish. | `emulator-5558`: first paired Home launch opened the quick check-in modal. Screenshot: `/tmp/closer-dark-16-paired-home.png`. | Give slider rows a stable two-column layout or wrap/value-align the score independently; verify at default and larger font scales. | **Fixed R13 (working tree)** — `OutcomeCheckInDialog.kt` `OutcomeSlider`: label given `Modifier.weight(1f)` and the row switched to `spacedBy(12.dp)`, so the numeric value keeps its own column and can never collide with a long label. Verified-by-construction (the one-time baseline-check-in dialog is gated `outcomeBaselineShownAt!=0` on these installs; the change is a deterministic layout fix). | -| C-DARK-UI-003 | P3 | Insets / bottom navigation and gesture area | **Several dark-mode scroll surfaces render useful content too close to or under bottom nav/gesture areas.** Play clips the lower "Desire Sync" card behind bottom nav; unpaired Home's bottom CTA is too tight to the nav area; Paywall's "Already subscribed? / Restore" card starts under the gesture area. Text is mostly readable, but it looks unfinished and can hide tap targets. | Screenshots: `/tmp/closer-dark-18-play.png`, `/tmp/closer-dark-07-post-profile.png`, `/tmp/closer-dark-23-paywall.png`. | Audit scaffold/content padding for bottom nav + system gesture insets; add consistent `navigationBarsPadding`/bottom spacer to scrollable content and modal/fullscreen paywall surfaces. | **Fixed + verified live R13 (working tree)** — added explicit bottom clearance to the three flagged scroll containers: Play `LazyColumn` `contentPadding = bottom 28.dp`; Home column `bottom 36.dp`; Paywall column `bottom 40.dp`. Verified live: Play last row (Bucket List / Past Games) clears the bottom nav; Paywall Restore/legal clears the gesture area. | - -## Issues — R12 (0 open P0–P2 = FLAWLESS · A-201 P1 fixed+verified-live pending 1 confirm · 2 open P3 [J-OBS, C-ART-EDGE-002]) -> R12 was a FRESH FULL A–J run. Found A-201 (P1, Date Match premium bypass) — **fixed + verified live this round** (gated -> via `CouplePremiumChecker`). C-DARKART-001 (P2) + C-ART-EDGE-001 (P3), fixed in R11, held through R12's visual sweep and -> are **pruned** to the archived-ID line below (detail in git / working tree). New P3 **C-ART-EDGE-002** (direct-call hero -> hard edges) is deferred polish. Remaining open = 2 non-blocking P3s. **A-201 fix + R11 art fixes are in the working tree -> (user commits).** - -| ID | Sev | Area | Description | Repro | Suggested fix | Status | -|---|---|---|---|---|---|---| -| A-201 | P1 | Premium / Date Match bypass (Pass A, R12) | **Premium date ideas are NOT gated — free users can view, swipe, like and match them with no paywall.** `DateIdea.isPremium` is documented as "requires an active premium entitlement," but `DateMatchRepositoryImpl.getDateIdeas()` returns `DateIdeaSeed.all` (premium ideas included, **no entitlement filter**), `DateMatchViewModel` loads them with no `CouplePremiumChecker`, and `DateMatchScreen` only renders a cosmetic `PremiumBadge()` — no lock overlay, no paywall on like/super-like. So the premium tier for Date Match is unenforced. **Escaped prior Pass A rounds** (which asserted "all gates use CouplePremiumChecker" — Date Match has NO gate). (Plan buckets "premium bypass" as P0; filed P1 as it's a content-tier subset, no security/data impact.) | 5554 (QA **free**): Play → Date Match → reject through deck → reach a **★ Premium** idea ("night camping getaway") → Like (heart) → **no paywall**, swipe accepted, deck advanced to the next premium idea ("Zipline canopy tour"); 0 FATAL, no `PERMISSION_DENIED`. | Gate premium date ideas through `CouplePremiumChecker`: either filter `isPremium` ideas out of a free couple's deck, OR intercept like/super-like on a premium idea → route to Paywall when neither partner is premium (couple-shared). Mirror the Desire Sync / Question-pack gate. | **Confirmed live R13 → PRUNED** (added to the archived-ID line). R13 re-verify: free QA → Date Match → Love a ★Premium idea ("Overnight camping getaway") → **Paywall**, deck did **not** advance (returned to the same premium card on back); 0 FATAL. Fix is in the working tree (`DateMatchViewModel`/`DateMatchScreen`, committed `2cd0af6`-era + this confirm). | -| 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. | **Fixed + verified live R13 (working tree)** — routed the **8 opaque** (RGB, no-alpha) hero tiles through `BrandIllustration(tile=true)`: daily_question (the repro), couple_paywall, couple_subscription, couple_onboarding, partner_activation, tonight_partner_prompt, couple_invite, together_empty. Confirmed via `identify` that spin_wheel / streak_milestone / reveal_celebration are transparent-or-celebration and left as-is (they float). Verified live: Today "Weekend Side Quest" hero now feathers into the dark surface (no hard bright block); paywall hero blends. | -| 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)`). | **Fixed + verified live R13 (working tree)** — `ChatComposer` media buttons (40→48dp default), voice play/pause (36→48), recording-cancel (44→48), and the failed-message retry/dismiss (28→48) in `ChatComponents.kt` + `ConversationScreen.kt`. Verified live: composer clickables now measure 126px = 48dp on both axes; layout clean (premium-lock badges intact). | +| ID | Sev | Area | Description | Suggested fix | Status | +|---|---|---|---|---|---| +| M-001 | P2 | Settings / notifications | **Quiet hours did not suppress backgrounded/killed partner pushes.** "Quiet hours — 10 PM–8 AM, no notifications" was stored **local-only** (DataStore); partner pushes carry a `notification` block the OS shows directly when the recipient is backgrounded/killed, and the only client check (`PartnerNotificationManager.isInQuietHours`) runs **foreground-only** (`AppMessagingService.onMessageReceived`). So the "no notifications" promise was broken for the main case. Repro: Sam QH ON @22:28 CST, backgrounded → QA chat → "QA sent a message" posted to Sam's shade. | Client mirrors window+tz to `users/{uid}`; Cloud Functions (`onMessageWritten`/`onAnswerWritten`/`onAnswerRevealed`/`onGameSessionUpdate`) suppress via fail-open `notifications/quietHours.ts:recipientInQuietHours()`; `firestore.rules` user-doc allowlist extended for `quietHours*`+`timezone`. | **Fixed — verified live R15** (fn log suppress vs notify; deployed prod). Pending 1 confirm. | +| BRAND-DARK-COVERAGE | P3 | Art / theme | Most illustrations are **light-only** — only 12 of ~25 have a `drawable-night-nodpi/` dark variant. All `illustration_couple_*` heroes (paywall/subscription/onboarding/invite/history), `daily_question`, `partner_activation`, `tonight_partner_prompt`, `together_empty`, and **all 10 `pack_art_*` banners** show the **light/pink image on a dark screen** (feathered edges don't change the image colors). | Generate dark/aubergine-palette variants for each light-only asset → `drawable-night-nodpi/` (identical filename); `BrandIllustration` auto-selects per in-app theme. Re-run the decoupled-theme check. List in `ClaudeBrandingReview.md`. | **Open (P3)** | +| BRAND-ICON-CUSTOM | P3 | Icons / brand | **~60 distinct generic Material icons** across ~201 call sites (generic hearts `Favorite`/`FavoriteBorder`, `Person`, `Lock`, `Star`, `PlayArrow`, `ArrowBack`, …) — these are placeholders, not the Closer brand. | Replace each with a bespoke `glyph_*` in the house style (`ImageVector.vectorResource` + `Icon(tint)`), highest-traffic first; ship bar = **0 generic Material icons**. Backlog table in `ClaudeBrandingReview.md`. | **Open (P3)** | ## Resolved & confirmed (archived — full detail in git history) -A-001 · A-003 · **A-201** · 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 (R13 pruned **A-201** [Date-Match premium ideas ungated → now gated to Paywall via `CouplePremiumChecker`] — fixed R12, confirmed live R13; in working tree) (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**.) +A-001 · A-003 · **A-201** · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · **C-DARKART-001** · **C-DARK-UI-001** · **C-DARK-UI-002** · **C-DARK-UI-003** · C-DS-001 · **C-ART-EDGE-001** · **C-ART-EDGE-002** · **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** · **J-OBS** — all fixed and re-verified (R14 pruned the 5 R13 fixes — **C-DARK-UI-001** ToT dark redesign · **C-DARK-UI-002** check-in label/value · **C-DARK-UI-003** bottom-inset clearance · **C-ART-EDGE-002** 8 opaque heroes feathered · **J-OBS** 48dp touch targets — held through R14's full A–J sweep; in working tree) (R13 pruned **A-201** [Date-Match premium ideas ungated → now gated to Paywall via `CouplePremiumChecker`] — fixed R12, confirmed live R13; in working tree) (R12 pruned C-DARKART-001 [in-app-theme `-night` art] + C-ART-EDGE-001 [feathered edges] — fixed R11, held through R12 visual sweep; in working tree) (R11 pruned the 5 R10 P2 fixes — C-HOME-001 single Home card · C-NAV-002 `popUpTo(WHEEL_SESSION){inclusive}` present + R10-live · C-NAV-003 single app bar re-confirmed live · C-PW-001 dark paywall pills legible re-confirmed live · C-SEC-001 recovery row active for accepter re-confirmed live — all committed in `9c84c36`; E-GAME-003 `onGamePartFinished` deployed + committed `2cd0af6`) (E-GAME-002 confirmed live R10: `startNotifiedAt` set + partner_started_game queued to right partner + foreground banner + Join→joined active ToT at same Q1; commits 6e79cd9/38fdc6d) (commits in history; F-RACE-001 re-confirmed R8; **I-001** query→`whereIn(dayKeys)` + **I-002** Long-score→`Number.toInt()`, fixed `ab29f6b`, re-confirmed live R9: 0 outcomes denials/CCE). Pruned per the one-confirmation-round rule. (C-OBS / `outcomes` list / SubscriptionScreen per-user gate = investigated, **not bugs**.) ## Security cornerstone — clean (Pass D, deep dive, Round 7) - **D1 at-rest:** chat text + `lastMessagePreview` + all 4 game-answer collections (ToT / How Well / Desire Sync / Wheel, both users) + Memory Lane capsules + date-swipe actions = `enc:v1:`. No plaintext content; only metadata in clear. @@ -112,6 +109,7 @@ also set an `ACTION_VIEW` + `closer://` **data Uri** on the notification intent: path (no Uri) loads results; **warm tap from Settings now routes** (was the stuck case). ## Round history (one line each) +- **R14 (2026-06-27) — full fresh A–J run (pure QA, no code), FLAWLESS, 0 new findings.** Confirmation round on the R13 build: A premium enforcement audit + couple-shared unlock + entitlement push live; B 3 async games full 2-device + first-finisher nudge + Memory Lane/CC/Date Match core; C decoupled-theme-art mandate; D cornerstone live (403s + enc:v1:); E triggers/copy live; F offline + process-death; I jank 5.25%; J 48dp holds. 0 FATAL both emulators. The 5 R13 fixes held → pruned to the archived line. - **R13 (2026-06-27) — open-backlog fix pass + full fresh A–J, FLAWLESS (0 open P0–P3).** Fixed all 5 carried/open UI items (C-DARK-UI-001 ToT dark redesign · C-DARK-UI-002 check-in label · C-DARK-UI-003 bottom insets · C-ART-EDGE-002 8 opaque heroes feathered · J-OBS 48dp targets), confirmed A-201 live → pruned, and shipped the branding **Premium-unlock modal** (one-time, both partners, couple-shared). A–J: D security cornerstone re-verified LIVE (non-member 403, self-grant 403, at-rest `enc:v1:`); premium gates → Paywall; ToT both themes; jank 6.43%. Diff UI-only → E/F/G carried. 0 FATAL both emulators. App changes in working tree (user commits). - **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; @@ -154,4 +152,4 @@ also set an `ACTION_VIEW` + `closer://` **data Uri** on the notification intent: ## Operational constants - **Execution mode:** autonomous run-to-completion — don't stop; fix blockers inline; cycle fix→re-QA until flawless. Don't hand back when context fills — re-read this run-state + coverage after any compaction. Commit before interruptible work; recover stuck sessions via the session-start ritual. - **Standing authorization (user, 2026-06-24):** may `firebase deploy --only firestore:rules` + has admin access (Firestore reads/writes/seeds + entitlement toggles) — run without pausing. Only the macOS requirement for iOS (Parts 2/3) is a hard stop. -- **Hardening backlog → Future.md:** App Check not enforced on Firestore; `users/{uid}` update rule allows arbitrary non-`hasPremium` fields (tighten to a field allowlist). +- **Hardening backlog → Future.md:** App Check not enforced on Firestore. _(Correction R15: the `users/{uid}` update rule is NOT open — it enforces a **field allowlist** (`firestore.rules` ~L198, `hasOnly([...])`); R15 extended it for `quietHours*`+`timezone`. Keep that list in sync with `FirestoreUserDataSource` when adding a client-written field.)_ diff --git a/Future.md b/Future.md index f4893c5c..b9435f1e 100644 --- a/Future.md +++ b/Future.md @@ -27,6 +27,13 @@ Improvement & feature ideas surfaced while QA-testing as a consumer (each works Check token** (raw Firestore REST) returned `200` for a member — so rules are the *sole* gate. Rules correctly deny non-members/cross-couple (all `403`), so this is not a live hole, but enabling App Check enforcement on Firestore would block non-app clients entirely (defense-in-depth). *Prompted by:* R7 D3 raw-API angle. +- **Biometric app-lock re-locks on cold-start/process-death but maybe not plain background→resume.** R15 code review: + `MainActivity` gates `AppNavigation` behind `BiometricLockScreen` when `biometricLoginEnabled` and `sessionVerified` + is false; `sessionVerified` is a `remember{}` that resets on Activity recreation (cold-start, process death) — so the + lock re-arms there — but a plain background→foreground without recreation keeps `sessionVerified = true`, so it may not + re-prompt. Architecturally sound (no compose-tree bypass; content isn't composed until unlocked), but consider + re-locking on `ON_STOP`/timeout so a picked-up unlocked phone re-prompts. *Prompted by:* R15 Pass M code audit (not + live-tested — emulator has no enrolled biometric). > Artwork to generate (ChatGPT prompts, house-style-matched) lives in `ClaudeBrandingReview.md`, not here. diff --git a/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt index 39c62df7..468348ca 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt @@ -164,6 +164,32 @@ class FirestoreUserDataSource @Inject constructor(private val db: FirebaseFirest .addOnFailureListener { cont.resumeWithException(it) } } + /** + * Mirror the user's quiet-hours window to their user doc so Cloud Functions can suppress + * partner-action pushes server-side (M-001 — the OS shows a `notification` block directly when + * the recipient is backgrounded, so client-side suppression alone can't keep the "no + * notifications" promise). Window is stored as minutes-from-midnight + the device timezone id. + */ + suspend fun updateQuietHours( + uid: String, + enabled: Boolean, + startMinutes: Int, + endMinutes: Int, + timezone: String + ): Unit = suspendCancellableCoroutine { cont -> + userRef(uid).set( + mapOf( + "quietHoursEnabled" to enabled, + "quietHoursStartMinutes" to startMinutes, + "quietHoursEndMinutes" to endMinutes, + "timezone" to timezone + ), + SetOptions.merge() + ) + .addOnSuccessListener { cont.resume(Unit) } + .addOnFailureListener { cont.resumeWithException(it) } + } + suspend fun deleteUserData(uid: String): Unit = suspendCancellableCoroutine { cont -> userRef(uid).delete() diff --git a/app/src/main/java/app/closer/data/repository/UserRepositoryImpl.kt b/app/src/main/java/app/closer/data/repository/UserRepositoryImpl.kt index 7ebc16ab..e1d7e1d3 100644 --- a/app/src/main/java/app/closer/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/app/closer/data/repository/UserRepositoryImpl.kt @@ -42,6 +42,14 @@ class UserRepositoryImpl @Inject constructor( override suspend fun updateNotificationPrefs(uid: String, partnerAnswered: Boolean, chatMessage: Boolean) = dataSource.updateNotificationPrefs(uid, partnerAnswered, chatMessage) + override suspend fun updateQuietHours( + uid: String, + enabled: Boolean, + startMinutes: Int, + endMinutes: Int, + timezone: String + ) = dataSource.updateQuietHours(uid, enabled, startMinutes, endMinutes, timezone) + override suspend fun clearCoupleId(uid: String) = dataSource.clearCoupleId(uid) override suspend fun deleteUserData(uid: String) = dataSource.deleteUserData(uid) diff --git a/app/src/main/java/app/closer/domain/repository/UserRepository.kt b/app/src/main/java/app/closer/domain/repository/UserRepository.kt index 579ff9e4..6f84c5f2 100644 --- a/app/src/main/java/app/closer/domain/repository/UserRepository.kt +++ b/app/src/main/java/app/closer/domain/repository/UserRepository.kt @@ -15,6 +15,7 @@ interface UserRepository { suspend fun storeFcmToken(uid: String, token: String) suspend fun storeTokenMetadata(uid: String, token: String, metadata: TokenRegistrar.DeviceMetadata) suspend fun updateNotificationPrefs(uid: String, partnerAnswered: Boolean, chatMessage: Boolean) + suspend fun updateQuietHours(uid: String, enabled: Boolean, startMinutes: Int, endMinutes: Int, timezone: String) suspend fun clearCoupleId(uid: String) suspend fun deleteUserData(uid: String) } diff --git a/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt index 4d755e0d..4de7d679 100644 --- a/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt @@ -9,9 +9,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.util.TimeZone import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -103,6 +105,14 @@ class NotificationSettingsViewModel @Inject constructor( fun toggleQuietHours(on: Boolean) = viewModelScope.launch { settingsRepository.setQuietHours(on) + syncQuietHours() + } + + init { + // Backfill the recipient-side quiet-hours window/timezone to Firestore so the server can + // honor it for backgrounded/killed delivery (M-001) — covers users who enabled quiet hours + // before this build, the next time they open Notification settings. + syncQuietHours() } private fun syncNotifPrefs(partnerAnswered: Boolean, chatMessage: Boolean) { @@ -111,6 +121,23 @@ class NotificationSettingsViewModel @Inject constructor( runCatching { userRepository.updateNotificationPrefs(uid, partnerAnswered, chatMessage) } } } + + /** Mirror the local quiet-hours window + device timezone to the user doc (M-001). */ + private fun syncQuietHours() { + val uid = authRepository.currentUserId ?: return + viewModelScope.launch { + runCatching { + val qh = settingsRepository.settings.first().quietHours + userRepository.updateQuietHours( + uid = uid, + enabled = qh.enabled, + startMinutes = qh.startHour * 60 + qh.startMinute, + endMinutes = qh.endHour * 60 + qh.endMinute, + timezone = TimeZone.getDefault().id + ) + } + } + } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/docs/Engineering_Reference_Manual.md b/docs/Engineering_Reference_Manual.md index a457114e..975dc7ba 100644 --- a/docs/Engineering_Reference_Manual.md +++ b/docs/Engineering_Reference_Manual.md @@ -1144,6 +1144,11 @@ SCRIPTS.md These are bugs that cost real debugging time and are easy to re-introduce if you don't know they existed. Before changing the relevant area, re-read the linked fix commit and the QA report entry. Format: **ID** — what it was — where it lives now. +### M-001 — quiet hours must be enforced SERVER-SIDE (a `notification` block bypasses client code when backgrounded) +**Symptom (R15)**: "Quiet hours — 10 PM–8 AM, no notifications" did nothing for the case it exists for. With quiet hours ON and the recipient backgrounded/killed, partner chats/answers still posted to the shade. **Root cause**: quiet hours was **local-only** (`SettingsDataStore`, never written to Firestore) and the only check, `PartnerNotificationManager.isInQuietHours`, runs inside `AppMessagingService.onMessageReceived` — which FCM invokes **only in the foreground**. Partner pushes carry a `notification` block, so when the app is backgrounded/killed the **OS renders it directly** and no app code runs → the window is never consulted. The Settings copy was therefore a false promise for the primary scenario. +**Fix (R15)**: enforce server-side. (1) Client mirrors the window to the recipient's user doc — `FirestoreUserDataSource.updateQuietHours()` writes `quietHoursEnabled` + `quietHoursStartMinutes` + `quietHoursEndMinutes` + `timezone` (`TimeZone.getDefault().id`); `NotificationSettingsViewModel` syncs on toggle **and** on init (backfill). (2) `functions/src/notifications/quietHours.ts:recipientInQuietHours(userData)` computes the recipient's local now via `Intl.DateTimeFormat({timeZone})`, handles the midnight-crossing window, and is **FAIL-OPEN** (any missing/malformed field → returns false → deliver). (3) The four partner-action senders skip when it returns true: `onMessageWritten`, `onAnswerWritten`, `onAnswerRevealed`, `onGameSessionUpdate`. (4) `firestore.rules` user-doc update allowlist extended for the four new fields. +**Re-introduction risk**: (a) Client-side suppression of a server `notification`-block push only ever works foreground — any "don't notify when X" rule (quiet hours, snooze, DND) must be enforced where the push is **sent** (Cloud Functions), or sent as data-only (unreliable when killed). (b) **The `users/{uid}` update rule is a field allowlist** (`firestore.rules` ~L198, `hasOnly([...])`) — a new client-written user-doc field is silently `PERMISSION_DENIED` until added there *and* to `FirestoreUserDataSource`. (c) Keep the helper fail-open so a bug can only under-suppress (deliver), never wrongly drop a notification. (d) Scheduled/promotional senders (`reengagement`) already had their own quiet-hours check — the gap was the real-time partner-action path. + ### C-DARK-UI-001 — game surfaces must use theme tokens, not fixed palette darks **Symptom**: This-or-That active gameplay was off-brand and weakly legible in dark mode — option body text and mood/duration chips used fixed `CloserPalette.PurpleDeep`/`PinkAccentDeep` (dark values) on a dark surface, and `ChoicePromptBackdrop` drew a diagonal line + two circles that read like a technical diagram crossing the prompt. **Fix (R13)**: `ThisOrThatScreen.kt` — A/B options map to `MaterialTheme.colorScheme.primary`/`secondary` (light lavender/pink in dark, rich purple/magenta in light) with high-contrast `onSurface` body text + visible accent `BorderStroke` + a filled `onPrimary`/`onSecondary` selected state; `VersusBadge`, progress, the `N/total`+title pills, the mood number-circle, and `TotLengthChips` all read from `colorScheme`; `ChoicePromptBackdrop` redrawn as a soft glow + two faint paired-card silhouettes (low alpha, never crossing the prompt). Light+dark previews added. diff --git a/firestore.rules b/firestore.rules index 23059007..ce2f38d3 100644 --- a/firestore.rules +++ b/firestore.rules @@ -198,7 +198,9 @@ service cloud.firestore { && request.resource.data.diff(resource.data).affectedKeys().hasOnly([ 'email', 'displayName', 'photoUrl', 'sex', 'partnerId', 'coupleId', 'plan', 'createdAt', 'lastActiveAt', 'fcmToken', - 'notifPartnerAnswered', 'notifChatMessage' + 'notifPartnerAnswered', 'notifChatMessage', + // M-001: quiet-hours window mirrored for server-side push suppression. + 'quietHoursEnabled', 'quietHoursStartMinutes', 'quietHoursEndMinutes', 'timezone' ]); // Entitlements written server-side only (RevenueCat webhook via Admin SDK). diff --git a/functions/dist/games/onGameSessionUpdate.js b/functions/dist/games/onGameSessionUpdate.js index 0b565e56..9714c763 100644 --- a/functions/dist/games/onGameSessionUpdate.js +++ b/functions/dist/games/onGameSessionUpdate.js @@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.onGamePartFinished = exports.onGameSessionUpdate = void 0; const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); +const quietHours_1 = require("../notifications/quietHours"); /** * Firestore trigger that notifies partners when a game session is created or completed. * @@ -81,6 +82,8 @@ exports.onGameSessionUpdate = functions.firestore const partnerBName = (_f = (_e = userB.data()) === null || _e === void 0 ? void 0 : _e.displayName) !== null && _f !== void 0 ? _f : 'Partner B'; const avatarA = (_g = userA.data()) === null || _g === void 0 ? void 0 : _g.photoUrl; const avatarB = (_h = userB.data()) === null || _h === void 0 ? void 0 : _h.photoUrl; + // M-001: per-recipient quiet-hours lookup ("no notifications" promise). Fail-open. + const dataFor = (uid) => (uid === partnerA ? userA.data() : userB.data()); const currentData = (_j = change.after.data()) !== null && _j !== void 0 ? _j : {}; if (!change.after.exists) return; // deletion — nothing to notify @@ -109,7 +112,12 @@ exports.onGameSessionUpdate = functions.firestore const recipientId = startedBy === partnerA ? partnerB : partnerA; const starterName = startedBy === partnerA ? partnerAName : partnerBName; const starterAvatar = startedBy === partnerA ? avatarA : avatarB; - await notifyPartner(db, messaging, recipientId, starterName, gameType, 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, starterAvatar, sessionId); + if ((0, quietHours_1.recipientInQuietHours)(dataFor(recipientId))) { + console.log(`[onGameSessionUpdate] recipient ${recipientId} in quiet hours — suppressing start push`); + } + else { + await notifyPartner(db, messaging, recipientId, starterName, gameType, 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, starterAvatar, sessionId); + } } return; } @@ -125,9 +133,19 @@ exports.onGameSessionUpdate = functions.firestore }); if (claimed) { const gt = (_l = currentData.gameType) !== null && _l !== void 0 ? _l : 'wheel'; - // Notify BOTH partners, each naming the OTHER. - await notifyPartner(db, messaging, partnerA, partnerBName, gt, 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, avatarB, sessionId); - await notifyPartner(db, messaging, partnerB, partnerAName, gt, 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, avatarA, sessionId); + // Notify BOTH partners, each naming the OTHER. M-001: skip a recipient in quiet hours. + if ((0, quietHours_1.recipientInQuietHours)(dataFor(partnerA))) { + console.log(`[onGameSessionUpdate] ${partnerA} in quiet hours — suppressing finish push`); + } + else { + await notifyPartner(db, messaging, partnerA, partnerBName, gt, 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, avatarB, sessionId); + } + if ((0, quietHours_1.recipientInQuietHours)(dataFor(partnerB))) { + console.log(`[onGameSessionUpdate] ${partnerB} in quiet hours — suppressing finish push`); + } + else { + await notifyPartner(db, messaging, partnerB, partnerAName, gt, 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, avatarA, sessionId); + } } return; } diff --git a/functions/dist/games/onGameSessionUpdate.js.map b/functions/dist/games/onGameSessionUpdate.js.map index 866a2798..222f7b54 100644 --- a/functions/dist/games/onGameSessionUpdate.js.map +++ b/functions/dist/games/onGameSessionUpdate.js.map @@ -1 +1 @@ -{"version":3,"file":"onGameSessionUpdate.js","sourceRoot":"","sources":["../../src/games/onGameSessionUpdate.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onGameSessionUpdate.js","sourceRoot":"","sources":["../../src/games/onGameSessionUpdate.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/quietHours.js b/functions/dist/notifications/quietHours.js new file mode 100644 index 00000000..253c0a7d --- /dev/null +++ b/functions/dist/notifications/quietHours.js @@ -0,0 +1,51 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.recipientInQuietHours = recipientInQuietHours; +/** + * Quiet-hours suppression for partner-action pushes. + * + * The Settings UI promises "10 PM – 8 AM, no notifications". The client stores the window in local + * DataStore AND mirrors it to the recipient's `users/{uid}` doc (quietHoursEnabled / *StartMinutes / + * *EndMinutes / timezone). Because a partner push carries a `notification` block, the OS shows it + * directly when the recipient app is backgrounded/killed — so the only place the promise can be kept + * is server-side, here, before the push is sent (M-001). + * + * FAIL-OPEN by design: if quiet hours is not explicitly enabled, or any field (window/timezone) is + * missing or malformed, we return `false` (do NOT suppress). A bug here can therefore only ever fall + * back to today's behavior (notification delivered) — it can never wrongly drop a notification, and + * existing installs keep delivering exactly as before until the client backfills the fields. + */ +function recipientInQuietHours(userData, now = new Date()) { + var _a, _b; + if (!userData || userData.quietHoursEnabled !== true) + return false; + const start = userData.quietHoursStartMinutes; + const end = userData.quietHoursEndMinutes; + const tz = userData.timezone; + if (typeof start !== 'number' || typeof end !== 'number' || typeof tz !== 'string' || !tz) { + return false; + } + let nowMinutes; + try { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone: tz, + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).formatToParts(now); + const hour = Number((_a = parts.find((p) => p.type === 'hour')) === null || _a === void 0 ? void 0 : _a.value) % 24; + const minute = Number((_b = parts.find((p) => p.type === 'minute')) === null || _b === void 0 ? void 0 : _b.value); + if (Number.isNaN(hour) || Number.isNaN(minute)) + return false; + nowMinutes = hour * 60 + minute; + } + catch (_c) { + // Unknown/invalid timezone id → fail open. + return false; + } + // Window may cross midnight (e.g. 22:00 → 08:00). + return start <= end + ? nowMinutes >= start && nowMinutes <= end + : nowMinutes >= start || nowMinutes <= end; +} +//# sourceMappingURL=quietHours.js.map \ No newline at end of file diff --git a/functions/dist/notifications/quietHours.js.map b/functions/dist/notifications/quietHours.js.map new file mode 100644 index 00000000..328bbb0e --- /dev/null +++ b/functions/dist/notifications/quietHours.js.map @@ -0,0 +1 @@ +{"version":3,"file":"quietHours.js","sourceRoot":"","sources":["../../src/notifications/quietHours.ts"],"names":[],"mappings":";;AAcA,sDAkCC;AAhDD;;;;;;;;;;;;;GAaG;AACH,SAAgB,qBAAqB,CACnC,QAAoD,EACpD,MAAY,IAAI,IAAI,EAAE;;IAEtB,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,iBAAiB,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAElE,MAAM,KAAK,GAAG,QAAQ,CAAC,sBAAsB,CAAA;IAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,oBAAoB,CAAA;IACzC,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAA;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,EAAE,EAAE,CAAC;QAC1F,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,UAAkB,CAAA;IACtB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;YAC7C,QAAQ,EAAE,EAAE;YACZ,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,KAAK;SACd,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;QACrB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAA,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,0CAAE,KAAK,CAAC,GAAG,EAAE,CAAA;QACrE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAA,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,0CAAE,KAAK,CAAC,CAAA;QACpE,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAA;QAC5D,UAAU,GAAG,IAAI,GAAG,EAAE,GAAG,MAAM,CAAA;IACjC,CAAC;IAAC,WAAM,CAAC;QACP,2CAA2C;QAC3C,OAAO,KAAK,CAAA;IACd,CAAC;IAED,kDAAkD;IAClD,OAAO,KAAK,IAAI,GAAG;QACjB,CAAC,CAAC,UAAU,IAAI,KAAK,IAAI,UAAU,IAAI,GAAG;QAC1C,CAAC,CAAC,UAAU,IAAI,KAAK,IAAI,UAAU,IAAI,GAAG,CAAA;AAC9C,CAAC"} \ No newline at end of file diff --git a/functions/dist/questions/onAnswerRevealed.js b/functions/dist/questions/onAnswerRevealed.js index 4122d3e2..58dbe645 100644 --- a/functions/dist/questions/onAnswerRevealed.js +++ b/functions/dist/questions/onAnswerRevealed.js @@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.onAnswerRevealed = void 0; const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); +const quietHours_1 = require("../notifications/quietHours"); /** * Firestore trigger: when one partner OPENS (reveals) the shared answers — i.e. their own * daily answer doc flips isRevealed false → true — notify the other partner that they've @@ -93,6 +94,11 @@ exports.onAnswerRevealed = functions.firestore console.log(`[onAnswerRevealed] partner ${partnerId} has partner-activity notifications off`); return; } + // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. + if ((0, quietHours_1.recipientInQuietHours)(partnerUserDoc.data())) { + console.log(`[onAnswerRevealed] partner ${partnerId} is in quiet hours — suppressing`); + return; + } const questionId = typeof after.questionId === 'string' ? after.questionId : ''; const revealerDoc = await db.collection('users').doc(userId).get(); const revealerName = ((_e = revealerDoc.data()) === null || _e === void 0 ? void 0 : _e.displayName) || 'Your partner'; diff --git a/functions/dist/questions/onAnswerRevealed.js.map b/functions/dist/questions/onAnswerRevealed.js.map index cc76504e..8e6e46e9 100644 --- a/functions/dist/questions/onAnswerRevealed.js.map +++ b/functions/dist/questions/onAnswerRevealed.js.map @@ -1 +1 @@ -{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/questions/onAnswerWritten.js b/functions/dist/questions/onAnswerWritten.js index bbaa9a7a..540948cb 100644 --- a/functions/dist/questions/onAnswerWritten.js +++ b/functions/dist/questions/onAnswerWritten.js @@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.onAnswerWritten = void 0; const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); +const quietHours_1 = require("../notifications/quietHours"); /** * Firestore trigger that sends an FCM notification to the other partner when * one partner writes an answer under @@ -101,6 +102,11 @@ exports.onAnswerWritten = functions.firestore console.log(`[onAnswerWritten] partner ${partnerId} has partner-answered notifications off`); return; } + // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. + if ((0, quietHours_1.recipientInQuietHours)(partnerUserDoc.data())) { + console.log(`[onAnswerWritten] partner ${partnerId} is in quiet hours — suppressing`); + return; + } const answerData = snap.data(); const questionId = typeof answerData.questionId === 'string' ? answerData.questionId : ''; // Sender (the partner who just answered) avatar — used as the notification large icon. diff --git a/functions/dist/questions/onAnswerWritten.js.map b/functions/dist/questions/onAnswerWritten.js.map index 8e2c6a33..9c52da5b 100644 --- a/functions/dist/questions/onAnswerWritten.js.map +++ b/functions/dist/questions/onAnswerWritten.js.map @@ -1 +1 @@ -{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/questions/onMessageWritten.js b/functions/dist/questions/onMessageWritten.js index 88d0eee6..fddd7f59 100644 --- a/functions/dist/questions/onMessageWritten.js +++ b/functions/dist/questions/onMessageWritten.js @@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.onMessageWritten = void 0; const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); +const quietHours_1 = require("../notifications/quietHours"); /** * Firestore trigger that notifies the other partner when a chat message is * sent in a conversation (the couple chat or a per-question discussion). @@ -74,6 +75,11 @@ exports.onMessageWritten = functions.firestore console.log(`[onMessageWritten] partner ${partnerId} has chat notifications off`); return; } + // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. + if ((0, quietHours_1.recipientInQuietHours)(partnerUserDoc.data())) { + console.log(`[onMessageWritten] partner ${partnerId} is in quiet hours — suppressing`); + return; + } const tokens = []; if (partnerUserDoc.exists) { const legacyToken = (_d = partnerUserDoc.data()) === null || _d === void 0 ? void 0 : _d.fcmToken; diff --git a/functions/dist/questions/onMessageWritten.js.map b/functions/dist/questions/onMessageWritten.js.map index 87663b80..17daea78 100644 --- a/functions/dist/questions/onMessageWritten.js.map +++ b/functions/dist/questions/onMessageWritten.js.map @@ -1 +1 @@ -{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC;;;;;;;GAOG;AACU,QAAA,gBAAgB,GAAG,SAAS,CAAC,SAAS;KAChD,QAAQ,CAAC,wEAAwE,CAAC;KAClF,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;;IAChC,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,MAIvD,CAAA;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAsC,CAAA;IACnE,MAAM,QAAQ,GAAG,OAAO,WAAW,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAA;IAC/F,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,iDAAiD,SAAS,EAAE,CAAC,CAAA;QAC1E,OAAM;IACR,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,6BAA6B,QAAQ,YAAY,CAAC,CAAA;QAC/D,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAA;IACzD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,kDAAkD,QAAQ,EAAE,CAAC,CAAA;QAC1E,OAAM;IACR,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IAExE,+EAA+E;IAC/E,MAAM,YAAY,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,gBAAgB,CAAA;IAC5D,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,8BAA8B,SAAS,6BAA6B,CAAC,CAAA;QACjF,OAAM;IACR,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,IAAI,cAAc,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,WAAW,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;QACnD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QAC1B,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,EAAE;SAC3B,UAAU,CAAC,OAAO,CAAC;SACnB,GAAG,CAAC,SAAS,CAAC;SACd,UAAU,CAAC,WAAW,CAAC;SACvB,GAAG,EAAE,CAAA;IACR,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;;QACjC,MAAM,CAAC,GAAG,MAAA,GAAG,CAAC,IAAI,EAAE,0CAAE,KAAK,CAAA;QAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,gDAAgD,SAAS,EAAE,CAAC,CAAA;QACxE,OAAM;IACR,CAAC;IAED,0FAA0F;IAC1F,yFAAyF;IACzF,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IAClE,MAAM,cAAc,GAAG,MAAC,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,QAA+B,mCAAI,EAAE,CAAA;IAC/E,MAAM,UAAU,GAAG,MAAC,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,WAAkC,mCAAI,EAAE,CAAA;IAE9E,MAAM,OAAO,GAAqC;QAChD,YAAY,EAAE;YACZ,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,iBAAiB,CAAC,CAAC,CAAC,6BAA6B;YAClF,IAAI,EAAE,wBAAwB;SAC/B;QACD,IAAI,kBACF,IAAI,EAAE,cAAc,EACpB,SAAS,EAAE,QAAQ,EACnB,eAAe,EAAE,cAAc,IAC5B,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACjE;KACF,CAAA;IAED,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,UAAU,CAC1C,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACnB,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,gCAClB,OAAO,KACV,KAAK;QACL,0FAA0F;QAC1F,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,GAClC,CAAC,CAC9B,CACF,CAAA;IAED,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACpC,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACjC,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,QAAQ,CAAC,CAAA;IAC1E,CAAC;IAED,OAAO,CAAC,GAAG,CACT,uCAAuC,SAAS,qBAAqB,cAAc,cAAc,QAAQ,EAAE,CAC5G,CAAA;AACH,CAAC,CAAC,CAAA"} \ No newline at end of file +{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/src/games/onGameSessionUpdate.ts b/functions/src/games/onGameSessionUpdate.ts index e0478cfc..1a456517 100644 --- a/functions/src/games/onGameSessionUpdate.ts +++ b/functions/src/games/onGameSessionUpdate.ts @@ -1,5 +1,6 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { recipientInQuietHours } from '../notifications/quietHours' /** * Firestore trigger that notifies partners when a game session is created or completed. @@ -51,6 +52,8 @@ export const onGameSessionUpdate = functions.firestore const partnerBName = userB.data()?.displayName ?? 'Partner B' const avatarA = userA.data()?.photoUrl as string | undefined const avatarB = userB.data()?.photoUrl as string | undefined + // M-001: per-recipient quiet-hours lookup ("no notifications" promise). Fail-open. + const dataFor = (uid: string) => (uid === partnerA ? userA.data() : userB.data()) const currentData = change.after.data() ?? {} if (!change.after.exists) return // deletion — nothing to notify @@ -80,11 +83,15 @@ export const onGameSessionUpdate = functions.firestore const recipientId = startedBy === partnerA ? partnerB : partnerA const starterName = startedBy === partnerA ? partnerAName : partnerBName const starterAvatar = startedBy === partnerA ? avatarA : avatarB - await notifyPartner( - db, messaging, recipientId, starterName, gameType, - 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, - starterAvatar, sessionId - ) + if (recipientInQuietHours(dataFor(recipientId))) { + console.log(`[onGameSessionUpdate] recipient ${recipientId} in quiet hours — suppressing start push`) + } else { + await notifyPartner( + db, messaging, recipientId, starterName, gameType, + 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, + starterAvatar, sessionId + ) + } } return } @@ -100,17 +107,25 @@ export const onGameSessionUpdate = functions.firestore }) if (claimed) { const gt = currentData.gameType ?? 'wheel' - // Notify BOTH partners, each naming the OTHER. - await notifyPartner( - db, messaging, partnerA, partnerBName, gt, - 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, - avatarB, sessionId - ) - await notifyPartner( - db, messaging, partnerB, partnerAName, gt, - 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, - avatarA, sessionId - ) + // Notify BOTH partners, each naming the OTHER. M-001: skip a recipient in quiet hours. + if (recipientInQuietHours(dataFor(partnerA))) { + console.log(`[onGameSessionUpdate] ${partnerA} in quiet hours — suppressing finish push`) + } else { + await notifyPartner( + db, messaging, partnerA, partnerBName, gt, + 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, + avatarB, sessionId + ) + } + if (recipientInQuietHours(dataFor(partnerB))) { + console.log(`[onGameSessionUpdate] ${partnerB} in quiet hours — suppressing finish push`) + } else { + await notifyPartner( + db, messaging, partnerB, partnerAName, gt, + 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, + avatarA, sessionId + ) + } } return } diff --git a/functions/src/notifications/quietHours.ts b/functions/src/notifications/quietHours.ts new file mode 100644 index 00000000..aa08b8ac --- /dev/null +++ b/functions/src/notifications/quietHours.ts @@ -0,0 +1,49 @@ +/** + * Quiet-hours suppression for partner-action pushes. + * + * The Settings UI promises "10 PM – 8 AM, no notifications". The client stores the window in local + * DataStore AND mirrors it to the recipient's `users/{uid}` doc (quietHoursEnabled / *StartMinutes / + * *EndMinutes / timezone). Because a partner push carries a `notification` block, the OS shows it + * directly when the recipient app is backgrounded/killed — so the only place the promise can be kept + * is server-side, here, before the push is sent (M-001). + * + * FAIL-OPEN by design: if quiet hours is not explicitly enabled, or any field (window/timezone) is + * missing or malformed, we return `false` (do NOT suppress). A bug here can therefore only ever fall + * back to today's behavior (notification delivered) — it can never wrongly drop a notification, and + * existing installs keep delivering exactly as before until the client backfills the fields. + */ +export function recipientInQuietHours( + userData: FirebaseFirestore.DocumentData | undefined, + now: Date = new Date() +): boolean { + if (!userData || userData.quietHoursEnabled !== true) return false + + const start = userData.quietHoursStartMinutes + const end = userData.quietHoursEndMinutes + const tz = userData.timezone + if (typeof start !== 'number' || typeof end !== 'number' || typeof tz !== 'string' || !tz) { + return false + } + + let nowMinutes: number + try { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone: tz, + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).formatToParts(now) + const hour = Number(parts.find((p) => p.type === 'hour')?.value) % 24 + const minute = Number(parts.find((p) => p.type === 'minute')?.value) + if (Number.isNaN(hour) || Number.isNaN(minute)) return false + nowMinutes = hour * 60 + minute + } catch { + // Unknown/invalid timezone id → fail open. + return false + } + + // Window may cross midnight (e.g. 22:00 → 08:00). + return start <= end + ? nowMinutes >= start && nowMinutes <= end + : nowMinutes >= start || nowMinutes <= end +} diff --git a/functions/src/questions/onAnswerRevealed.ts b/functions/src/questions/onAnswerRevealed.ts index b9c8a6f4..a24a32d7 100644 --- a/functions/src/questions/onAnswerRevealed.ts +++ b/functions/src/questions/onAnswerRevealed.ts @@ -1,5 +1,6 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { recipientInQuietHours } from '../notifications/quietHours' /** * Firestore trigger: when one partner OPENS (reveals) the shared answers — i.e. their own @@ -64,6 +65,12 @@ export const onAnswerRevealed = functions.firestore return } + // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. + if (recipientInQuietHours(partnerUserDoc.data())) { + console.log(`[onAnswerRevealed] partner ${partnerId} is in quiet hours — suppressing`) + return + } + const questionId = typeof after.questionId === 'string' ? after.questionId : '' const revealerDoc = await db.collection('users').doc(userId).get() const revealerName = (revealerDoc.data()?.displayName as string | undefined) || 'Your partner' diff --git a/functions/src/questions/onAnswerWritten.ts b/functions/src/questions/onAnswerWritten.ts index 24f73297..afd4e8af 100644 --- a/functions/src/questions/onAnswerWritten.ts +++ b/functions/src/questions/onAnswerWritten.ts @@ -1,5 +1,6 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { recipientInQuietHours } from '../notifications/quietHours' /** * Firestore trigger that sends an FCM notification to the other partner when @@ -78,6 +79,12 @@ export const onAnswerWritten = functions.firestore return } + // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. + if (recipientInQuietHours(partnerUserDoc.data())) { + console.log(`[onAnswerWritten] partner ${partnerId} is in quiet hours — suppressing`) + return + } + const answerData = snap.data() as Partial> const questionId = typeof answerData.questionId === 'string' ? answerData.questionId : '' diff --git a/functions/src/questions/onMessageWritten.ts b/functions/src/questions/onMessageWritten.ts index 32ef876a..3e911d81 100644 --- a/functions/src/questions/onMessageWritten.ts +++ b/functions/src/questions/onMessageWritten.ts @@ -1,5 +1,6 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { recipientInQuietHours } from '../notifications/quietHours' /** * Firestore trigger that notifies the other partner when a chat message is @@ -48,6 +49,12 @@ export const onMessageWritten = functions.firestore return } + // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. + if (recipientInQuietHours(partnerUserDoc.data())) { + console.log(`[onMessageWritten] partner ${partnerId} is in quiet hours — suppressing`) + return + } + const tokens: string[] = [] if (partnerUserDoc.exists) { const legacyToken = partnerUserDoc.data()?.fcmToken