Corp/frontend/src/hooks/useWebPush.ts

198 lines
7.2 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.

/**
* React хук для управления Web Push уведомлениями
*/
import { useState, useEffect, useCallback } from 'react';
import {
isPushSupported,
getNotificationPermission,
requestNotificationPermission,
registerServiceWorker,
subscribeToPush,
unsubscribeFromPush,
getCurrentSubscription,
serializeSubscription,
} from '@/lib/push';
import { apiClient } from '@/lib/api';
import { toast } from 'sonner';
import {
getDeviceGroupId,
getDeviceName,
getInstallationId,
isMessengerLaunch,
type NotificationAppKind,
} from '@/lib/notifications/installations';
interface UseWebPushReturn {
isSupported: boolean;
isSubscribed: boolean;
isLoading: boolean;
permission: NotificationPermission;
subscribe: () => Promise<boolean>;
unsubscribe: () => Promise<boolean>;
requestPermission: () => Promise<NotificationPermission>;
}
/**
* Хук для управления Web Push подписками
*/
export function useWebPush({
appKind = 'main',
enabled = true,
}: {
appKind?: NotificationAppKind;
enabled?: boolean;
} = {}): UseWebPushReturn {
const [isSupported] = useState(() => isPushSupported());
const [isSubscribed, setIsSubscribed] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [permission, setPermission] = useState<NotificationPermission>('default');
const [swRegistration, setSwRegistration] = useState<ServiceWorkerRegistration | null>(null);
const [vapidPublicKey, setVapidPublicKey] = useState<string | null>(null);
// Инициализация: проверка поддержки и текущего статуса
useEffect(() => {
if (!enabled || !isSupported) {
setIsLoading(false);
return;
}
async function init() {
try {
setIsLoading(true);
// Получаем разрешение
const perm = getNotificationPermission();
setPermission(perm);
// Получаем VAPID ключ
const response = await apiClient.getVapidPublicKey();
setVapidPublicKey(response.vapid_public_key);
// Регистрируем Service Worker
const registration = await registerServiceWorker(appKind);
setSwRegistration(registration);
// Проверяем текущую подписку
const subscription = await getCurrentSubscription(registration);
setIsSubscribed(subscription !== null);
if (appKind === 'main' || isMessengerLaunch()) {
await apiClient.heartbeatNotificationInstallation({
installation_id: getInstallationId(appKind),
device_group_id: getDeviceGroupId(),
app_kind: appKind,
device_name: getDeviceName(),
installed: appKind === 'messenger' ? isMessengerLaunch() : true,
});
}
console.log('[useWebPush] Инициализация завершена, подписка:', subscription !== null);
} catch (error) {
console.error('[useWebPush] Ошибка инициализации:', error);
toast.error('Не удалось инициализировать уведомления');
} finally {
setIsLoading(false);
}
}
init();
}, [appKind, enabled, isSupported]);
// Запрос разрешения на уведомления
const requestPermission = useCallback(async (): Promise<NotificationPermission> => {
const perm = await requestNotificationPermission();
setPermission(perm);
return perm;
}, []);
// Подписаться на push уведомления
const subscribe = useCallback(async (): Promise<boolean> => {
if (!isSupported || !swRegistration || !vapidPublicKey) {
toast.error('Web Push не поддерживается');
return false;
}
setIsLoading(true);
try {
// Запрашиваем разрешение если нужно
if (permission !== 'granted') {
const perm = await requestPermission();
if (perm !== 'granted') {
toast.error('Разрешение на уведомления отклонено');
return false;
}
}
// Создаем подписку
const subscription = await subscribeToPush(swRegistration, vapidPublicKey);
const subscriptionData = serializeSubscription(subscription);
// Отправляем на сервер
await apiClient.subscribePush({
...subscriptionData,
device_name: getDeviceName(),
installation_id: getInstallationId(appKind),
device_group_id: getDeviceGroupId(),
app_kind: appKind,
installed: appKind === 'main' || isMessengerLaunch(),
});
setIsSubscribed(true);
toast.success('Подписка на уведомления активирована');
return true;
} catch (error) {
console.error('[useWebPush] Ошибка подписки:', error);
toast.error('Не удалось подписаться на уведомления');
return false;
} finally {
setIsLoading(false);
}
}, [appKind, isSupported, swRegistration, vapidPublicKey, permission, requestPermission]);
// Отписаться от push уведомлений
const unsubscribe = useCallback(async (): Promise<boolean> => {
if (!isSupported || !swRegistration) {
return false;
}
setIsLoading(true);
try {
// Получаем текущую подписку для endpoint
const subscription = await getCurrentSubscription(swRegistration);
const endpoint = subscription?.endpoint;
// Удаляем локальную подписку
const success = await unsubscribeFromPush(swRegistration);
if (success && endpoint) {
// Удаляем на сервере только это конкретное устройство
await apiClient.unsubscribePush(endpoint);
setIsSubscribed(false);
toast.success('Подписка отменена');
return true;
}
return false;
} catch (error) {
console.error('[useWebPush] Ошибка отписки:', error);
toast.error('Не удалось отписаться от уведомлений');
return false;
} finally {
setIsLoading(false);
}
}, [isSupported, swRegistration]);
return {
isSupported,
isSubscribed,
isLoading,
permission,
subscribe,
unsubscribe,
requestPermission,
};
}