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:
parent
ceb98f4abd
commit
29cc39aa42
|
|
@ -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+)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue