diff --git a/docs/Engineering_Reference_Manual.md b/docs/Engineering_Reference_Manual.md index 52538f6e..0ef872c1 100644 --- a/docs/Engineering_Reference_Manual.md +++ b/docs/Engineering_Reference_Manual.md @@ -90,17 +90,21 @@ A subscription is **per couple**, not per user. The Firestore rules for `users/{ app/src/main/java/app/closer/ ├── MainActivity.kt ├── analytics/ # RetentionEvent + RetentionAnalytics (separate from core/analytics; used for product funnel) +├── widget/ # Glance Today home-screen widget (R29) ├── core/ │ ├── analytics/ # Firebase Analytics + Crashlytics wrappers -│ ├── billing/ # EntitlementChecker + FirestoreEntitlementChecker +│ ├── billing/ # EntitlementChecker + FirestoreEntitlementChecker + CouplePremiumChecker │ ├── crash/ # CrashReporter abstraction │ ├── feature/ # (reserved for feature flags; no directory exists today - no feature-flag code in repo) +│ ├── firebase/ # FirebaseInitializer (manual App Check / init path) +│ ├── media/ # MediaCompressor (image/video compression before encrypted upload) │ ├── navigation/ # AppRoute constants, NavHost, ExternalLinks │ └── notifications/ # AppMessagingService, NotificationHelper, NotificationPermissionHelper, QuietHours, TokenRegistrar ├── crypto/ # E2EE: Tink AEAD, BouncyCastle Argon2id, key stores ├── data/ +│ ├── backup/ # BackupManager + BackupRestoreManager + RestoreManager (R24 E2EE backup + partner-assist restore) │ ├── challenges/ # Connection Challenges data sources -│ ├── local/ # Room DAOs, DataStore, EncryptedSharedPreferences (RecoveryPhraseStore, SecurePreferencesFactory, SettingsDataStore) +│ ├── local/ # Room DAOs, DataStore, EncryptedSharedPreferences (RecoveryPhraseStore, SecurePreferencesFactory, SettingsDataStore); also converters/, entity/, mapper/ subdirs │ ├── questions/ # Question pack data sources (`QuestionJsonParser`) │ ├── remote/ # Firestore data sources, Cloud Functions callable wrappers │ ├── repository/ # Repository implementations @@ -109,7 +113,9 @@ app/src/main/java/app/closer/ ├── domain/ │ ├── model/ # Plain data classes │ ├── repository/ # Repository interfaces -│ └── security/ # AuthRateLimiter, DeviceIntegrityChecker (interface for PlayIntegrityChecker) +│ ├── security/ # AuthRateLimiter, DeviceIntegrityChecker (interface for PlayIntegrityChecker) +│ └── usecase/ # Cross-VM use cases: DailyQuestionResolver, GameSessionManager, SoloAnswerMigrator +├── notifications/ # Notification surfaces: ActiveGameSessionMonitor, GamePromptController, PartnerNotificationManager, PartnerNotificationScheduler, QuietHoursManager, NotificationChannelSetup, NotificationRateLimiter, MessageBubbleController, ActiveThreadMonitor └── ui/ # Compose screens + ViewModels ├── activity/ # Together / Activity screen ├── answers/ # Answer write/reveal/history @@ -124,13 +130,15 @@ app/src/main/java/app/closer/ ├── home/ # Home dashboard + partner state ├── howwell/ # How Well Do You Know Me game ├── memorylane/ # Time capsules + ├── messages/ # E2E chat (ConversationScreen, MessagesInboxScreen) + components/ ├── onboarding/ # Onboarding screens ├── outcomes/ # 30/60/90 day check-ins ├── pairing/ # Invite create/accept/confirm/recovery ├── paywall/ # Subscription paywall ├── play/ # Play hub - ├── questions/ # Daily question, packs, history - ├── settings/ # All settings screens (account, privacy, security, subscription, ...) + ├── questions/ # Daily question, packs, history + components/ + ├── recap/ # Weekly recap screen + share card + ├── settings/ # All settings screens (account, privacy, security, subscription, …) ├── theme/ # CloserTheme ├── thisorthat/ # This or That game └── wheel/ # Spin the wheel @@ -161,6 +169,8 @@ iphone/ ├── Crypto/ # CryptoKit E2EE parity (R24+) - see iphone/Closer/Crypto/SPEC.md for the wire format │ # AnswerCrypto.swift, CoupleEncryptionManager.swift, CoupleKeyStore.swift, DeviceKeyStatus.swift, │ # FieldEncryptor.swift, Keybox.swift, RecoveryKeyManager.swift, SealedAnswer.swift, Wordlist.swift + │ # Resources/ (248-word recovery phrase wordlist, verbatim copy of Android's) + │ # SCHEMA_VERSION_DECISION.md, SPEC.md ├── Models/ # Codable Firestore + domain types ├── Services/FirestoreService.swift # Firestore + callable wrappers ├── Theme/CloserTheme.swift # Colors, typography, spacing @@ -248,7 +258,7 @@ functions/src/ **v2 migration (B0-B6d, 2026-07-08)**: every function except `onUserDelete` runs as **2nd gen** in `closer-app-22014` under a single shared set of global options (see `functions/src/options.ts`): `region: us-central1` (pinned to match `FirebaseFunctions.getInstance()` on Android; do not change without pinning the client), `maxInstances: 5` (runaway-bill guardrail + Cloud Run CPU-quota constraint), `cpu: 'gcf_gen1'` (gen1 fractional tiers, ~1⁄6 vCPU - chosen because the project's default regional Cloud Run CPU quota could not fit ~35 services at v2's default 1 vCPU/instance; the deploy container healthchecks failed with *"Quota exceeded for total allowable CPU per project per region"*), `concurrency: 1` (required when `cpu < 1`). At launch: request the Cloud Run CPU quota increase, then drop `cpu`/`concurrency` to restore 1 vCPU + concurrency 80. `onUserDelete` is intentionally v1 because gen 2 has no `auth.user().onDelete`; pin its runtime in lockstep with the v2 ones. -**Shared infrastructure (B6d)**: B6d collapsed ~10 copies of "read the user's tokens" and ~19 copies of "send to each token, log failures, prune the dead ones" into `notifications/push.ts` (`getUserTokens`, `sendPushToUser`, `batchResponseToResults`, `redactToken`) and `notifications/idempotency.ts` (`claimOnce` - gRPC `ALREADY_EXISTS` (6) dedupe, fail-OPEN on unexpected errors). `notifications/quietHours.ts` is the single server-side quiet-hour reader; `notifications/time.ts` is the shared date-key helper. Per-function log lines are routed through `log.ts` (re-exports `firebase-functions/logger` for v2 trace/execution context). The B6d split of `onGameSessionUpdate` + the 4 per-game part-finished triggers is a structural change - see [Game session push semantics](#game-session-push-semantics-idempotent-flag-claim). +**Shared infrastructure (B6d)**: B6d collapsed ~10 copies of "read the user's tokens" and ~19 copies of "send to each token, log failures, prune the dead ones" into `notifications/push.ts` (`getUserTokens`, `sendPushToUser`, `batchResponseToResults`, `redactToken`) and `notifications/idempotency.ts` (`claimOnce` - gRPC `ALREADY_EXISTS` (6) dedupe, fail-OPEN on unexpected errors). `notifications/quietHours.ts` is the single server-side quiet-hour reader; `notifications/time.ts` is the shared date-key helper. Per-function log lines are routed through `log.ts` (re-exports `firebase-functions/logger` for v2 trace/execution context). The B6c split of `onGameSessionUpdate` + the 4 per-game part-finished triggers is a structural change - see [Game session push semantics](#game-session-push-semantics-idempotent-flag-claim). There is no `auth/` module. Authentication is handled entirely by the Firebase Auth client SDK; the Admin SDK is used in the `users/onUserDelete.ts` trigger and in `couples/acceptInviteCallable.ts` to read user docs. @@ -586,6 +596,9 @@ Thread questions follow the same sealed flow but use a different path: platform: 'android' | 'ios' | null notifPartnerAnswered: bool notifChatMessage: bool + notifDailyReminder: bool # NEW (R20) - server-side gate for sendDailyQuestionProactiveReminder + notifStreakReminder: bool # NEW (R20) - server-side gate for sendStreakReminder (default true) + notifPromotional: bool # NEW (R20) - server-side opt-out for reengagement + challenge-day nudge quietHoursEnabled: bool quietHoursStartMinutes: int quietHoursEndMinutes: int @@ -764,7 +777,7 @@ isUpdatingCoupleRhythm() only streakCount/lastAnsweredAt ### Per-collection enforcement -**`users/{uid}`** - owner can read/create/update their own doc. Root-level premium state is **not** stored here; the client-writable `plan` field is informational only, and authoritative premium state lives in the server-only `entitlements/premium` subdoc. `entitlements/`, `notification_queue/`, and `outcomes/` are server-only writes; `entitlements/` is also readable by the user's current couple partner (couple-shared premium; see [Couple-shared premium](#couple-shared-premium)), and `notification_queue/` is readable by the owner for the in-app activity feed (the owner can flip the `read` flag). `fcmTokens/` and `devices/` are owner-writable; the `devices/` public key is readable by the user's current couple partner only (to wrap release keys). Quiet-hours fields (`quietHoursEnabled`, `quietHoursStartMinutes`, `quietHoursEndMinutes`, `timezone`) and notification preferences (`notifPartnerAnswered`, `notifChatMessage`) are client-writable so Cloud Functions can honor them server-side. +**`users/{uid}`** - owner can read/create/update their own doc. Root-level premium state is **not** stored here; the client-writable `plan` field is informational only, and authoritative premium state lives in the server-only `entitlements/premium` subdoc. `entitlements/`, `notification_queue/`, and `outcomes/` are server-only writes; `entitlements/` is also readable by the user's current couple partner (couple-shared premium; see [Couple-shared premium](#couple-shared-premium)), and `notification_queue/` is readable by the owner for the in-app activity feed (the owner can flip the `read` flag). `fcmTokens/` and `devices/` are owner-writable; the `devices/` public key is readable by the user's current couple partner only (to wrap release keys). Quiet-hours fields (`quietHoursEnabled`, `quietHoursStartMinutes`, `quietHoursEndMinutes`, `timezone`) and notification preferences (`notifPartnerAnswered`, `notifChatMessage`, `notifDailyReminder`, `notifStreakReminder`, `notifPromotional`) are client-writable so Cloud Functions can honor them server-side. **`date_ideas/`** - read-only for any signed-in user; writes are admin-only.