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.
98 lines
2.5 KiB
TypeScript
98 lines
2.5 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
normalizePermissionState,
|
|
resolveStorageStatus,
|
|
type PermissionStatus,
|
|
type StorageStatus,
|
|
} from '../lib/devicePermissions';
|
|
|
|
type DevicePermissionsState = {
|
|
notifications: PermissionStatus;
|
|
camera: PermissionStatus;
|
|
storage: StorageStatus;
|
|
};
|
|
|
|
type DevicePermissionsHook = DevicePermissionsState & {
|
|
loading: boolean;
|
|
refresh: () => Promise<void>;
|
|
requestPersistentStorage: () => Promise<boolean>;
|
|
};
|
|
|
|
export function useDevicePermissions(): DevicePermissionsHook {
|
|
const [permissions, setPermissions] = React.useState<DevicePermissionsState>({
|
|
notifications: 'unsupported',
|
|
camera: 'unsupported',
|
|
storage: 'unsupported',
|
|
});
|
|
const [loading, setLoading] = React.useState(true);
|
|
|
|
const refresh = React.useCallback(async () => {
|
|
setLoading(true);
|
|
|
|
try {
|
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
let notificationState: PermissionStatus = 'unsupported';
|
|
if ('Notification' in window) {
|
|
notificationState = normalizePermissionState(Notification.permission);
|
|
}
|
|
|
|
let cameraState: PermissionStatus = 'unsupported';
|
|
if (navigator.permissions?.query) {
|
|
try {
|
|
const cameraPermission = await navigator.permissions.query({
|
|
name: 'camera' as PermissionName,
|
|
});
|
|
cameraState = normalizePermissionState(cameraPermission.state);
|
|
} catch {
|
|
cameraState = 'unsupported';
|
|
}
|
|
}
|
|
|
|
const storageSupported = Boolean(navigator.storage?.persisted);
|
|
let persisted: boolean | null = null;
|
|
if (storageSupported) {
|
|
persisted = await navigator.storage.persisted();
|
|
}
|
|
|
|
setPermissions({
|
|
notifications: notificationState,
|
|
camera: cameraState,
|
|
storage: resolveStorageStatus(persisted, storageSupported),
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const requestPersistentStorage = React.useCallback(async () => {
|
|
if (typeof navigator === 'undefined' || !navigator.storage?.persist) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const granted = await navigator.storage.persist();
|
|
setPermissions((prev) => ({
|
|
...prev,
|
|
storage: granted ? 'persisted' : 'available',
|
|
}));
|
|
return granted;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
void refresh();
|
|
}, [refresh]);
|
|
|
|
return {
|
|
...permissions,
|
|
loading,
|
|
refresh,
|
|
requestPersistentStorage,
|
|
};
|
|
}
|