Added Phase‑1 continuation work across deep links, offline moderation queue, and admin push.
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.
This commit is contained in:
170
resources/js/admin/mobile/hooks/useAdminPushSubscription.ts
Normal file
170
resources/js/admin/mobile/hooks/useAdminPushSubscription.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user