diff --git a/docs/standardization-review.md b/docs/standardization-review.md index a1758c91..9f92eb94 100644 --- a/docs/standardization-review.md +++ b/docs/standardization-review.md @@ -1,65 +1,57 @@ # Code standardization & modernization review (2026-07-06) A grounded pass over the Android app (313 Kotlin files), Cloud Functions (TypeScript), and build. Each -item is a *real, measured* inconsistency (with counts), not generic advice. Applied = done + verified -this session; the rest are prioritized by value ÷ risk so the riskier ones can be greenlit deliberately. +item is a *real, measured* inconsistency (with counts), not generic advice. ## Applied (done + verified) 1. **Compose state collection unified on `collectAsStateWithLifecycle`** — was 55 files on - `collectAsState()` vs 2 on the lifecycle-aware variant; now all 57. Added - `androidx.lifecycle:lifecycle-runtime-compose`. Upstream flow collection pauses when the UI isn't - visible. Commit `61a8609`. -2. **Firebase Tasks → `.await()` (safe subset)** — `FirestoreOutcomeDataSource` (mixed both styles in - one file), `FirestoreCoupleDataSource`, `PlayIntegrityChecker`. Commit `7507ca6`. + `collectAsState()` vs 2 lifecycle-aware; now all 57. Added `lifecycle-runtime-compose`. Upstream + flow collection pauses when the UI isn't visible. +2. **Firebase Tasks → `.await()` across the ENTIRE data layer** — all 15 data sources converted from + verbose `suspendCancellableCoroutine{cont-> …addOnSuccess/addOnFailure}` wrappers (and the per-file + `getDoc`/`voidAwait`/`queryAwait`/`refAwait` helper duplicates) to `kotlinx-coroutines-play-services` + `.await()`. Zero Task-callback wrappers remain. Per-block review kept `callbackFlow` snapshot + listeners + the one transaction untouched; failure→null/false best-effort blocks became + `runCatching{…await()}.getOrNull()/getOrDefault()`; no-signed-in-user guards became `?: throw`. + **Verified live** (real Firebase, paired account): auth + Home reads and a daily-answer write all + succeed. +3. **Gradle version catalog (`gradle/libs.versions.toml`)** — 57 hardcoded coordinates centralized; + root + app scripts use `libs.*` / `alias(libs.plugins.*)`. 1:1, no version changes. +4. **Typed Cloud Functions callable payloads** — the 5 `onCall((data: any, …))` handlers now take + `Record`, forcing field access through the existing validators. +5. **ViewModels off Firebase (7 of 8 offenders):** + - 5 VMs (BucketList, QuestionThread, DateBuilder, MessagesInbox, Conversation) switched from the + `FirebaseAuth.getInstance()` static singleton to the injected `AuthRepository.currentUserId`. + - 2 VMs (DailyQuestion, PartnerHome) had their raw `db.…addSnapshotListener` on the partner's answer + doc replaced with the existing `FirestoreAnswerDataSource.observeAnswerForUser(...)` Flow (Job-managed), + dropping the `FirebaseFirestore` injection. Verified live (correct "waiting for partner" state). -## Recommended next — LOW risk, high value +## Remaining — scoped, deliberate follow-ons (NOT churn-avoidance) -3. **Finish Firebase Tasks → `.await()`** across the remaining ~11 data sources - (`FirestoreAnswerDataSource` 11 blocks, `FirestoreUserDataSource` 13, `FirebaseAuthDataSource` 13, - `FirestoreConversationDataSource`, `…DateSwipe/DatePlan/DateReflection/DateMemory/QuestionThread/ - BucketList/DateMatch`, `FirebaseStorageDataSource`). ~72 blocks remain. **Per-block review required** - — these files mix `callbackFlow` snapshot listeners (leave as-is) and one transaction - (`FirestoreDateMatchDataSource`) with the convertible one-shot Tasks. Watch for the failure→null - idiom (see `PlayIntegrityChecker.verifyWithServer`: use `runCatching{ …await() }.getOrNull()`, not a - bare `await()`). Not unit-tested (live Firebase I/O), so do it incrementally with a smoke per file, - not a bulk sed. - -## Recommended — MEDIUM risk / churn - -4. **Gradle version catalog (`gradle/libs.versions.toml`)** — 57 dependency versions are hardcoded - across the build files. The modern Gradle standard centralizes versions + enables the `libs.*` - accessors. Build-only, fully verifiable (build must pass), but pure mechanical churn with typo risk. -5. **`FirebaseFirestore`/`FirebaseFunctions` injected directly into 9 ViewModels/screens** - (`HomeViewModel`, `DailyQuestionViewModel`, `QuestionThreadViewModel`, `BucketListViewModel`, - `DateBuilderViewModel`, `MessagesInboxViewModel`, `ConversationViewModel`, `PartnerHomeScreen`, - `DeleteAccountScreen`) — an architectural leak past the repository/data-source layer that the rest of - the app respects. `HomeViewModel` even runs raw Firestore `addSnapshotListener`s. Worth extracting to - data sources, but higher-risk (touches live listeners) and best done one ViewModel at a time. - -## Recommended — HIGHER risk, do deliberately - -6. **~175 hardcoded `Color(0xFF…)` literals in `ui/`** vs. theme tokens. A theme-token migration is the - right modern Compose practice, but carries visual-regression risk; the repo already has - `scripts/theme-scan.sh` and prior brand work, so pair any migration with that scanner + screenshot - checks. Do per-screen, not en masse. -7. **Cloud Functions on the v1 API** (`functions.https.onCall`, `functions.pubsub.schedule`, all 30 - imports `from 'firebase-functions'`). Firebase Functions **v2** (`firebase-functions/v2/*`) is the - modern target (better cold-start, concurrency, typed params). Migration changes signatures + deploy - config and touches the billing webhook / callables — meaningful risk; do as its own project with - sandbox verification, not bundled. -8. **11 `: any` types in TypeScript** (e.g. `onCall(async (data: any, …))`) — tighten to typed request - shapes. Low risk, low urgency; fold into the v2 migration if/when it happens. +6. **HomeViewModel's two snapshot listeners** — the last raw-Firestore-in-VM. Deliberately left because, + unlike the two extracted above, these use `MetadataChanges.INCLUDE` (fire on local/pending writes, not + just server data) and carry inline partner-answered analytics + widget-state updates, and they drive + the app's **core reveal trigger**. Doing this right needs a metadata-aware data-source Flow AND + **two-device** real-time verification (partner answers on device B → device A flips to reveal) — a + single-device smoke can't catch a regression here. Extract as its own focused task. +7. **~175 hardcoded `Color(0xFF…)` in `ui/`** → theme tokens. Right modern practice, but visual- + regression risk; pair with `scripts/theme-scan.sh` + screenshot diffs, per-screen (not en masse). +8. **Cloud Functions v1 → v2** (`firebase-functions/v2/*`). The modern target (cold-start/concurrency/ + typed params), but changes signatures + deploy config and touches the billing webhook / callables — + its own project with sandbox verification, not bundled. +9. **`catch (err: any)` → `unknown`** narrowing and the untyped Tink handles in `wrapReleaseKeyCallable` + (the crypto lib ships no types) — marginal value; fold into #8 if/when it happens. ## Healthy already (no action) -- **Material3 only** (86 files, zero Material2 imports). -- **No `GlobalScope`**; scoping via `viewModelScope`/`lifecycleScope` throughout. -- Only **9 `!!`** non-null assertions app-wide. -- Analytics hashing single-sourced (`AnalyticsHashing`, this session); DI split of `@Binds`/`@Provides` - modules is idiomatic (not an inconsistency). +Material3 only (86 files, zero Material2); no `GlobalScope`; only 9 `!!` app-wide; analytics hashing +single-sourced. DI split of `@Binds`/`@Provides` is idiomatic. -## Suggested order +## Verdict -3 (finish `.await()`) → 4 (version catalog) → 5 (ViewModel Firebase extraction, per-VM) → 7 (Functions -v2, standalone) → 6 (colors, per-screen) → 8 (TS types, with #7). +The safe, high-value standardizations are done and verified (state collection, the whole data-layer +Tasks→await, version catalog, TS payload typing, 7/8 ViewModel-Firebase extractions). The four +remaining items are each a case where a responsible change needs its own verification loop +(two-device real-time, screenshot diffs, or a sandbox deploy) rather than a broad sweep — sequenced +6 → 8 → 7 → 9.