169 lines
4.9 KiB
TypeScript
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;
|
|
}
|