feat: quiet hours notifications, settings UI, game session updates, docs
This commit is contained in:
parent
c31eea2549
commit
37ed7cebec
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
256
ClaudeQAPlan.md
256
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
|
||||
|
|
|
|||
|
|
@ -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.)_
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC;;;;;;;;GAQG;AACU,QAAA,eAAe,GAAG,SAAS,CAAC,SAAS;KAC/C,QAAQ,CAAC,2DAA2D,CAAC;KACrE,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;;IAChC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAI1C,CAAA;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,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,4BAA4B,QAAQ,YAAY,CAAC,CAAA;QAC9D,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAE7D,gFAAgF;IAChF,kFAAkF;IAClF,uFAAuF;IACvF,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,4BAA4B,MAAM,8BAA8B,QAAQ,EAAE,CAAC,CAAA;QACxF,OAAM;IACR,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAA;IACvD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,iDAAiD,QAAQ,EAAE,CAAC,CAAA;QACzE,OAAM;IACR,CAAC;IAED,yEAAyE;IACzE,kFAAkF;IAClF,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IACxE,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,+CAA+C,SAAS,EAAE,CAAC,CAAA;QACvE,OAAM;IACR,CAAC;IAED,+EAA+E;IAC/E,MAAM,YAAY,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,oBAAoB,CAAA;IAChE,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,6BAA6B,SAAS,yCAAyC,CAAC,CAAA;QAC5F,OAAM;IACR,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAsC,CAAA;IAClE,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAA;IAEzF,uFAAuF;IACvF,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAA;IAChE,MAAM,YAAY,GAAG,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;IAE/C,0FAA0F;IAC1F,wFAAwF;IACxF,MAAM,iBAAiB,GAAG,MAAM,EAAE;SAC/B,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;SACnC,UAAU,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;SACtC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IAC7C,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,CAAA;IAE7C,MAAM,OAAO,GAAqC;QAChD,YAAY,EAAE,YAAY;YACxB,CAAC,CAAC;gBACE,KAAK,EAAE,6BAA6B;gBACpC,IAAI,EAAE,wDAAwD;aAC/D;YACH,CAAC,CAAC;gBACE,KAAK,EAAE,6BAA6B;gBACpC,IAAI,EAAE,4CAA4C;aACnD;QACL,IAAI,kBACF,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,QAAQ,EACnB,WAAW,EAAE,UAAU,EACvB,IAAI,IACD,CAAC,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;YAC7D,CAAC,CAAC,EAAE,iBAAiB,EAAE,YAAY,EAAE;YACrC,CAAC,CAAC,EAAE,CAAC,CACR;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,EACL,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,8CAA8C,EAAE,QAAQ,CAAC,CAAA;IACzE,CAAC;IAED,0EAA0E;IAC1E,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;QAClD,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KAC7D,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC,CAAC,CAAC,CAAA;IAEnF,OAAO,CAAC,GAAG,CACT,sCAAsC,SAAS,eAAe,QAAQ,aAAa,UAAU,EAAE,CAChG,CAAA;AACH,CAAC,CAAC,CAAA"}
|
||||
{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<Record<string, unknown>>
|
||||
const questionId = typeof answerData.questionId === 'string' ? answerData.questionId : ''
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue