feat: add automated theme-mismatch scanner to Pass C methodology (Tier 1-3)
This commit is contained in:
parent
37ed7cebec
commit
fe3ea7715c
|
|
@ -1,7 +1,7 @@
|
|||
# Claude QA Coverage Matrix
|
||||
|
||||
> **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`.
|
||||
> 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.
|
||||
> 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. Then drove Pass N: **N-001 (P1) Bucket List fully non-functional → FIXED+verified live**; **N-002 (P2) Date Builder "Create Plan" no-op (incomplete feature) → OPEN, needs product decision.** L (chat E2E render+decrypt+receipts+reactions+at-rest), P (UI copy + 6103-Q bank) clean; smoke 6/6 GREEN. **1 open P2 (N-002); 2 fixed pending confirm (M-001, N-001); 2 P3 brand backlogs.** 0 FATAL. functions+rules deployed to prod; client changes in working tree (user commits), debug APK installed both emulators.
|
||||
>
|
||||
> **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).**
|
||||
>
|
||||
|
|
@ -25,11 +25,11 @@
|
|||
| 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) |
|
||||
| N — Daily Q / reveal / check-ins / interactive | R15 (driven): daily-Q + **reveal both-answered gate** ✓; **Bucket List CRUD FIXED+verified live (N-001)** — add(`enc:v1:`)/complete/delete/list; Outcomes/Your Progress code-correct (resolves coupleId, submits); **Date Builder N-002 (P2) — "Create Plan" no-op, incomplete feature (open)**; Activity feed render-checked (prior). | ⚠️ **mostly pass** — N-001 fixed (pending confirm); **N-002 open (needs product decision)** |
|
||||
| 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-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.**
|
||||
**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. **R15: 1 open P2 (N-002 Date Builder); 2 fixed pending confirm (M-001 quiet hours, N-001 Bucket List); 2 open P3 brand backlogs.**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ 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).
|
||||
- **R15** — gap-closing round (Passes L/M/N/P + regression smoke); **3 bugs found, 2 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 + rules allowlist; verified live (fn log suppress vs notify); deployed prod. **N-001 (P1) Bucket List fully non-functional** (coupleId never set → all CRUD no-ops) — FIXED (VM resolves couple in init) + verified live (add `enc:v1:`/complete/delete/render). **N-002 (P2) Date Builder "Create Plan" no-op** — incomplete feature (dateIdeaId never wired, coupleId empty, prefs never displayed) — OPEN, needs product decision. L chat-core, P copy+question-bank (6103 Qs) 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).
|
||||
|
|
|
|||
130
ClaudeQAPlan.md
130
ClaudeQAPlan.md
|
|
@ -430,7 +430,11 @@ Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges
|
|||
`pass`). Coverage row format: `game × starter × join-entry × premium-state × depth/count × lifecycle-edge × result`;
|
||||
only `pass` when start/join/play/finish/reopen/recover are all verified.
|
||||
|
||||
### Pass C — Visual pass, light + dark, ALL screens
|
||||
### ⛔ Pass C — Visual pass, light + dark, ALL screens (MANDATORY: run scan BEFORE sweep)
|
||||
> **⛔ CLAUDE: Run the automated theme scan (below, Automated Tier 1) before starting the visual sweep.
|
||||
> Read the output at `/tmp/claude-theme-scan-<date>.md` and file findings to ClaudeReport.md first.
|
||||
> The sweep must verify every flagged screen in BOTH themes.**
|
||||
|
||||
Every route in `core/navigation/AppRoute.kt` (~50), in **both** modes: text contrast/readability (no invisible/
|
||||
low-contrast), no clipping/overflow/ellipsis breakage, icons visible, backgrounds adapt, controls legible. Groups:
|
||||
auth/onboarding/pairing (fresh acct); Home (solo + paired); Play + every game; Today + reveal/history; Messages
|
||||
|
|
@ -454,8 +458,120 @@ Account); Paywall; Your Progress/Activity; Recovery.
|
|||
helper / direct `painterResource` tile also feathers.) Fix pattern (if it regresses): feather the edges to transparent,
|
||||
or a vignette matching the surface, or ship transparent-edged art — applied in the shared `BrandIllustration`/`EmptyState`
|
||||
helpers so it's consistent everywhere.
|
||||
- **Probe:** `ui/theme/Theme.kt` hardcoded brand colors + chat's custom `closerBackgroundBrush` — verify dark mode
|
||||
truly adapts; grep screens for hardcoded `Color(0x...)`.
|
||||
- **⛔ CLAUDE — RUN THE AUTOMATED THEME SCAN FIRST (MANDATORY, BEFORE THE VISUAL SWEEP):**
|
||||
Do NOT start the manual visual sweep until the automated scan has completed and you have reviewed its results.
|
||||
The scan is run from the project root (`/home/kaspa/.openclaw/Projects/relationship-app/`). It produces a
|
||||
report in `/tmp/claude-theme-scan-<date>.md`. Read that report and file all findings to `ClaudeReport.md`
|
||||
BEFORE the visual sweep — screens with known hardcoded colors need extra scrutiny.
|
||||
|
||||
**Tier 1 — Hardcoded container & surface color scan (catches ~80% of theme mismatches)**
|
||||
Every hit below that sets a SURFACE / CONTAINER / BACKGROUND / DIVIDER to a hardcoded value (not a
|
||||
`MaterialTheme.colorScheme.*` token) is an automated finding. Classify by failure mode:
|
||||
|
||||
```bash
|
||||
cd /home/kaspa/.openclaw/Projects/relationship-app
|
||||
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md"
|
||||
echo "# Theme Mismatch Scan — $(date)" > "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**Pattern 1 — Surface/Card/Dialog/ModalBottomSheet with hardcoded color (CRITICAL):**
|
||||
These wrap content with a colored container. A hardcoded color here means the background will NOT swap in
|
||||
dark mode, producing invisible text, white-on-white, or light-on-light. This is EXACTLY the AddItemDialog
|
||||
pattern (`Surface(color = Color.White)` inside a dark-themed app).
|
||||
```bash
|
||||
grep -rnE '(Surface|Card|Dialog|AlertDialog|ModalBottomSheet|BottomSheet|Scaffold)\s*\([^)]*' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| grep -iE 'color = Color\.(White|Black|Red|Blue|Green|Yellow|Cyan|Magenta|Gray|LightGray|DarkGray|(0x[0-9A-F]{8}))' \
|
||||
| grep -ivE '(Color\.Transparent|materialColorScheme|colorScheme\.)' \
|
||||
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**Pattern 2 — Background modifier with hardcoded color (CRITICAL):**
|
||||
Same as Pattern 1 but applied as a modifier. The screen's background won't adapt.
|
||||
```bash
|
||||
grep -rnE 'Modifier\.(background|fillMaxSize|fillMaxWidth)\([^)]*Color\.(White|Black|[A-Z][a-z]+)\b' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| grep -ivE '(Color\.Transparent|colorScheme\.|isDarkTheme|LocalAppInDarkTheme)' \
|
||||
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
|
||||
grep -rn 'Modifier\.background(Color(' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| grep -ivE '(colorScheme|isDarkTheme|LocalAppInDarkTheme)' \
|
||||
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**Pattern 3 — Component color overrides with hardcoded colors (MAJOR):**
|
||||
Buttons, TextFields, Tabs, Dividers with explicitly set container/content colors that won't adapt.
|
||||
```bash
|
||||
grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|SwitchDefaults\.colors)\s*\([^)]*color = Color\.' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| grep -ivE '(colorScheme|Color\.Transparent)' \
|
||||
| while read line; do echo "🟠 MAJOR $line"; done >> "$SCAN_OUTPUT"
|
||||
grep -rnE '(Divider|HorizontalDivider)\s*\([^)]*color = Color\.' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| while read line; do echo "🟠 MAJOR $line"; done >> "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**Pattern 4 — Text/Icon color hardcoded on a themed surface (MAJOR):**
|
||||
Text or icon tint set to `Color.White` or `Color(0xFF...)` while sitting on a surface that may adapt.
|
||||
Some of these are intentional (white text on a purple button is correct). Flag them; during visual sweep,
|
||||
confirm each is on a properly-themed container.
|
||||
```bash
|
||||
grep -rnE '(Text|Icon)\s*\([^)]*color = Color\.(White|Black|(0x[0-9A-F]{8}))' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| grep -ivE '(colorScheme|isDarkTheme|Theme\.kt)' \
|
||||
| while read line; do echo "🟡 MINOR-REVIEW $line"; done >> "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**Pattern 5 — Direct painterResource bypassing BrandIllustration (MAJOR):**
|
||||
Any screen using `painterResource(R.drawable.illustration_*)` or `painterResource(R.drawable.pack_art_*)`
|
||||
directly instead of going through `BrandIllustration` means its art will NOT follow the decoupled in-app
|
||||
theme (C-DARKART-001). Each hit needs either conversion to `BrandIllustration` or verification that the
|
||||
screen's art already has manual theme handling. Exclude glyphs (always the same regardless of theme).
|
||||
```bash
|
||||
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**Pattern 6 — Border with hardcoded color (MINOR-MAJOR):**
|
||||
Borders set with hardcoded colors that match a light surface won't contrast on dark.
|
||||
```bash
|
||||
grep -rnE 'Modifier\.border\([^)]*Color\.(White|Black|[A-Z][a-z]+)\b' app/src/main/java/app/closer/ui/ --include="*.kt" \
|
||||
| grep -ivE '(Color\.Transparent|colorScheme)' \
|
||||
| while read line; do echo "🟡 REVIEW $line"; done >> "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**Tier 2 — Theme definition validation (run once per QA round, or after any Theme.kt change)**
|
||||
Verify the `darkColors` scheme in `Theme.kt` has EVERY color slot explicitly defined (not relying on
|
||||
Material3 defaults). Missing slots auto-fill from base, which may not match the brand palette.
|
||||
Required slots: `primary`, `onPrimary`, `primaryContainer`, `onPrimaryContainer`, `secondary`,
|
||||
`onSecondary`, `secondaryContainer`, `onSecondaryContainer`, `tertiary`, `onTertiary`,
|
||||
`tertiaryContainer`, `onTertiaryContainer`, `background`, `surface`, `onBackground`, `onSurface`,
|
||||
`surfaceVariant`, `onSurfaceVariant`, `outline`, `outlineVariant`, `error`, `onError`,
|
||||
`errorContainer`, `onErrorContainer`, `inverseSurface`, `inverseOnSurface`, `inversePrimary`,
|
||||
`surfaceTint`, `scrim`.
|
||||
```bash
|
||||
echo "\n## Tier 2 — Theme definition validation" >> "$SCAN_OUTPUT"
|
||||
# List explicitly defined dark scheme slots
|
||||
grep -oE '\b([a-z]+(Container|Surface|Primary|Secondary|Tertiary|Error|Scrim|Tint)?)\s*=' app/src/main/java/app/closer/ui/theme/Theme.kt \
|
||||
| grep -v '^//' \
|
||||
| sed 's/\s*=//' > /tmp/dark-scheme-slots.txt
|
||||
# Cross-check against required
|
||||
for slot in primary onPrimary primaryContainer onPrimaryContainer secondary onSecondary secondaryContainer onSecondaryContainer tertiary onTertiary tertiaryContainer onTertiaryContainer background surface onBackground onSurface surfaceVariant onSurfaceVariant outline outlineVariant error onError errorContainer onErrorContainer inverseSurface inverseOnSurface inversePrimary surfaceTint scrim; do
|
||||
if ! grep -q "$slot" /tmp/dark-scheme-slots.txt; then
|
||||
echo "⚠️ MISSING: $slot not explicitly defined in dark scheme" >> "$SCAN_OUTPUT"
|
||||
fi
|
||||
done
|
||||
echo "\n**NOTE:** The systematic scan above is a STARTING POINT. It catches structural patterns but does NOT
|
||||
catch every possible theme-mismatch scenario. Some failures are compositional — e.g. a themed color used
|
||||
on the wrong surface, or a gradient with hardcoded stops. The visual sweep is still MANDATORY." >> "$SCAN_OUTPUT"
|
||||
```
|
||||
|
||||
**⛔ CLAUDE: After running the scan, read the report, file all findings to ClaudeReport.md as Pass C
|
||||
theme defects, then proceed to the manual visual sweep. Any screen flagged as CRITICAL or MAJOR must be
|
||||
verified in BOTH themes during the sweep. If you fix hardcoded colors as part of the QA round, log the
|
||||
fix and re-run the scan to confirm it's clean.**
|
||||
|
||||
**Tier 3 — Compose screenshot diff suite (endgame, not yet implemented):**
|
||||
The true "catch everything" solution is an automated screenshot comparison pipeline that renders every
|
||||
route in light mode, renders the same route in dark mode, and pixel-diffs them — flagging any screen
|
||||
where the dark version has white backgrounds, invisible text, or wrong-variant art. This catches
|
||||
compositional and gradient-based mismatches that static analysis cannot. When implemented, use
|
||||
`papAROS`, `Shot`, or Roborazzi with a custom `darkTheme = true` test parameter for each route.
|
||||
Log this to `Future.md` as "Tier 3: Compose screenshot diff for visual regression".
|
||||
- **THEME-VARIANT ART must follow the IN-APP theme, not just the system (mandatory — RUN THE DECOUPLED STATE):** the app
|
||||
has its own theme toggle (Settings → Appearance → Light/Dark/Device) that swaps Compose colors but does **not** change
|
||||
the Android config `uiMode`, while `-night` drawables (`drawable-night-nodpi/`) and `painterResource` resolve off the
|
||||
|
|
@ -947,6 +1063,14 @@ The non-game interactive surfaces that have no functional home (Pass B is games
|
|||
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.
|
||||
- **ACTUALLY PERSIST + verify via admin read — an empty list can be a DEAD feature, not an empty one (RETROSPECTIVE —
|
||||
N-001/N-002).** For every interactive feature, create real data through the UI and confirm it **lands in Firestore**
|
||||
(admin read) AND **renders back**; don't accept the empty/initial state as "works." Bucket List looked like an empty
|
||||
list but was fully non-functional (`coupleId` never set → every op silently `return`ed); Date Builder's "Create Plan"
|
||||
silently no-ops (`dateIdeaId` never wired) and writes to a collection no screen reads. Reflex: any VM that gates on
|
||||
`if (someId.isEmpty()) return` and expects the screen to call `setX(...)` is suspect — `grep` for the `setX` caller; if
|
||||
none, it's dead. Also confirm there's a **display surface** for whatever a "save/create" writes (a save into an unread
|
||||
collection is an incomplete feature, not a working one).
|
||||
- **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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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): 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). **Then drove Pass N (user "FIX"):** **N-001 (P1) — Bucket List was entirely non-functional** (coupleId never wired → all CRUD silently no-op) → **FIXED + verified live** (add `enc:v1:`/complete/delete/render). **N-002 (P2) — "Plan a Date"/Date Builder "Create Plan" was a no-op** (wrote to an unread prefs collection; `dateIdeaId`/`coupleId` never wired) → **FIXED + verified live** (re-pointed to create a PLANNED `DatePlan` → Home shows "Date coming up"). Outcomes/Your Progress code-correct. **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**); daily-Q + reveal 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.**
|
||||
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
> 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.
|
||||
- **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.** **Pass N driven (user "FIX"):** **N-001 (P1) Bucket List was fully non-functional** (coupleId never set → all CRUD no-ops) → **FIXED + verified live** (add `enc:v1:` / complete / delete / list render; client-only). **N-002 (P2) "Plan a Date"/Date Builder "Create Plan" no-op** (wrote to unread prefs collection; `dateIdeaId`/`coupleId` never wired) → **FIXED + verified live** (re-pointed `DateBuilderViewModel` to create a PLANNED `DatePlan` via `savePlan` + resolve coupleId → `date_plan` status=planned, `enc:v1:`; Home shows "Date coming up"). Outcomes/Your Progress code-correct (resolves coupleId); daily-Q/reveal render ✓. Uncommitted (user commits): client (`BucketListViewModel`, `DateBuilderViewModel`) — M-001's functions/rules/client were committed by the user mid-round (+ user dropped 3 dark-variant PNGs in `drawable-night-nodpi/` toward BRAND-DARK-COVERAGE). **M-001 functions+rules DEPLOYED to prod; N-001/N-002 are client-only (debug APK installed both emulators).** NEXT (R16): confirm M-001 + N-001 + N-002 hold → prune; 2 P3 brand backlogs; revisit Date Builder "both-partners-generate" vision if wanted.
|
||||
- **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`.
|
||||
|
|
@ -46,13 +46,13 @@
|
|||
| Severity | Open | Fixed (pending 1 confirm) |
|
||||
|---|---|---|
|
||||
| P0 | 0 | 0 |
|
||||
| P1 | 0 | 0 |
|
||||
| P2 | **0** | **1** (M-001 quiet hours) |
|
||||
| P1 | 0 | **1** (N-001 Bucket List) |
|
||||
| P2 | **0** | **2** (M-001 quiet hours, N-002 Date Builder) |
|
||||
| P3 | **2** (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM) | **0** |
|
||||
|
||||
_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`._
|
||||
_R15: found + FIXED **3 bugs** — **M-001** (P2 quiet hours), **N-001** (P1 Bucket List non-functional), **N-002** (P2
|
||||
Date Builder "Create Plan" no-op) — all verified live, pending 1 confirm. **0 open P0–P2.** 2 P3 brand-asset backlogs
|
||||
open._
|
||||
|
||||
## 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**
|
||||
|
|
@ -61,6 +61,8 @@ brand-asset backlogs still open (every image needs a dark variant; every icon mu
|
|||
|
||||
| ID | Sev | Area | Description | Suggested fix | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| N-002 | P2 | Dates / Date Builder | **"Plan a Date" / Date Builder "Create Plan" was a no-op.** `DateBuilderViewModel.savePreference()` bailed on `state.dateIdeaId.isEmpty()` (no entry ever calls `setDateIdeaId`), built a `DatePlanPreference` with empty `coupleId`, and wrote to `date_plan_preferences` which **no screen reads**. Net: fill form → Create Plan → nothing saved, no error. | Re-point the builder to create a real **PLANNED `DatePlan`** via `repository.savePlan()` (the collection Home already displays via `getPlansByStatus(PLANNED)`), resolving `coupleId` from `CoupleRepository`; dropped the dead `dateIdeaId` guard. _(Product note: this makes the existing single-user form work end-to-end → Home "Date coming up"; the model's older "generate from BOTH partners' prefs" vision is unbuilt — revisit if that's wanted.)_ | **Fixed — verified live R15** (Create Plan → `date_plan` status=planned, `enc:v1:` duration; Home shows "Date coming up"). Client-only. Pending 1 confirm. |
|
||||
| N-001 | P1 | Dates / Bucket List | **Bucket List was entirely non-functional** — `setCoupleId` was never called, so `coupleId` stayed `""` and `addItem`/`loadItems`/`toggleComplete`/`deleteItem` all silently `return`ed. Items could never be added, loaded, completed, or deleted. | `BucketListViewModel` resolves the couple itself in `init` via `CoupleRepository.getCoupleForUser()` (mirrors `MemoryLaneViewModel`), then `setCoupleId` → `loadItems`. | **Fixed — verified live R15** (add persists `enc:v1:`; complete sets flags; delete removes; list renders). Client-only, no deploy. Pending 1 confirm. |
|
||||
| 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)** |
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
Non-blocking ideas: things that work today but could be better, plus feature ideas. Actual bugs
|
||||
(broken/incorrect behavior) live in `ClaudeReport.md`, not here.
|
||||
|
||||
## UI
|
||||
Themes
|
||||
|
||||
- **"Add to Bucket List" has mixed dark/light mode UI.** The input field, button, and surrounding elements use
|
||||
light-mode colors even when the app is in dark mode. Needs theme alignment.
|
||||
|
||||
## QA
|
||||
|
||||
Improvement & feature ideas surfaced while QA-testing as a consumer (each works today — none are defects).
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import app.closer.domain.model.BucketListItem
|
||||
import app.closer.domain.repository.BucketListRepository
|
||||
import app.closer.domain.repository.CoupleRepository
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
|
@ -16,12 +17,29 @@ import kotlinx.coroutines.launch
|
|||
|
||||
@HiltViewModel
|
||||
class BucketListViewModel @Inject constructor(
|
||||
private val repository: BucketListRepository
|
||||
private val repository: BucketListRepository,
|
||||
private val coupleRepository: CoupleRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(BucketListUiState())
|
||||
val uiState: StateFlow<BucketListUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
// Resolve the couple ourselves (mirrors MemoryLaneViewModel). Previously the screen never
|
||||
// called setCoupleId, so coupleId stayed "" and every add/load/complete/delete silently
|
||||
// returned early — the whole Bucket List was a no-op (N-001).
|
||||
resolveCouple()
|
||||
}
|
||||
|
||||
private fun resolveCouple() {
|
||||
viewModelScope.launch {
|
||||
val uid = FirebaseAuth.getInstance().currentUser?.uid ?: return@launch
|
||||
runCatching { coupleRepository.getCoupleForUser(uid) }
|
||||
.getOrNull()
|
||||
?.let { setCoupleId(it.id) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setCoupleId(coupleId: String) {
|
||||
_uiState.update { it.copy(coupleId = coupleId) }
|
||||
loadItems()
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ package app.closer.ui.dates
|
|||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.closer.domain.model.DatePlanPreference
|
||||
import app.closer.domain.model.DatePlan
|
||||
import app.closer.domain.model.DatePlanStatus
|
||||
import app.closer.domain.repository.CoupleRepository
|
||||
import app.closer.domain.repository.DatePlanRepository
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -15,7 +18,8 @@ import kotlinx.coroutines.launch
|
|||
|
||||
@HiltViewModel
|
||||
class DateBuilderViewModel @Inject constructor(
|
||||
private val repository: DatePlanRepository
|
||||
private val repository: DatePlanRepository,
|
||||
private val coupleRepository: CoupleRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(DateBuilderUiState())
|
||||
|
|
@ -43,22 +47,39 @@ class DateBuilderViewModel @Inject constructor(
|
|||
|
||||
fun savePreference() {
|
||||
val state = _uiState.value
|
||||
if (state.dateIdeaId.isEmpty()) return
|
||||
|
||||
val preference = DatePlanPreference(
|
||||
dateIdeaId = state.dateIdeaId,
|
||||
preferredDate = state.scheduledDate,
|
||||
preferredTime = state.scheduledTime.take(MAX_TIME_LENGTH),
|
||||
budget = state.budget,
|
||||
duration = state.duration.take(MAX_DURATION_LENGTH)
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isSaving = true, error = null) }
|
||||
runCatching { repository.savePreference(preference) }
|
||||
|
||||
// N-002: create a real PLANNED date_plan (surfaced on Home as the upcoming date) and resolve
|
||||
// the couple ourselves. Previously this wrote a date_plan_preference keyed off a dateIdeaId
|
||||
// that no entry ever set + an empty coupleId, so "Create Plan" silently saved nothing.
|
||||
val uid = FirebaseAuth.getInstance().currentUser?.uid
|
||||
val couple = uid?.let { runCatching { coupleRepository.getCoupleForUser(it) }.getOrNull() }
|
||||
if (couple == null) {
|
||||
_uiState.update {
|
||||
it.copy(isSaving = false, error = "Couldn't save. Check your connection and try again.")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val plan = DatePlan(
|
||||
coupleId = couple.id,
|
||||
dateIdeaId = state.dateIdeaId,
|
||||
scheduledDate = state.scheduledDate,
|
||||
scheduledTime = state.scheduledTime.take(MAX_TIME_LENGTH),
|
||||
budget = state.budget,
|
||||
duration = state.duration.take(MAX_DURATION_LENGTH),
|
||||
status = DatePlanStatus.PLANNED,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
)
|
||||
|
||||
runCatching { repository.savePlan(plan) }
|
||||
.onSuccess { _uiState.update { it.copy(isSaving = false, saved = true) } }
|
||||
.onFailure { e ->
|
||||
Log.w(TAG, "Could not save date preference", e)
|
||||
Log.w(TAG, "Could not save date plan", e)
|
||||
_uiState.update {
|
||||
it.copy(isSaving = false, error = "Couldn't save. Check your connection and try again.")
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
|
|
@ -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.
|
||||
|
||||
### N-001 / N-002 — VMs that wait for the screen to push an id silently no-op if nothing pushes it
|
||||
**Symptom (R15)**: the **Bucket List was entirely non-functional** — add/load/complete/delete all did nothing, no error, no logcat. **Root cause**: `BucketListViewModel` gated every operation on `if (coupleId.isEmpty()) return`, expecting the screen to call `setCoupleId(...)` — but `BucketListScreen` never did (the nav route passes no coupleId and there's no `LaunchedEffect`). So `coupleId` stayed `""` and every op returned early **silently**. Same class hit **Date Builder (N-002, still open)**: `savePreference()` bails on `dateIdeaId.isEmpty()` and **nothing ever calls `setDateIdeaId`**, so "Create Plan" is a no-op (and the preference is built with an empty `coupleId`, and nothing in the UI reads `date_plan_preferences`).
|
||||
**Fix (R15, N-001)**: `BucketListViewModel` resolves the couple **itself** in `init` via `CoupleRepository.getCoupleForUser(uid)` → `setCoupleId` → `loadItems` (mirrors `MemoryLaneViewModel`/`YourProgressViewModel`, the correct pattern). Bucket items encrypt at rest (`enc:v1:`) once a real coupleId flows.
|
||||
**Re-introduction risk**: the safe pattern is a VM that **resolves its own required context** (couple/uid) in `init` via the injected repository — NOT one that depends on the screen remembering to call a `setX(...)`. Audit: `grep -rn "setCoupleId\|setDateIdeaId\|fun set[A-Z]" ui/**/ViewModel` and confirm a caller exists, or move the resolution into the VM. **A silent `if (x.isEmpty()) return` guard makes a dead feature look like an empty one** — QA must persist real data and confirm it via an admin Firestore read, never trust the empty-state render. (N-002 is a deeper *incomplete feature*: even fixing the save writes into a collection no screen displays — needs a product decision on what "Plan a Date" does + where the plan is shown.)
|
||||
|
||||
### 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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue