diff --git a/docs/Engineering_Reference_Manual.md b/docs/Engineering_Reference_Manual.md index ad673632..11b54c18 100644 --- a/docs/Engineering_Reference_Manual.md +++ b/docs/Engineering_Reference_Manual.md @@ -884,7 +884,7 @@ The Android `FirestoreEntitlementChecker` is the source of truth for premium sta 3. Treats an `expiresAt` in the past as `premium = false`. 4. **Fails closed**: on Firestore listener error, `isPremium()` emits `false` (i.e. "not premium") rather than guessing. -The iOS `DefaultEntitlementChecker` actor does **not** observe Firestore entitlements. It reads RevenueCat `CustomerInfo` only, via `Purchases.shared.customerInfoStream`. **iOS premium state is therefore client-verified, not server-verified** - this is a known gap that should be closed before production. +The iOS `BillingService` (`iphone/Closer/Core/Billing/BillingService.swift`) does **not** observe Firestore entitlements. It reads RevenueCat `CustomerInfo` only, via `Purchases.shared.customerInfoStream` exposed as `BillingService.customerInfoStream`. **iOS premium state is therefore client-verified, not server-verified** - this is a known gap that should be closed before production. The Android app uses both `EntitlementChecker` (per-user server-verified premium) and `CouplePremiumChecker` (couple-shared premium: true if either partner has premium). `CouplePremiumChecker` is the drop-in used for couple-wide gates like chat media, shared games, and date features so a single subscription covers both partners. @@ -914,17 +914,26 @@ Callers collect `isPremium()` reactively rather than caching a one-time snapshot The `EntitlementChecker.isPremium()` Flow is collected by feature-level ViewModels; features that gate render the paywall deep-link on a `false` value rather than throwing or showing empty state. -**Gated features (current list):** +**Gated features (current list, as of 2026-07-08):** -- Premium question packs (`QuestionPackRepositoryImpl.isPremiumGate()`) -- Full answer history (free shows last 7; premium shows all) -- Custom questions -- Private notes -- Extra categories (beyond the free tier) -- Full spin-wheel session history -- Future AI-assisted question suggestions +- **Premium question packs** - `QuestionPackLibraryViewModel` injects `CouplePremiumChecker`; the pack filter `PackFilter.PREMIUM` plus each `QuestionCategory.access == "premium"` check. +- **Premium wheel categories** - `CategoryPickerViewModel` sets `isLocked = category.access == "premium" && !hasPremium`; tapping a locked category routes to the paywall. +- **Date Match premium ideas** - `DateMatchViewModel` (R12 fix for **A-201**); swiping LOVE/MAYBE on a premium idea when neither partner is premium emits `paywallRequired` and routes to the paywall. +- **Full spin-wheel session history** - `WheelHistoryViewModel` observes `premiumChecker.isPremium()` and limits the free tier to the most recent sessions. +- **Chat media** - `ConversationViewModel` (R24) gates image/voice upload on `couplePremiumChecker` (this is the canonical couple-shared gate - "either partner has Premium unlocks chat media for the couple"). -**How to gate a new feature:** +**NOT currently gated** (the v0.2.1 manual claimed these but the source has no gate): + +- "Full answer history (free shows last 7; premium shows all)" - `AnswerHistoryViewModel` has NO `CouplePremiumChecker` or `EntitlementChecker` injection. The 7-answer cap is not implemented. +- "Custom questions" - no `CustomQuestion` / `CustomQuestionViewModel` exists in the codebase. +- "Private notes" - no `PrivateNote` / `PrivateNoteViewModel` exists in the codebase. +- "Extra categories (beyond the free tier)" - the manual conflates this with the "Premium wheel categories" gate above. There is no separate category count cap. + +**Future (planned):** + +- AI-assisted question suggestions (mentioned in the v0.1.0 plan; no source yet). + +If you add a new premium feature, mirror the established pattern: inject `CouplePremiumChecker` (or `EntitlementChecker` for the rare per-user gate), collect `isPremium()` in the VM, navigate to `paywallScreen()` on `false`. Server-side: any new premium-only Cloud Function must verify `users/{uid}/entitlements/premium` before doing work. Do not trust the client flag for server-side gating.**How to gate a new feature:** 1. Inject `EntitlementChecker` into the relevant ViewModel or Repository. 2. On entry, collect `isPremium()`. If `false`, navigate to the paywall route (`paywallScreen()` in `AppNavigation`) with a returnTo argument. Do not silently fail or render empty state.