Files
fotospiel-app/resources/js/guest/hooks/usePushSubscription.ts

169 lines
4.9 KiB
TypeScript

import React from 'react';
import { getPushConfig } from '../lib/runtime-config';
import { registerPushSubscription, unregisterPushSubscription } from '../services/pushApi';
type PushSubscriptionState = {
supported: boolean;
permission: NotificationPermission;
subscribed: boolean;
loading: boolean;
error: string | null;
enable: () => Promise<void>;
disable: () => Promise<void>;
refresh: () => Promise<void>;
};
export function usePushSubscription(eventToken?: string): PushSubscriptionState {
const pushConfig = React.useMemo(() => getPushConfig(), []);
const supported = React.useMemo(() => {
return typeof window !== 'undefined'
&& typeof navigator !== 'undefined'
&& typeof Notification !== 'undefined'
&& 'serviceWorker' in navigator
&& 'PushManager' in window
&& pushConfig.enabled;
}, [pushConfig.enabled]);
const [permission, setPermission] = React.useState<NotificationPermission>(() => {
if (typeof Notification === 'undefined') {
return 'default';
}
return Notification.permission;
});
const [subscription, setSubscription] = React.useState<PushSubscription | null>(null);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const refresh = React.useCallback(async () => {
if (!supported || !eventToken) {
return;
}
try {
const registration = await navigator.serviceWorker.ready;
const current = await registration.pushManager.getSubscription();
setSubscription(current);
} catch (err) {
console.warn('Unable to refresh push subscription', err);
setSubscription(null);
}
}, [eventToken, supported]);
React.useEffect(() => {
if (!supported) {
return;
}
void refresh();
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'push-subscription-change') {
void refresh();
}
};
navigator.serviceWorker?.addEventListener('message', handleMessage);
return () => {
navigator.serviceWorker?.removeEventListener('message', handleMessage);
};
}, [refresh, supported]);
const enable = React.useCallback(async () => {
if (!supported || !eventToken) {
setError('Push-Benachrichtigungen werden auf diesem Gerät nicht unterstützt.');
return;
}
setLoading(true);
setError(null);
try {
const permissionResult = await Notification.requestPermission();
setPermission(permissionResult);
if (permissionResult !== 'granted') {
throw new Error('Bitte erlaube Benachrichtigungen, um Push zu aktivieren.');
}
const registration = await navigator.serviceWorker.ready;
const existing = await registration.pushManager.getSubscription();
if (existing) {
await registerPushSubscription(eventToken, existing);
setSubscription(existing);
return;
}
if (!pushConfig.vapidPublicKey) {
throw new Error('Push-Konfiguration ist nicht vollständig.');
}
const newSubscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(pushConfig.vapidPublicKey).buffer as ArrayBuffer,
});
await registerPushSubscription(eventToken, newSubscription);
setSubscription(newSubscription);
} catch (err) {
const message = err instanceof Error ? err.message : 'Push konnte nicht aktiviert werden.';
setError(message);
console.error(err);
await refresh();
} finally {
setLoading(false);
}
}, [eventToken, pushConfig.vapidPublicKey, refresh, supported]);
const disable = React.useCallback(async () => {
if (!supported || !eventToken || !subscription) {
return;
}
setLoading(true);
setError(null);
try {
await unregisterPushSubscription(eventToken, subscription.endpoint);
await subscription.unsubscribe();
setSubscription(null);
} catch (err) {
const message = err instanceof Error ? err.message : 'Push konnte nicht deaktiviert werden.';
setError(message);
console.error(err);
} finally {
setLoading(false);
}
}, [eventToken, subscription, supported]);
return {
supported,
permission,
subscribed: Boolean(subscription),
loading,
error,
enable,
disable,
refresh,
};
}
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = typeof window !== 'undefined'
? window.atob(base64)
: Buffer.from(base64, 'base64').toString('binary');
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; i += 1) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}