Files
sendou.ink/app/features/notifications/core/pushSubscription.ts
2026-09-02 21:02:01 +03:00

52 lines
1.9 KiB
TypeScript

import { Config } from "~/config";
import { logger } from "~/utils/logger";
import { NOTIFICATIONS_SUBSCRIBE_ROUTE } from "~/utils/urls";
/** Whether this browser supports push notifications. Only call after hydration. */
export function isPushSupported() {
return (
"serviceWorker" in navigator &&
"PushManager" in window &&
"Notification" in window
);
}
/** Subscribes this browser to push (reusing a live subscription) and syncs it to the server. Throws if either fails, meaning the server may not know where to deliver. */
export async function subscribeToPush() {
const registration = await navigator.serviceWorker.register("/sw-2.js");
const subscription =
(await registration.pushManager.getSubscription()) ??
(await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: Config.vapid.publicKey,
}));
const response = await fetch(NOTIFICATIONS_SUBSCRIBE_ROUTE, {
method: "post",
body: JSON.stringify(subscription),
headers: { "content-type": "application/json" },
});
if (!response.ok) {
throw new Error(
`Syncing push subscription to server failed: ${response.status}`,
);
}
}
/** This browser's live push subscription, or null (never subscribed, expired, revoked). Only call after hydration. */
export async function findPushSubscription() {
const registration = await navigator.serviceWorker.getRegistration();
return (await registration?.pushManager.getSubscription()) ?? null;
}
/** Self-heals push for an opted-in browser: resubscribes and re-syncs when the subscription expired, was revoked, or the server lost its copy (e.g. 410 Gone). Safe on every load; failures are swallowed. */
export async function resyncPushSubscription() {
if (!isPushSupported() || Notification.permission !== "granted") return;
try {
await subscribeToPush();
} catch (err) {
logger.error("Failed to resync push subscription", err);
}
}