/** Web-push на клиенте: разрешение браузера, подписка/отписка.
* Работает парой со sw.js (пуш/клик) и API /me/push* (docs/06-settings-pwa.md). */
/** base64url (public key VAPID) → bytes для pushManager */
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const raw = atob(base64);
const output = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i += 1) output[i] = raw.charCodeAt(i);
return output;
}
/** Пуш возможен: SW/https и API браузера. Не поддерживается — UI покажет почему. */
export function pushSupported() {
return Boolean(
typeof window !== "undefined" &&
"serviceWorker" in navigator &&
"PushManager" in window &&
"Notification" in window,
);
}
export function swRegistration() {
return navigator.serviceWorker.getRegistration("/");
}
/** Текущее разрешение браузера: "granted"|"denied"|"default"|"unsupported" */
export function permissionState() {
if (!("Notification" in window)) return "unsupported";
return Notification.permission;
}
/** Активная подписка текущего браузера (null — нет). */
export async function currentSubscription() {
const reg = await swRegistration();
if (!reg) return null;
return reg.pushManager.getSubscription();
}
/**
* Договориться с пользователем о разрешении и подписать браузер.
* Возвращает объект для POST /me/push/subscriptions.
* Отклонённое ("denied") разрешение не повторяем — возвращает null.
*/
export async function subscribe(vapidPublicKey) {
if (!pushSupported()) throw new Error("Браузер не поддерживает web-push");
if (Notification.permission === "denied") return null;
if (Notification.permission !== "granted") {
const outcome = await Notification.requestPermission();
if (outcome !== "granted") return null;
}
const reg = await swRegistration();
if (!reg) throw new Error("Service worker ещё не готов (обновите страницу)");
const existing = await reg.pushManager.getSubscription();
const subscription =
existing ||
(await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
}));
const data = subscription.toJSON();
return {
endpoint: data.endpoint,
keys: data.keys,
ua: navigator.userAgent,
};
}
/** Отписать браузер от push (сервер — через removeSubscription). */
export async function unsubscribe() {
const reg = await swRegistration();
const subscription = reg && (await reg.pushManager.getSubscription());
if (subscription) await subscription.unsubscribe();
}