Closer/app
null 7341f64ae2 fix(crypto): warm apps adopt rotations via a couple-doc watcher; streak RMW → transaction
Landmine audit after C-ROTATE-001 (user: "any landmines that need fixing?"),
hunting the class that has now bitten twice — stale reads feeding one-shot
decisions. Two found, both fixed.

1. A WARM app never adopted a partner's rotation at all. Adoption ran at process
   start (MainActivity, once per uid) and on Home's initial load — and nowhere
   else. The 🔑 push routes to Security, which doesn't adopt; the couple listener
   in HomeViewModel watches the USER doc (pairing state), not the couple doc. So
   an app sitting open on any screen when the partner rotates renders 🔒 for
   every new-key message until process death — and a foregrounded process can
   stay warm for days. Fix: the couple doc is now observable
   (FirestoreCoupleDataSource.observeCouple → CoupleRepository.observeCoupleForUser,
   composed observeUser → coupleId → flatMapLatest so pairing changes reroute
   the stream), and CoupleKeyRotationAdopter.watch(uid) adopts on every
   emission. MainActivity's hook is now that long-lived watch (collectLatest on
   the auth uid: sign-out cancels it cleanly). Snapshot listeners deliver the
   partner's write from the server within seconds — no cache trap, no push
   required, warm or cold. Adoption stays idempotent, a failed adoption is
   recorded and the watch keeps collecting, and a stream error is recorded and
   ends the watch quietly (degrades to the one-shot hooks, never crashes).

2. updateStreak was the same disease verbatim: cache-first get() → compute next
   streak → merge write. A stale snapshot double-counts or clobbers the streak
   when both partners answer near-simultaneously. It now runs as a Firestore
   transaction — server-read values, retried on contention, concurrent answers
   converge. Same StreakCalculator semantics, same skip-if-unchanged.

Audited and deliberately left alone: updateDisplayName/updateSex read coupleId
via a cache-first snapshot, but a stale/missing coupleId only means the field is
stored plaintext until migrateProfileFields heals it on key-unlock — self-
healing already exists, so no change. requestRestore is stale-cache-immune
(delete-then-set on a fixed doc id). The backup manifest already does CAS.

CoupleKeyRotationAdopterTest pins the watcher contract (adopt per emission, skip
nulls, keep collecting past failures, stream errors recorded not thrown);
mutation-checked — dropping the recording fails exactly those two tests. Full
suite green, assembleDebug clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:46:52 -05:00
..
schemas
src fix(crypto): warm apps adopt rotations via a couple-doc watcher; streak RMW → transaction 2026-07-15 19:46:52 -05:00
build.gradle.kts feat(avatar): frame a picked photo, and fall back to initials not a heart 2026-07-14 23:19:37 -05:00
proguard-rules.pro