dstpabuaran.com/resources/js/hooks/use-push-notification.ts
Yoga Pangestu 95be00c9d1 feat: add push notification functionality and service worker
- Implemented push notifications with a service worker (sw.ts) to handle caching and notifications.
- Added API routes for push subscription and notification management in routes/api.php.
- Created NotificationTest to validate notification service functionality and ensure correct notifications are sent based on user roles and actions.
- Updated Permissions component to manage notification permissions and subscriptions.
- Enhanced CategoryIndex component to highlight categories based on notifications.
- Excluded service worker from TypeScript compilation in tsconfig.json.
- Updated Vite configuration to include service worker in the build process.
2026-08-01 11:36:20 +07:00

150 lines
4.6 KiB
TypeScript

import { useCallback, useState } from 'react';
type PermissionStatus = 'granted' | 'denied' | 'prompt' | 'unknown';
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; i++) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function getInitialPermission(): PermissionStatus {
if (!('Notification' in window)) {
return 'denied';
}
return Notification.permission as PermissionStatus;
}
function getIsSupported(): boolean {
return (
'Notification' in window &&
'serviceWorker' in navigator &&
'PushManager' in window
);
}
export function usePushNotification() {
const [permission, setPermission] =
useState<PermissionStatus>(getInitialPermission);
const [isSupported] = useState<boolean>(getIsSupported);
const requestPermission =
useCallback(async (): Promise<PermissionStatus> => {
if (!('Notification' in window)) {
return 'denied';
}
const result = await Notification.requestPermission();
setPermission(result as PermissionStatus);
return result as PermissionStatus;
}, []);
const subscribe = useCallback(
async (vapidPublicKey: string): Promise<boolean> => {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
vapidPublicKey,
) as BufferSource,
});
const { endpoint } = subscription;
const key = subscription.getKey('p256dh');
const auth = subscription.getKey('auth');
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': decodeURIComponent(
document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ??
'',
),
},
body: JSON.stringify({
endpoint,
public_key: key
? btoa(String.fromCharCode(...new Uint8Array(key)))
: null,
auth_token: auth
? btoa(String.fromCharCode(...new Uint8Array(auth)))
: null,
content_encoding: 'aes128gcm',
}),
});
return response.ok;
} catch (error) {
console.error(
'Failed to subscribe to push notifications:',
error,
);
return false;
}
},
[],
);
const unsubscribe = useCallback(async (): Promise<boolean> => {
try {
const registration = await navigator.serviceWorker.ready;
const subscription =
await registration.pushManager.getSubscription();
if (!subscription) {
return true;
}
const response = await fetch('/api/push/unsubscribe', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': decodeURIComponent(
document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? '',
),
},
body: JSON.stringify({
endpoint: subscription.endpoint,
}),
});
if (response.ok) {
await subscription.unsubscribe();
}
return response.ok;
} catch (error) {
console.error(
'Failed to unsubscribe from push notifications:',
error,
);
return false;
}
}, []);
return {
permission,
isSupported,
requestPermission,
subscribe,
unsubscribe,
};
}