docs(manual): review pass — missing dirs + 3 notif prefs + B6c/B6d mix-up
After pushing the Phase 2 sync, evidence-first review against the live
repo caught:
- Repository layout (Android) was missing directories that have shipped
since v0.2.1: widget/ (Glance Today, R29), core/firebase/, core/media/,
data/backup/ (R24 E2EE backup + partner-assist), data/local/{converters,
entity,mapper} subdirs, data/security/, domain/usecase/ (resolver +
GameSessionManager + SoloAnswerMigrator), top-level notifications/
package, and ui/{recap,messages,questions}/ + components/ subdirs.
- Repository layout (iOS) was missing Crypto/Resources/ (wordlist) and
the Crypto/ design notes (SCHEMA_VERSION_DECISION.md, SPEC.md).
- User doc field list + per-collection enforcement were missing the 3
R20 notif preferences: notifDailyReminder, notifStreakReminder,
notifPromotional. Verified all 5 are mirrored by
FirestoreUserDataSource.updateNotificationPrefs() and listed in the
firestore.rules user-doc allowlist.
- One copy fix: 'The B6d split of onGameSessionUpdate' -> 'The B6c
split' (B6c did the part-finished split; B6d was the logger finish).
Anchors verified clean (30/30). DEVELOPMENT_LOG.md is gitignored so
the local review notes stay on this box.
This commit is contained in:
parent
1242799001
commit
d31b58d238
|
|
@ -90,17 +90,21 @@ A subscription is **per couple**, not per user. The Firestore rules for `users/{
|
||||||
app/src/main/java/app/closer/
|
app/src/main/java/app/closer/
|
||||||
├── MainActivity.kt
|
├── MainActivity.kt
|
||||||
├── analytics/ # RetentionEvent + RetentionAnalytics (separate from core/analytics; used for product funnel)
|
├── analytics/ # RetentionEvent + RetentionAnalytics (separate from core/analytics; used for product funnel)
|
||||||
|
├── widget/ # Glance Today home-screen widget (R29)
|
||||||
├── core/
|
├── core/
|
||||||
│ ├── analytics/ # Firebase Analytics + Crashlytics wrappers
|
│ ├── analytics/ # Firebase Analytics + Crashlytics wrappers
|
||||||
│ ├── billing/ # EntitlementChecker + FirestoreEntitlementChecker
|
│ ├── billing/ # EntitlementChecker + FirestoreEntitlementChecker + CouplePremiumChecker
|
||||||
│ ├── crash/ # CrashReporter abstraction
|
│ ├── crash/ # CrashReporter abstraction
|
||||||
│ ├── feature/ # (reserved for feature flags; no directory exists today - no feature-flag code in repo)
|
│ ├── 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
|
│ ├── navigation/ # AppRoute constants, NavHost, ExternalLinks
|
||||||
│ └── notifications/ # AppMessagingService, NotificationHelper, NotificationPermissionHelper, QuietHours, TokenRegistrar
|
│ └── notifications/ # AppMessagingService, NotificationHelper, NotificationPermissionHelper, QuietHours, TokenRegistrar
|
||||||
├── crypto/ # E2EE: Tink AEAD, BouncyCastle Argon2id, key stores
|
├── crypto/ # E2EE: Tink AEAD, BouncyCastle Argon2id, key stores
|
||||||
├── data/
|
├── data/
|
||||||
|
│ ├── backup/ # BackupManager + BackupRestoreManager + RestoreManager (R24 E2EE backup + partner-assist restore)
|
||||||
│ ├── challenges/ # Connection Challenges data sources
|
│ ├── 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`)
|
│ ├── questions/ # Question pack data sources (`QuestionJsonParser`)
|
||||||
│ ├── remote/ # Firestore data sources, Cloud Functions callable wrappers
|
│ ├── remote/ # Firestore data sources, Cloud Functions callable wrappers
|
||||||
│ ├── repository/ # Repository implementations
|
│ ├── repository/ # Repository implementations
|
||||||
|
|
@ -109,7 +113,9 @@ app/src/main/java/app/closer/
|
||||||
├── domain/
|
├── domain/
|
||||||
│ ├── model/ # Plain data classes
|
│ ├── model/ # Plain data classes
|
||||||
│ ├── repository/ # Repository interfaces
|
│ ├── 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
|
└── ui/ # Compose screens + ViewModels
|
||||||
├── activity/ # Together / Activity screen
|
├── activity/ # Together / Activity screen
|
||||||
├── answers/ # Answer write/reveal/history
|
├── answers/ # Answer write/reveal/history
|
||||||
|
|
@ -124,13 +130,15 @@ app/src/main/java/app/closer/
|
||||||
├── home/ # Home dashboard + partner state
|
├── home/ # Home dashboard + partner state
|
||||||
├── howwell/ # How Well Do You Know Me game
|
├── howwell/ # How Well Do You Know Me game
|
||||||
├── memorylane/ # Time capsules
|
├── memorylane/ # Time capsules
|
||||||
|
├── messages/ # E2E chat (ConversationScreen, MessagesInboxScreen) + components/
|
||||||
├── onboarding/ # Onboarding screens
|
├── onboarding/ # Onboarding screens
|
||||||
├── outcomes/ # 30/60/90 day check-ins
|
├── outcomes/ # 30/60/90 day check-ins
|
||||||
├── pairing/ # Invite create/accept/confirm/recovery
|
├── pairing/ # Invite create/accept/confirm/recovery
|
||||||
├── paywall/ # Subscription paywall
|
├── paywall/ # Subscription paywall
|
||||||
├── play/ # Play hub
|
├── play/ # Play hub
|
||||||
├── questions/ # Daily question, packs, history
|
├── questions/ # Daily question, packs, history + components/
|
||||||
├── settings/ # All settings screens (account, privacy, security, subscription, ...)
|
├── recap/ # Weekly recap screen + share card
|
||||||
|
├── settings/ # All settings screens (account, privacy, security, subscription, …)
|
||||||
├── theme/ # CloserTheme
|
├── theme/ # CloserTheme
|
||||||
├── thisorthat/ # This or That game
|
├── thisorthat/ # This or That game
|
||||||
└── wheel/ # Spin the wheel
|
└── wheel/ # Spin the wheel
|
||||||
|
|
@ -161,6 +169,8 @@ iphone/
|
||||||
├── Crypto/ # CryptoKit E2EE parity (R24+) - see iphone/Closer/Crypto/SPEC.md for the wire format
|
├── Crypto/ # CryptoKit E2EE parity (R24+) - see iphone/Closer/Crypto/SPEC.md for the wire format
|
||||||
│ # AnswerCrypto.swift, CoupleEncryptionManager.swift, CoupleKeyStore.swift, DeviceKeyStatus.swift,
|
│ # AnswerCrypto.swift, CoupleEncryptionManager.swift, CoupleKeyStore.swift, DeviceKeyStatus.swift,
|
||||||
│ # FieldEncryptor.swift, Keybox.swift, RecoveryKeyManager.swift, SealedAnswer.swift, Wordlist.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
|
├── Models/ # Codable Firestore + domain types
|
||||||
├── Services/FirestoreService.swift # Firestore + callable wrappers
|
├── Services/FirestoreService.swift # Firestore + callable wrappers
|
||||||
├── Theme/CloserTheme.swift # Colors, typography, spacing
|
├── 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.
|
**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.
|
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
|
platform: 'android' | 'ios' | null
|
||||||
notifPartnerAnswered: bool
|
notifPartnerAnswered: bool
|
||||||
notifChatMessage: 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
|
quietHoursEnabled: bool
|
||||||
quietHoursStartMinutes: int
|
quietHoursStartMinutes: int
|
||||||
quietHoursEndMinutes: int
|
quietHoursEndMinutes: int
|
||||||
|
|
@ -764,7 +777,7 @@ isUpdatingCoupleRhythm() only streakCount/lastAnsweredAt
|
||||||
|
|
||||||
### Per-collection enforcement
|
### 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.
|
**`date_ideas/`** - read-only for any signed-in user; writes are admin-only.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue