Corp/frontend/src/lib/push.ts

195 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Web Push утилиты для управления подписками
*/
import { registerAppServiceWorker, registerMessengerServiceWorker } from "@/lib/pwa";
import type { NotificationAppKind } from "@/lib/notifications/installations";
/**
* Конвертирует base64 строку в Uint8Array для applicationServerKey
*/
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
/**
* Проверяет поддержку Web Push браузером
*/
export function isPushSupported(): boolean {
if (typeof window === 'undefined') return false;
// В development Service Worker намеренно отключён и удаляется в pwa.ts.
// Не запускаем Web Push инициализацию, которая заведомо завершится ошибкой.
if (process.env.NODE_ENV !== 'production') return false;
if (!('serviceWorker' in navigator)) {
console.warn('[PushNotifications] Service Worker не поддерживается');
return false;
}
if (!('PushManager' in window)) {
console.warn('[PushNotifications] Push API не поддерживается');
return false;
}
if (!('Notification' in window)) {
console.warn('[PushNotifications] Notification API не поддерживается');
return false;
}
return true;
}
/**
* Получает текущее разрешение на уведомления
*/
export function getNotificationPermission(): NotificationPermission {
if (typeof window === 'undefined' || !('Notification' in window)) {
return 'default';
}
return Notification.permission;
}
/**
* Запрашивает разрешение на уведомления
*/
export async function requestNotificationPermission(): Promise<NotificationPermission> {
if (typeof window === 'undefined' || !('Notification' in window)) {
return 'default';
}
return await Notification.requestPermission();
}
/**
* Регистрирует Service Worker
*/
export async function registerServiceWorker(
appKind: NotificationAppKind = "main",
): Promise<ServiceWorkerRegistration> {
const registration = appKind === "messenger"
? await registerMessengerServiceWorker()
: await registerAppServiceWorker();
if (!registration) {
throw new Error('Service Worker не поддерживается');
}
console.log('[PushNotifications] Service Worker зарегистрирован:', registration.scope);
return registration;
}
/**
* Получает или создает push подписку
*/
export async function subscribeToPush(
registration: ServiceWorkerRegistration,
vapidPublicKey: string
): Promise<PushSubscription> {
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey as BufferSource
});
console.log('[PushNotifications] Push подписка создана');
return subscription;
}
/**
* Отписывается от push уведомлений
*/
export async function unsubscribeFromPush(
registration: ServiceWorkerRegistration
): Promise<boolean> {
const subscription = await registration.pushManager.getSubscription();
if (!subscription) {
return false;
}
const success = await subscription.unsubscribe();
console.log('[PushNotifications] Push подписка удалена');
return success;
}
/**
* Remove every Web Push subscription owned by this origin in the browser.
* This is intentionally local-only so it can also run after an auth session
* expires and the API can no longer authenticate the user.
*/
export async function unsubscribeAllLocalPushSubscriptions(): Promise<number> {
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
return 0;
}
const registrations = await navigator.serviceWorker.getRegistrations();
const shownNotifications = await Promise.all(
registrations.map((registration) =>
registration.getNotifications().catch(() => []),
),
);
shownNotifications.flat().forEach((notification) => notification.close());
const subscriptions = await Promise.all(
registrations.map((registration) => registration.pushManager.getSubscription()),
);
const activeSubscriptions = subscriptions.filter(
(subscription): subscription is PushSubscription => subscription !== null,
);
const results = await Promise.allSettled(
activeSubscriptions.map((subscription) => subscription.unsubscribe()),
);
return results.filter(
(result) => result.status === 'fulfilled' && result.value,
).length;
}
/**
* Получает текущую push подписку
*/
export async function getCurrentSubscription(
registration: ServiceWorkerRegistration
): Promise<PushSubscription | null> {
return await registration.pushManager.getSubscription();
}
/**
* Конвертирует PushSubscription в формат для отправки на сервер
*/
export function serializeSubscription(subscription: PushSubscription) {
const key = subscription.getKey('p256dh');
const token = subscription.getKey('auth');
return {
endpoint: subscription.endpoint,
keys: {
p256dh: key ? btoa(String.fromCharCode(...new Uint8Array(key))) : '',
auth: token ? btoa(String.fromCharCode(...new Uint8Array(token))) : ''
}
};
}
export async function sendNotificationControl(
action: string,
scope: Record<string, unknown>,
): Promise<void> {
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
const registrations = await navigator.serviceWorker.getRegistrations().catch(() => []);
for (const registration of registrations) {
const worker = registration.active || registration.waiting || registration.installing;
worker?.postMessage({ type: 'NOTIFICATION_CONTROL', action, scope });
}
}