50 lines
2.0 KiB
TypeScript
50 lines
2.0 KiB
TypeScript
/**
|
||
* Quiet-hours suppression for partner-action pushes.
|
||
*
|
||
* The Settings UI promises "10 PM – 8 AM, no notifications". The client stores the window in local
|
||
* DataStore AND mirrors it to the recipient's `users/{uid}` doc (quietHoursEnabled / *StartMinutes /
|
||
* *EndMinutes / timezone). Because a partner push carries a `notification` block, the OS shows it
|
||
* directly when the recipient app is backgrounded/killed — so the only place the promise can be kept
|
||
* is server-side, here, before the push is sent (M-001).
|
||
*
|
||
* FAIL-OPEN by design: if quiet hours is not explicitly enabled, or any field (window/timezone) is
|
||
* missing or malformed, we return `false` (do NOT suppress). A bug here can therefore only ever fall
|
||
* back to today's behavior (notification delivered) — it can never wrongly drop a notification, and
|
||
* existing installs keep delivering exactly as before until the client backfills the fields.
|
||
*/
|
||
export function recipientInQuietHours(
|
||
userData: FirebaseFirestore.DocumentData | undefined,
|
||
now: Date = new Date()
|
||
): boolean {
|
||
if (!userData || userData.quietHoursEnabled !== true) return false
|
||
|
||
const start = userData.quietHoursStartMinutes
|
||
const end = userData.quietHoursEndMinutes
|
||
const tz = userData.timezone
|
||
if (typeof start !== 'number' || typeof end !== 'number' || typeof tz !== 'string' || !tz) {
|
||
return false
|
||
}
|
||
|
||
let nowMinutes: number
|
||
try {
|
||
const parts = new Intl.DateTimeFormat('en-US', {
|
||
timeZone: tz,
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false,
|
||
}).formatToParts(now)
|
||
const hour = Number(parts.find((p) => p.type === 'hour')?.value) % 24
|
||
const minute = Number(parts.find((p) => p.type === 'minute')?.value)
|
||
if (Number.isNaN(hour) || Number.isNaN(minute)) return false
|
||
nowMinutes = hour * 60 + minute
|
||
} catch {
|
||
// Unknown/invalid timezone id → fail open.
|
||
return false
|
||
}
|
||
|
||
// Window may cross midnight (e.g. 22:00 → 08:00).
|
||
return start <= end
|
||
? nowMinutes >= start && nowMinutes <= end
|
||
: nowMinutes >= start || nowMinutes <= end
|
||
}
|