From 29cc39aa42d2ac24694df70ecaf691c9db2b0956 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 8 Jul 2026 23:28:23 -0500 Subject: [PATCH] =?UTF-8?q?docs(manual):=20batch=208=20review=20=E2=80=94?= =?UTF-8?q?=20restored=20'How=20to=20gate=20a=20new=20feature'=20section?= =?UTF-8?q?=20+=20game-push=20flag=20claim=20was=20wrong?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/Engineering_Reference_Manual.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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+)