resources/js/admin/mobile/lib.
- Admin push is end‑to‑end: new backend model/migration/service/job + API endpoints, admin runtime config, push‑aware
service worker, and a settings toggle via useAdminPushSubscription. Notifications now auto‑refresh on push.
- New PHP/JS tests: admin push API feature test and queue/haptics unit tests
Added admin-specific PWA icon assets and wired them into the admin manifest, service worker, and admin shell, plus a
new “Device & permissions” card in mobile Settings with a persistent storage action and translations.
Details: public/manifest.json, public/admin-sw.js, resources/views/admin.blade.php, new icons in public/; new hook
resources/js/admin/mobile/hooks/useDevicePermissions.ts, helpers/tests in resources/js/admin/mobile/lib/
devicePermissions.ts + resources/js/admin/mobile/lib/devicePermissions.test.ts, and Settings UI updates in resources/
js/admin/mobile/SettingsPage.tsx with copy in resources/js/admin/i18n/locales/en/management.json and resources/js/
admin/i18n/locales/de/management.json.
171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import React from 'react';
|
|
import { getAdminPushConfig } from '../../lib/runtime-config';
|
|
import { registerAdminPushSubscription, unregisterAdminPushSubscription } from '../../api';
|
|
import { getAdminDeviceId } from '../../lib/device';
|
|
|
|
type PushSubscriptionState = {
|
|
supported: boolean;
|
|
permission: NotificationPermission;
|
|
subscribed: boolean;
|
|
loading: boolean;
|
|
error: string | null;
|
|
enable: () => Promise<void>;
|
|
disable: () => Promise<void>;
|
|
refresh: () => Promise<void>;
|
|
};
|
|
|
|
export function useAdminPushSubscription(): PushSubscriptionState {
|
|
const pushConfig = React.useMemo(() => getAdminPushConfig(), []);
|
|
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) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
const current = await registration.pushManager.getSubscription();
|
|
setSubscription(current);
|
|
setPermission(Notification.permission);
|
|
} catch (err) {
|
|
console.warn('Unable to refresh admin push subscription', err);
|
|
setSubscription(null);
|
|
}
|
|
}, [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) {
|
|
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 registerAdminPushSubscription(existing, getAdminDeviceId());
|
|
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 registerAdminPushSubscription(newSubscription, getAdminDeviceId());
|
|
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);
|
|
}
|
|
}, [pushConfig.vapidPublicKey, refresh, supported]);
|
|
|
|
const disable = React.useCallback(async () => {
|
|
if (!supported || !subscription) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await unregisterAdminPushSubscription(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);
|
|
}
|
|
}, [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;
|
|
}
|