mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
feat: add web push notifications support
Implement push notification system with VAPID keys, client service worker integration, and push subscription management. Includes server-side PushService for sending notifications and PushNotifier for reactive event handling, plus client-side usePushNotifications hook and service worker support.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
|
||||
function isPushSupported(): boolean {
|
||||
return typeof window !== 'undefined'
|
||||
&& 'serviceWorker' in navigator
|
||||
&& 'PushManager' in window
|
||||
&& 'Notification' in window
|
||||
}
|
||||
|
||||
function base64UrlToUint8Array(base64Url: string): Uint8Array {
|
||||
const padding = '='.repeat((4 - (base64Url.length % 4)) % 4)
|
||||
const base64 = (base64Url + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
const raw = atob(base64)
|
||||
const output = new Uint8Array(raw.length)
|
||||
for (let i = 0; i < raw.length; i += 1) {
|
||||
output[i] = raw.charCodeAt(i)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function usePushNotifications(api: ApiClient | null) {
|
||||
const [isSupported, setIsSupported] = useState(false)
|
||||
const [permission, setPermission] = useState<NotificationPermission>('default')
|
||||
const [isSubscribed, setIsSubscribed] = useState(false)
|
||||
|
||||
const refreshSubscription = useCallback(async () => {
|
||||
if (!isPushSupported()) {
|
||||
setIsSupported(false)
|
||||
setIsSubscribed(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSupported(true)
|
||||
setPermission(Notification.permission)
|
||||
|
||||
if (Notification.permission !== 'granted') {
|
||||
setIsSubscribed(false)
|
||||
return
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
const subscription = await registration.pushManager.getSubscription()
|
||||
setIsSubscribed(Boolean(subscription))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void refreshSubscription()
|
||||
}, [refreshSubscription])
|
||||
|
||||
const requestPermission = useCallback(async (): Promise<boolean> => {
|
||||
if (!isPushSupported()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const result = await Notification.requestPermission()
|
||||
setPermission(result)
|
||||
if (result !== 'granted') {
|
||||
setIsSubscribed(false)
|
||||
}
|
||||
return result === 'granted'
|
||||
}, [])
|
||||
|
||||
const subscribe = useCallback(async (): Promise<boolean> => {
|
||||
if (!api || !isPushSupported()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Notification.permission !== 'granted') {
|
||||
setPermission(Notification.permission)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
const existing = await registration.pushManager.getSubscription()
|
||||
const { publicKey } = await api.getPushVapidPublicKey()
|
||||
const applicationServerKey = base64UrlToUint8Array(publicKey).buffer as ArrayBuffer
|
||||
const subscription = existing ?? await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey
|
||||
})
|
||||
|
||||
const json = subscription.toJSON()
|
||||
const keys = json.keys
|
||||
if (!json.endpoint || !keys?.p256dh || !keys.auth) {
|
||||
return false
|
||||
}
|
||||
|
||||
await api.subscribePushNotifications({
|
||||
endpoint: json.endpoint,
|
||||
keys: {
|
||||
p256dh: keys.p256dh,
|
||||
auth: keys.auth
|
||||
}
|
||||
})
|
||||
setIsSubscribed(true)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('[PushNotifications] Failed to subscribe:', error)
|
||||
return false
|
||||
}
|
||||
}, [api])
|
||||
|
||||
const unsubscribe = useCallback(async (): Promise<boolean> => {
|
||||
if (!api || !isPushSupported()) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
const subscription = await registration.pushManager.getSubscription()
|
||||
if (!subscription) {
|
||||
setIsSubscribed(false)
|
||||
return true
|
||||
}
|
||||
|
||||
const endpoint = subscription.endpoint
|
||||
const success = await subscription.unsubscribe()
|
||||
await api.unsubscribePushNotifications({ endpoint })
|
||||
setIsSubscribed(false)
|
||||
return success
|
||||
} catch (error) {
|
||||
console.error('[PushNotifications] Failed to unsubscribe:', error)
|
||||
return false
|
||||
}
|
||||
}, [api])
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
permission,
|
||||
isSubscribed,
|
||||
requestPermission,
|
||||
subscribe,
|
||||
unsubscribe
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user