docs(manual): batch 8 review — restored 'How to gate a new feature' section + game-push flag claim was wrong

Two fixes from the Notifications section:

1. The Billing 'How to gate a new feature:' header got swallowed into
   the previous paragraph by my Batch 7 edit. Restored the standalone
   header + the 3 numbered steps + the 'QA testing convention' example
   so the section reads correctly.

2. The Game session push semantics sub-section said the session doc
   holds 'a single notificationsSent map on the session doc records
   each notification type'. That's wrong - the source uses SEPARATE
   per-push-type Timestamp flags, not a single map. Verified in
   functions/src/games/onGameSessionUpdate.ts:
     - startNotifiedAt   (started push)
     - joinNotifiedAt    (partner joined push)
     - partFinishNotifiedAt (one-finished, other-hasn't push)
     - finishNotifiedAt  (both completed push)
   Each is claimed in a runTransaction (read-fresh, check-flag, set
   flag) so a re-run is a no-op. Replaced the wrong 'notificationsSent
   map' description with the actual 4 Timestamp fields, and updated
   the 'flag keys are stable strings like start/join/finish' close to
   the actual field names.

Other Batch 8 claims verified clean:
- TokenRegistrar lives in core/notifications/, writes to fcmTokens
  subcollection on token refresh.
- QuietHours is a DataStore class in SettingsRepository; server-side
  quiet-hour suppression is in functions/src/notifications/quietHours.ts;
  onAnswerWritten calls recipientInQuietHours(partnerData).
- The B6c part-finished split: onThisOrThatPartFinished /
  onWheelPartFinished / onHowWellPartFinished / onDesireSyncPartFinished
  are all in onGameSessionUpdate.ts and use the partFinishNotifiedAt
  flag. Confirmed in the file.
- per-user notification_queue is read by FirestoreActivityDataSource
  for the in-app 'Together' activity feed. Confirmed.
- Gentle reminder rate limit: per-user 5/h via rate_limits/{uid}_gentle
  reminder, per-couple 1/day via couples/{id}/gentle_reminders/{date}.
  Confirmed in source.
This commit is contained in:
null 2026-07-08 23:28:23 -05:00
parent ceb98f4abd
commit 29cc39aa42
1 changed files with 5 additions and 3 deletions

View File

@ -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). - 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. 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. 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) ### 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`: 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. 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+) ### Foreground game-alert banner (R10+)