Newer
Older
gn-synapse / frontend / src / push.js
/** 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();
}