diff --git a/docs/Engineering_Reference_Manual.md b/docs/Engineering_Reference_Manual.md index 11b54c18..b2526d5c 100644 --- a/docs/Engineering_Reference_Manual.md +++ b/docs/Engineering_Reference_Manual.md @@ -933,7 +933,9 @@ The `EntitlementChecker.isPremium()` Flow is collected by feature-level ViewMode - 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:** +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. @@ -998,7 +1000,7 @@ The notification is both an FCM push (for the system tray) and an entry in `user ### Game session push semantics (idempotent flag-claim) -The Cloud Function `functions/src/games/onGameSessionUpdate.ts` is the hub for every partner-facing push derived from a game session. The original implementation diffed `status` fields to decide whether to send a push, which produced duplicate notifications when both partners updated the doc close together. The current implementation uses an **idempotent flag-claim** pattern: a single `notificationsSent` map on the session doc records each notification type the moment it's dispatched. The trigger checks the flag, sends if absent, writes the flag. Re-running the trigger is a no-op. +The Cloud Function `functions/src/games/onGameSessionUpdate.ts` is the hub for every partner-facing push derived from a game session. The original implementation diffed `status` fields to decide whether to send a push, which produced duplicate notifications when both partners updated the doc close together. The current implementation uses an **idempotent flag-claim** pattern: a separate server-only Timestamp per notification type on the session doc (`startNotifiedAt`, `joinNotifiedAt`, `partFinishNotifiedAt`, `finishNotifiedAt`) is claimed in a transaction the moment the corresponding push is dispatched. The trigger checks the flag, sends if absent, writes the flag. Re-running the trigger is a no-op. B6c split the same file into **four per-game part-finished triggers** plus the main `onGameSessionUpdate`: @@ -1009,7 +1011,7 @@ B6c split the same file into **four per-game part-finished triggers** plus the m Each fires when its session's `answers` subcollection has exactly one key (one partner finished, the other hasn't) and idempotently claims `partFinishNotifiedAt` on the session doc before sending `partner_completed_part` to the other member. Replacing the old single "if both answers exist, notify finished" pattern. See E-GAME-003 below. -This pattern is the rule for any partner-facing push derived from a game session - **never diff, always claim**. The flag keys are stable strings like `'start'`, `'join'`, `'finish'`, `'partner_answered'` (game-specific). +This pattern is the rule for any partner-facing push derived from a game session - **never diff, always claim**. The flag keys are per-push-type Timestamps: `startNotifiedAt` (started), `joinNotifiedAt` (partner joined), `partFinishNotifiedAt` (one partner finished, the other hasn't), `finishNotifiedAt` (both completed). ### Foreground game-alert banner (R10+)